Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Refactoring with SOLID Principles

Introduction

In our last lesson, we learned to act like detectives, identifying the "code smells" and anti-patterns that signal underlying design problems in a codebase. You learned to spot issues like God Classes, Feature Envy, and framework-specific anti-patterns in Spring Boot, all of which are symptoms of tight coupling and low cohesion. This diagnostic skill is the essential first step.

Now, we move from diagnosis to treatment. This lesson will equip you with the foundational framework for refactoring those tightly-coupled components into a clean, modular, and maintainable system: the SOLID principles. For any developer aspiring to a senior role, these five principles are not just buzzwords; they are the guiding philosophy behind writing professional, scalable, and resilient software. You will learn to apply each principle to systematically dismantle a monolithic piece of code, transforming it into a collection of well-defined, loosely-coupled components.

1. What Are the SOLID Principles?

The SOLID principles are a set of five design guidelines for object-oriented programming, introduced by Robert C. Martin ("Uncle Bob"). They are not rigid rules but rather heuristics that help you create systems that are easier to maintain, test, and extend over time. Adhering to them is a direct remedy for the code smells we discussed previously.

A visual summary of the five SOLID principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.

Before we dive into applying them, let's get a clear definition of each.

A Solid Guide to SOLID Principles

The Baeldung blog is a highly respected resource in the Java community, and this article provides a clear, concise overview of each SOLID principle with simple code examples.

Please read through the article, starting from the introduction and continuing through each of the five principles: Single Responsibility Open/Closed Liskov Substitution Interface Segregation Dependency Inversion Focus on understanding the core idea behind each principle. Don't worry about memorizing every detail; we will see them in action next.

For a more dynamic overview, the following video from Geekific provides an excellent animated explanation of the same concepts.

SOLID Principles in Java Explained | Object-Oriented Clean Code & Design | Geekific Remastered

This video provides a clear, animated walkthrough of each SOLID principle with Java examples.

Watch the entire video from the introduction through the explanation of Dependency Inversion to get a solid grasp of the five principles.

2. A Step-by-Step Refactoring with SOLID

Now, let's see how these principles are applied sequentially to refactor a realistic legacy system. We'll follow the journey of a monolithic BankAccount application as it's broken down into modular components.

This flowchart shows the process of refactoring a monolithic legacy application by applying each of the SOLID principles in order, from Single Responsibility to Dependency Inversion.

This step-by-step process is a powerful mental model for approaching your own refactoring challenges. We will use a detailed article that demonstrates this process with Java code.

Java — SOLID Patterns / Refactoring | by Araf Karsh Hamid

This article by Araf Karsh Hamid is an excellent, practical guide that refactors a single legacy Java class using each SOLID principle in turn. It's a perfect case study for your goal of modernizing tightly-coupled systems.

We will work through this article section by section. First, read the Problem Statement. This introduces the BankAccountLegacyService—a classic "God Class" that handles business logic, persistence, and notifications, which you'll recognize from our previous lesson. Next, we'll apply each principle: Single Responsibility Principle (SRP): Read section 1. SRP. Notice how the saveAccountDetails and sendNotification logic is extracted from the main service into dedicated BankAccountRepositoryService and BankNotificationService classes. This directly fixes the God Class smell. Open/Closed Principle (OCP): Read section 2. OCP. The initial calculateInterest method uses a switch statement, which would need modification every time a new account type is added. The refactoring introduces a BankAccountInterface and an AbstractBankAccountService, allowing new account types to be added by creating new classes that extend the functionality without modifying existing code. Liskov Substitution Principle (LSP): Read section 3. LSP. This principle is violated because a FixedDeposit account shouldn't have a deposit() method. The refactoring elegantly solves this by creating more granular interfaces: BankWithdrawalInterface for accounts that only allow withdrawals, and a BankDepositInterface that extends it for accounts allowing both. Interface Segregation Principle (ISP): Read section 4. ISP. Here, the BankAccountLegacyInterface is bloated with loan-related methods that not all bank accounts need. ISP is applied by splitting off a dedicated LoanAccountInterface. This ensures classes only implement methods that are relevant to them. Dependency Inversion Principle (DIP): Finally, read the explanation in section 5. DIP. This principle ties everything together. High-level modules (like your main application logic) should not depend on concrete low-level implementations (like a specific BankEmailNotificationService). Instead, both should depend on abstractions (like a NotificationInterface). This is the foundation of the dependency injection you use in Spring Boot, allowing you to easily swap implementations (e.g., from email to SMS notifications) without changing the core business logic.

3. SOLID in Practice: The Moves of a Senior Developer

The SOLID principles are the "why" behind good design. The practical refactoring techniques are the "how." In the previous lesson, you watched Sandro Mancuso refactor a piece of legacy Java code. Let's revisit some of his moves and see how they map directly to the SOLID principles we've just discussed.

Testing and Refactoring Legacy Code

Watching Sandro Mancuso's refactoring process again, but this time with the SOLID principles in mind, will connect the theory to tangible actions.

Consider these key moments from the video: Towards Dependency Inversion: Watch how he isolates a hardwired dependency on a static method. He first creates a "seam" by extracting a method. Later, he completely removes this dependency by passing the dependency as a parameter to the method. This is a manual way of achieving Dependency Inversion—making the high-level TripService depend on a loggedInUser object (an abstraction of data) rather than a concrete, static UserSession utility. Applying Single Responsibility: Remember the "Feature Envy" code smell? He fixes this by moving the friendship-checking logic to the User class itself, where it truly belongs. Watch his explanation of "Tell, Don't Ask" from this section. This is a direct application of the Single Responsibility Principle: the User class is responsible for managing its own relationships. Achieving Full Dependency Inversion: The final and most powerful step is when he changes the static TripDAO call to an instance method and injects it via the constructor. This allows him to use mocks for testing. This entire sequence, from creating the constructor to injecting the mock, is the quintessential application of the Dependency Inversion Principle. The TripService now depends on an abstraction (the TripDAO instance, which could be an interface) rather than a concrete, static helper.

Seeing these principles applied through concrete refactoring steps demonstrates that they are not abstract ideals but a practical toolkit for improving code quality day-to-day.

4. Practical Application: Plan Your Refactoring

In the last lesson's exercise, you identified code smells in your own project. Now, it's time to create a plan to fix one of them using SOLID.

  1. Select a Target: Choose one of the code smells you documented in your personal knowledge base (Notion/Obsidian). A "God Class" or a service with many hardcoded dependencies is an excellent candidate.
  2. Diagnose the Violation: Identify which SOLID principle(s) are being violated. Write a clear statement.
    • Example: "The OrderProcessor class violates the Single Responsibility Principle because it handles payment processing, inventory updates, and email notifications. It has three reasons to change."
  3. Propose a Refactoring Plan: Outline the steps you would take to refactor the code according to SOLID principles. You don't need to write the full implementation, but describe the new structure.
    • What new classes/interfaces would you create? (e.g., PaymentGatewayInterface, StripePaymentGateway, InventoryService, NotificationService).
    • How would dependencies change? (e.g., "The OrderProcessor would no longer create a StripeClient directly. Instead, it would depend on the PaymentGatewayInterface via constructor injection.").
  4. Document the Plan: Record your diagnosis and plan in your knowledge base under a new entry, "Refactoring Plan for [Your Class Name]". This serves as a "paper trail of competence" and a blueprint for future work.

This exercise shifts your thinking from just finding problems to strategically solving them, which is a hallmark of a senior developer's mindset.

Conclusion

Today, you've moved beyond simply identifying bad code and have learned the foundational design principles for creating good code. The SOLID principles are your guide to dismantling complex, tightly-coupled systems and reassembling them as modular, flexible, and maintainable components.

Key Takeaways:

  • Single Responsibility Principle (SRP): A class should have only one reason to change.
  • Open/Closed Principle (OCP): Software should be open for extension, but closed for modification.
  • Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering program behavior.
  • Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use.
  • Dependency Inversion Principle (DIP): High-level modules should depend on abstractions, not on concrete low-level implementations.

Mastering these principles is fundamental to your "Senior Developer Leap." They provide the vocabulary and mental models to design systems that can evolve gracefully, rather than collapsing under the weight of their own complexity.

In our next lesson, we will build upon this foundation by exploring Design Patterns. While SOLID principles give us the rules for good design, design patterns provide us with proven, reusable solutions to common problems within that ruleset. You'll see how patterns like Factory, Adapter, and Strategy are concrete implementations of the SOLID philosophy.

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

Sign up