Skip to main content
Create your own
Lesson illustration

Refactoring for SOLID Principles

Hello! Welcome back to our course on Low-Level Design.

In our last session, we trained ourselves to become code detectives, learning to spot the "code smells" that signal violations of the SOLID principles. We saw how God Classes, rigid switch statements, and surprising subclasses can make code fragile and hard to maintain.

Today, we switch hats from detective to architect. Our goal is to actively refactor code to improve its adherence to SOLID principles. This isn't just about cleaning up code; it's about making systems more modular, testable, and extensible—qualities highly valued in any system design interview. We will take the "before" code we learned to identify and transform it into the robust "after" version.

This process of refactoring is central to evolving a system. The diagram below illustrates how a monolithic application can be methodically broken down and improved by applying each SOLID principle in turn.

Applying SOLID Principles to a Bank Account Legacy App
This diagram from an article by Araf Karsh Hamid visualizes the journey from a monolithic design to a clean, SOLID architecture. We will follow a similar path in this lesson.

The Refactoring Workflow

For each of the five principles, we'll follow a simple workflow:

  1. Identify the Violation: Briefly recall the code smell.
  2. Analyze the Fix: Watch a practical demonstration of refactoring the problematic code.
  3. Understand the Impact: Discuss why the new design is better.

The following video provides excellent, interview-focused examples that we will use as our primary guide. The presenter shows "bad" code and walks through refactoring it into "good" code for each principle.

Learn SOLID Principles - Interview Questions

We will be using the video 'Learn SOLID Principles - Interview Questions' from the Daily Code Buffer channel as our main resource. It clearly demonstrates the refactoring process for each SOLID principle in a way that's highly relevant for interview preparation.

I will guide you to specific timestamps as we cover each principle. Keep this video open and ready to play.

Let's begin with the first principle.


1. Refactoring for the Single Responsibility Principle (SRP)

  • The Problem: A class is doing too much. It has multiple, unrelated responsibilities (e.g., business logic, persistence, notifications).
  • The Goal: Split the class into smaller, more cohesive classes, each with a single responsibility.

Watch how a class that handles both account operations and transaction operations is refactored.

Learn SOLID Principles - Interview Questions

This segment shows an AccountOperations class that also handles deposits. This violates SRP because account management and transactions are two different responsibilities.

Watch from 01:03 to 04:02. Observe how the deposit method is moved from AccountOperations to a new, dedicated TransactionOperations class.

Analysis of the Fix:

By splitting the original class, the new design is significantly better:

  • Maintainability: If you need to change how transactions work (e.g., add logging, interact with a payment gateway), you only modify TransactionOperations. The AccountOperations class remains untouched.
  • Testability: You can now write unit tests for TransactionOperations in complete isolation from account management logic, and vice-versa.
  • Clarity: The purpose of each class is now clear and unambiguous.

2. Refactoring for the Open/Closed Principle (OCP)

  • The Problem: A class must be modified every time a new variation of a feature is added. This is often signaled by a switch statement or a long if-else-if chain that checks an object's type.
  • The Goal: Refactor the design so you can add new functionality by adding new classes (extension) without changing existing, tested code (modification).

Now, let's see how to fix a calculator that needs modification every time you want to add a new mathematical operation.

Learn SOLID Principles - Interview Questions

The 'bad' Calculator class uses a switch statement to handle different operations. This violates OCP because adding 'multiplication' would require modifying the class.

Watch from 03:54 to 07:40. Focus on how an Operation interface is introduced, allowing new operations to be added as new classes that implement this interface.

Analysis of the Fix:

This refactoring introduces a classic design pattern: the Strategy pattern. The Operation interface defines a contract for an algorithm (in this case, a calculation). Concrete classes like AddOperation and SubtractOperation provide the specific implementations.

The Calculator class is now "closed" because its calculateNumber method doesn't need to change. It simply works with any object that satisfies the Operation contract. To add multiplication, you just create a MultiplicationOperation class. The system is now "open" for extension.

As a Spring Boot developer, you see this principle everywhere. When you define a new @Component or @Service, you are extending the application's capabilities without modifying the core Spring Framework code.


3. Refactoring for the Liskov Substitution Principle (LSP)

  • The Problem: A subclass cannot be used interchangeably with its parent class without causing errors. A common symptom is a subclass method that throws an UnsupportedOperationException.
  • The Goal: Restructure the class hierarchy or interfaces so that subtypes genuinely honor the contract of their base types.

Observe how a problematic LoanPayment hierarchy is refactored to be compliant with LSP.

Learn SOLID Principles - Interview Questions

In this example, CreditCardLoan cannot be forceClosed, forcing it to throw an exception and breaking the LoanPayment contract. This violates LSP.

Watch from 07:40 to 11:47. Notice that the solution isn't just about code, but about rethinking the abstractions. The concept of a SecuredLoan is introduced.

Analysis of the Fix:

The root cause of the violation was a flawed abstraction. The original LoanPayment interface incorrectly assumed that all loans share the same set of operations.

The fix involves creating a more accurate and granular set of abstractions:

  1. A base LoanPayment interface with methods common to all loans (e.g., doPayment).
  2. A more specific SecuredLoan interface that extends the base and adds methods specific to securable loans (e.g., forceCloseLoan).

Now, a HomeLoan can implement SecuredLoan, while a CreditCardLoan only needs to implement the base LoanPayment interface. The LoanClosureService can then safely depend on the SecuredLoan interface, knowing that any object it receives will have the forceCloseLoan method.

Test your understanding!

In the original "bad" design, imagine you have a method processEndOfYear(List<LoanPayment> allLoans). Inside this method, it iterates through the list and calls loan.forceCloseLoan(). What specific problem would occur when a CreditCardLoan object is in this list, and how does the refactored design prevent this?

Show answer

In the bad design, the processEndOfYear method would compile correctly, but it would crash at runtime with an UnsupportedOperationException as soon as it encountered a CreditCardLoan object.

The refactored design prevents this at compile time. The method signature would be changed to processEndOfYear(List<SecuredLoan> securableLoans). Now, you cannot even add a CreditCardLoan instance to this list, because it does not implement the SecuredLoan interface. The type system itself enforces the correctness of the program.


4. Refactoring for the Interface Segregation Principle (ISP)

  • The Problem: A large, "fat" interface forces implementing classes to provide methods they don't need, leading to empty implementations or exceptions.
  • The Goal: Break down large interfaces into smaller, role-specific interfaces. A class can then implement only the interfaces relevant to its function.

Let's see this in action with a Data Access Object (Dao) interface that tries to do too much.

Learn SOLID Principles - Interview Questions

The Dao interface in this example includes methods for both database and file operations, forcing every implementation to handle both, which is a clear ISP violation.

Watch from 11:47 to 16:19. Observe how the monolithic Dao is split into a base Dao interface plus smaller, specific interfaces like DBInterface and FileInterface.

Analysis of the Fix:

This refactoring is a direct application of the principle. Instead of one fat interface, we now have:

  • DaoInterface: Defines common operations like createRecord and deleteRecord.
  • DBInterface: Defines database-specific operations like openConnection.
  • FileInterface: Defines file-specific operations like openFile.

A class like DBDADaoConnection can now implement both DaoInterface and DBInterface, getting exactly the methods it needs without being polluted by irrelevant file operations. This makes the code cleaner and the intent of each class much clearer.


5. Refactoring for the Dependency Inversion Principle (DIP)

  • The Problem: A high-level module (e.g., a business service) directly depends on a low-level module (e.g., a concrete database class). This tight coupling makes the system rigid and hard to test.
  • The Goal: Invert the dependency. Both high-level and low-level modules should depend on abstractions (interfaces).

The video revisits the calculator example to demonstrate how to fix a DIP violation.

Learn SOLID Principles - Interview Questions

The initial 'bad' design has the Calculator class directly creating new AddOperation() and new SubOperation(). This couples the high-level calculator to low-level concrete operation classes.

Watch from 16:19 to 19:46. The fix should look very familiar to you from your Spring Boot experience. Notice how the dependency is now passed into the method—a form of dependency injection.

Analysis of the Fix:

Your background in Spring Boot makes this principle very intuitive. You rarely write new MyServiceImpl() inside your business logic. Instead, you declare a field of the interface type (MyService) and let the Spring container @Autowire (inject) the concrete implementation.

The refactoring in the video achieves the same goal. The Calculator's calculate method no longer creates its own dependencies. It receives an object of type CalculatorOperation (the abstraction). It doesn't know or care about the concrete implementation (AddOperation, SubtractOperation, etc.). This decouples the components, allowing you to easily substitute implementations—for example, with a mock object during testing.

A Comprehensive Case Study

We've looked at each principle in isolation. To see how they all work together to transform a larger application, the following article provides a complete, step-by-step refactoring of a legacy banking application.

Java — SOLID Patterns / Refactoring

The article 'Java — SOLID Patterns / Refactoring' by Araf Karsh Hamid is an excellent, in-depth case study. It takes a monolithic banking app and applies each SOLID principle to make it modular and maintainable.

You don't need to read this entire article right now, but I encourage you to review it after the lesson. It's a fantastic example of applying these principles to a real-world problem. Skim through the sections now to see how it applies SRP, OCP, LSP, and ISP to the banking domain.

This article reinforces everything we've discussed today with a consistent, practical example, solidifying your understanding of how these principles guide real-world software architecture.

Conclusion

Today we've moved from identifying problems to implementing solutions. By refactoring code to align with SOLID principles, we create systems that are not just correct, but also resilient to change, easier to test, and more understandable.

Key Takeaways & Refactoring Strategies:

  • SRP Violation: Fix by splitting a large class into smaller, single-purpose classes.
  • OCP Violation: Fix if/else or switch chains by introducing an abstraction (interface) and multiple concrete implementations (the Strategy pattern).
  • LSP Violation: Fix by redesigning your inheritance hierarchy or interfaces to ensure subtypes truly honor the parent contract.
  • ISP Violation: Fix fat interfaces by segregating them into smaller, role-specific interfaces.
  • DIP Violation: Fix tight coupling by depending on abstractions (interfaces) and using dependency injection, rather than new-ing up concrete low-level classes.

In an interview, being able to spot a SOLID violation in a proposed design and confidently suggesting a refactoring strategy is a powerful way to demonstrate your architectural maturity.

In our next module, we will begin our exploration of Creational Design Patterns, starting with the Singleton pattern. You'll see that many design patterns are, in fact, established, reusable solutions that help us adhere to the SOLID principles we've just mastered.

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

Sign up