Skip to main content
Create your own
Lesson illustration

SOLID Principles and Design Smells

Hello! Welcome back to your system design course.

In our last lesson, we saw how to incorporate powerful design patterns like Strategy, State, Singleton, and Factory to solve common problems in our designs. We learned that these patterns provide reusable, elegant solutions for building flexible and robust systems.

Today, we're going to take our design refinement skills to the next level. This lesson focuses on the learning outcome: Refine the design by applying SOLID principles and identifying design smells. We'll explore the fundamental principles that underpin good object-oriented design and learn to recognize the "warning signs"—or design smells—that indicate our design could be improved. Mastering this will not only make you a better engineer but also equip you to confidently justify your design choices in an interview.

1. The Foundation of Good Design: SOLID Principles

You've already seen how design patterns help implement good design practices. The SOLID principles are the formal rules that define these practices. Coined by Robert C. Martin ("Uncle Bob"), they are a set of five guidelines that help us create systems that are easy to maintain, extend, and understand.

To get a comprehensive overview, let's watch a video that explains these principles with Java examples, keeping the context of a system design interview in mind.

SOLID principles explained | Java | System Design Interview

The video 'SOLID principles explained' by ByteMonk provides a clear and practical walkthrough of all five SOLID principles. It uses simple Java examples and directly relates the concepts to what's expected in system design interviews.

Please watch the entire video (from 00:00 to 11:14). As you watch, try to connect each principle to your own experience with Java and the design patterns we discussed in the last lesson.

Now that you have a solid overview, let's break down each principle and reinforce it with another excellent resource.

For a deeper dive with more code examples, we'll refer to the highly-regarded Baeldung article on SOLID.

A Solid Guide to SOLID Principles

The article 'A Solid Guide to SOLID Principles' from Baeldung is a classic reference for these concepts. It provides alternative examples that will help solidify your understanding.

Read sections 2 through 7. For each principle, compare the example here with the one you saw in the video. This will give you a broader perspective on how each principle can be applied.

Let's summarize and connect these principles to what we've learned.

  • S - Single Responsibility Principle (SRP): A class should have only one reason to change.

    • In short: Each class should do one thing and do it well.
    • Connection: When we designed the Parking Lot, we didn't put payment logic and spot assignment logic in the same class. The video's example of splitting a monolithic Employee class into PayCalculator, EmployeeRepository, and EmployeeReportGenerator is a perfect illustration. Each has a single, focused responsibility.
  • O - Open/Closed Principle (OCP): Software entities should be open for extension, but closed for modification.

    • In short: You should be able to add new functionality without changing existing code.
    • Connection: This is the primary benefit of the Strategy pattern. In our e-commerce shipping example from the last lesson, we could add a new USPS_ShippingStrategy without ever touching the ShoppingCart class. The system is open to new strategies but the ShoppingCart code is closed to modification.
  • L - Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

    • In short: A subclass must be a true, substitutable replacement for its parent.
    • Connection: The Baeldung example with MotorCar and ElectricCar is classic. If your code expects a Car and calls turnOnEngine(), it shouldn't crash when you pass it an ElectricCar. The subclass shouldn't break the parent's contract. This principle ensures polymorphism works as expected.
  • I - Interface Segregation Principle (ISP): Many client-specific interfaces are better than one general-purpose interface.

    • In short: Don't force clients to implement interfaces they don't use.
    • Connection: Imagine a Worker interface with methods for work(), takeLunchBreak(), and attendMeeting(). If you have a RobotWorker class, forcing it to implement takeLunchBreak() is awkward. ISP suggests splitting the large interface into smaller, more focused ones like Workable, Breakable, etc.
  • D - Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions.

    • In short: Depend on interfaces, not concrete classes.
    • Connection: This is the heart of the Spring framework you use. Through dependency injection (@Autowired), your services depend on interfaces (e.g., UserRepository) rather than concrete implementations (new UserRepositoryImpl()). This decouples your components, making them easier to test (by mocking the interface) and swap out. The Strategy and State patterns are also perfect examples of DIP in action.

The following image provides a great visual summary of how you might apply these principles to refactor a legacy application.

Applying SOLID Principles to a Banking Application
This flowchart shows the process of applying each SOLID principle sequentially to refactor a monolithic bank account application into a well-structured, maintainable system.

2. Identifying Design Smells

If SOLID principles are the rules for good design, then design smells are the symptoms of bad design. A "smell" isn't a bug; it's a characteristic in the code that suggests a deeper problem. Recognizing these smells is the first step to refining your design.

Let's explore some of the most common smells you might encounter in an LLD interview.

Code Smells: A Solution Architect's Guide

This guide from 'Solutions Architecture' provides an excellent, detailed breakdown of various code smells, categorized into logical groups. It includes clear 'before' and 'after' Java examples for refactoring.

Please read the introduction and the sections on 'Bloaters', 'Object-Oriented Abusers', and 'Couplers'. Pay close attention to the following smells, as they are most critical for LLD: Large Class / God Object: Notice how this is a direct violation of SRP. Primitive Obsession: A subtle but important smell. Think about why creating an Address class is better than using eight separate String fields. Switch Statements: See how this often points to a missed opportunity for the Strategy or State pattern, violating OCP. Feature Envy: A great indicator that a method is in the wrong class. Message Chains: Understand how this violates the Law of Demeter and creates tight coupling.

Let's summarize the key smells and their link to SOLID principles:

  • Bloaters (e.g., God Class, Long Method): These are classes or methods that have grown too large and taken on too many responsibilities. This is a clear sign of SRP violation.
  • Object-Oriented Abusers (e.g., Switch Statements, Refused Bequest): These indicate a misuse of OO features. A complex switch statement often signals an OCP violation (you have to modify it to add new cases). A Refused Bequest (subclass doesn't want/need inherited functionality) is often an LSP violation.
  • Couplers (e.g., Feature Envy, Message Chains): These smells indicate tight coupling between classes. Message chains like a.getB().getC().doSomething() expose the internal structure of objects and violate the Law of Demeter ("Principle of Least Knowledge"), which is closely related to DIP and creating loosely coupled systems.

This mind map can help you remember the different categories of code smells.

Types of Code Smells Mind Map
A mind map categorizing various code smells. Recognizing these categories—like Bloaters, Couplers, and OO Abusers—helps you quickly spot potential design issues.
Test your understanding!

You are reviewing the following code for a document processing system.

class DocumentManager {
    private String content;

    public DocumentManager(String content) {
        this.content = content;
    }

    // Exports the document to different formats
    public void export(String format) {
        if (format.equals("PDF")) {
            // Logic to convert content to PDF format
            System.out.println("Exporting to PDF...");
        } else if (format.equals("Word")) {
            // Logic to convert content to Word format
            System.out.println("Exporting to Word DOCX...");
        } else if (format.equals("JSON")) {
            // Logic to convert content to JSON
            System.out.println("Exporting to JSON...");
        } else {
            throw new IllegalArgumentException("Unsupported format");
        }
    }
}

Identify at least two SOLID principles being violated and the corresponding design smell. How would you refactor this?

Show answer

Violations and Smells:

  1. Open/Closed Principle (OCP) Violation: To add a new export format (e.g., "HTML"), you must modify the export method's if-else block. The class is not closed for modification. The design smell here is the Switch Statement (or in this case, a long if-else chain).
  2. Single Responsibility Principle (SRP) Violation: The DocumentManager class has multiple reasons to change. A change in PDF export logic, Word export logic, or JSON export logic would all require modifying this class. Its single responsibility should be managing the document's content, not handling the specifics of every possible export format.

Refactoring:

You would use the Strategy Pattern.

  1. Create an Exporter interface:

    interface Exporter {
        void export(String content);
    }
    
  2. Create concrete strategy classes for each format:

    class PdfExporter implements Exporter {
        public void export(String content) {
            System.out.println("Exporting to PDF...");
        }
    }
    
    class WordExporter implements Exporter {
        public void export(String content) {
            System.out.println("Exporting to Word DOCX...");
        }
    }
    
    // etc. for JSON, HTML...
    
  3. The DocumentManager would then delegate the export task to a strategy object, adhering to OCP, SRP, and DIP.

    class DocumentManager {
        private String content;
    
        public DocumentManager(String content) {
            this.content = content;
        }
    
        public void export(Exporter exporter) {
            exporter.export(this.content);
        }
    }
    

Conclusion

Great job! Today, you've learned to look at a design with a critical eye. By understanding SOLID principles and recognizing design smells, you can move beyond just a "working" design to one that is truly robust, maintainable, and extensible—qualities that are highly valued in any top engineering team.

Key Takeaways:

  • SOLID principles are the blueprint for good design. They guide you toward creating decoupled, cohesive, and flexible components.
  • Design patterns are practical implementations of SOLID principles. The Strategy pattern embodies OCP and DIP, while the State pattern also adheres to SRP and OCP.
  • Design smells are red flags. They signal that a principle has likely been violated and your design could be improved through refactoring.
  • Refinement is an iterative process. You apply principles, remove smells, and use patterns to continuously improve your design until it is clean and robust.

In our next and final lesson for this module, we'll put everything together. We will develop a strategy for communicating a low-level design solution in an interview setting. You'll learn how to articulate your requirements, present your class diagrams, and justify your design choices using the principles and patterns you've now mastered.

Can't find a good explanation? Sign up and we'll make it for you

Sign up