Full Stack • Java • System Design • Cloud • AI Engineering

SOLID Refactoring Case Study - From Legacy Code to Clean Architecture

Learn how to refactor a legacy Java Spring Boot application using SOLID principles. Explore step-by-step improvements, architecture evolution, design decisions, refactoring strategies, code smells, and enterprise best practices.


SOLID Refactoring Case Study


Introduction

Most developers don't start new projects.

Instead, they maintain existing enterprise applications that have evolved over many years.

These applications often contain:

  • Thousands of classes
  • Hundreds of APIs
  • Legacy business logic
  • Tight coupling
  • Duplicate code
  • Complex dependencies

A common challenge is refactoring legacy code without breaking existing functionality.

This is where the SOLID Principles become extremely valuable.

In this article, we'll take a realistic Spring Boot application and gradually refactor it using all five SOLID principles.


The Business Scenario

Imagine an E-Commerce platform.

The application supports:

  • Customer Management
  • Order Processing
  • Inventory
  • Payments
  • Shipping
  • Notifications
  • Audit Logs

Initially, the project was small.

Over five years, dozens of developers added features without proper architectural boundaries.

Now every change introduces bugs.


Symptoms of Legacy Code

Developers notice:

  • Huge service classes
  • Long methods
  • Duplicate validation
  • Tight coupling
  • Difficult testing
  • Slow deployments
  • Frequent production bugs

Legacy Architecture

flowchart TD

Client

-->

OrderController

OrderController --> OrderService

OrderService --> Database

OrderService --> Payment

OrderService --> Email

OrderService --> Inventory

OrderService --> Kafka

OrderService --> Logging

OrderService --> PDF

OrderService --> SMS

One service handles everything.


Initial Order Service Responsibilities

The legacy OrderService performs:

  • Validate Request
  • Calculate Price
  • Save Order
  • Process Payment
  • Reserve Inventory
  • Generate Invoice
  • Send Email
  • Send SMS
  • Publish Kafka Event
  • Write Audit Log
  • Generate Report

This clearly violates multiple SOLID principles.


Step 1 — Identify Code Smells

Before refactoring, identify the problems.

Common code smells include:

  • God Class
  • Long Method
  • Duplicate Code
  • Feature Envy
  • Large Constructor
  • Shotgun Surgery
  • Primitive Obsession
  • Tight Coupling
  • Large Interfaces
  • Hardcoded Dependencies

Step 2 — Apply SRP

The first refactoring focuses on Single Responsibility Principle.

Split responsibilities.

flowchart LR

OrderService

-->

ValidationService

OrderService --> PaymentService

OrderService --> InventoryService

OrderService --> InvoiceService

OrderService --> NotificationService

OrderService --> AuditService

Each class now has one business responsibility.


Benefits After SRP

  • Smaller classes
  • Easier testing
  • Better readability
  • Easier maintenance

Step 3 — Apply OCP

Originally:

if(payment == CARD)

else if(payment == UPI)

else if(payment == PAYPAL)

Replace conditional logic with abstractions.

classDiagram

class PaymentProcessor

<<interface>> PaymentProcessor

class CardPayment

class UpiPayment

class PaypalPayment

PaymentProcessor <|.. CardPayment

PaymentProcessor <|.. UpiPayment

PaymentProcessor <|.. PaypalPayment

Adding another payment method requires a new implementation instead of modifying existing code.


Benefits After OCP

  • Easier extension
  • Lower regression risk
  • Cleaner business logic

Step 4 — Apply LSP

Every payment processor must behave consistently.

flowchart LR

OrderService

-->

PaymentProcessor

PaymentProcessor --> Card

PaymentProcessor --> UPI

PaymentProcessor --> Wallet

The order service should not know which implementation is executing.

Every processor must satisfy the same contract.


Benefits After LSP

  • Reliable polymorphism
  • Consistent behavior
  • Safer inheritance

Step 5 — Apply ISP

Originally:

NotificationService

↓

Email

↓

SMS

↓

Push

↓

WhatsApp

↓

Slack

Split interfaces.

classDiagram

class EmailSender

<<interface>>

class SmsSender

<<interface>>

class PushSender

<<interface>>

class WhatsAppSender

<<interface>>

Each client depends only on the operations it needs.


Benefits After ISP

  • Smaller interfaces
  • Easier implementation
  • Lower coupling

Step 6 — Apply DIP

Originally:

OrderService

↓

new StripePayment()

After refactoring:

flowchart LR

OrderService

-->

PaymentProcessor

PaymentProcessor --> Stripe

PaymentProcessor --> PayPal

PaymentProcessor --> Wallet

Spring injects the implementation.


Benefits After DIP

  • Loose coupling
  • Easier mocking
  • Easier cloud migration
  • Better testing

Architecture Evolution

Before Refactoring

flowchart TD

Controller

-->

Huge Order Service

Huge Order Service --> Database

Huge Order Service --> Email

Huge Order Service --> Payment

Huge Order Service --> Kafka

Huge Order Service --> Reports

After Refactoring

flowchart TD

Controller

-->

Order Service

Order Service --> Validation Service

Order Service --> Payment Service

Order Service --> Inventory Service

Order Service --> Notification Service

Order Service --> Invoice Service

Order Service --> Event Publisher

The architecture is modular and easier to maintain.


Spring Boot Layered Architecture

flowchart LR

REST Controller

-->

Business Service

Business Service --> Repository

Repository --> Database

Supporting services:

  • Validation
  • Notification
  • Audit
  • Event Publishing

remain independent.


Event-Driven Refactoring

Instead of directly sending notifications:

Save Order

↓

Send Email

↓

Send SMS

Publish an event.

flowchart LR

Order Created

-->

Kafka

Kafka --> Email Service

Kafka --> SMS Service

Kafka --> Analytics Service

New consumers can be added without changing the order service.


Repository Refactoring

Business logic should not contain SQL.

flowchart LR

Service

-->

Repository

Repository --> PostgreSQL

Persistence remains isolated.


Constructor Injection

Before:

new PaymentService()

new EmailService()

new InventoryService()

After:

Spring Container

↓

Inject Dependencies

↓

OrderService

This improves testability and follows DIP.


Enterprise Architecture

flowchart TD

Client

-->

API Gateway

API Gateway --> Order Service

Order Service --> Payment Interface

Order Service --> Inventory Interface

Order Service --> Notification Interface

Order Service --> Event Publisher

Event Publisher --> Kafka

Kafka --> Email Service

Kafka --> Analytics Service

Kafka --> Audit Service

Refactoring Strategy

A safe refactoring process:

  1. Add unit tests.
  2. Identify code smells.
  3. Extract responsibilities.
  4. Introduce interfaces.
  5. Apply dependency injection.
  6. Replace conditional logic with polymorphism.
  7. Extract events.
  8. Verify behavior.
  9. Optimize performance.
  10. Deploy incrementally.

Metrics Before Refactoring

Example indicators:

Metric Before
OrderService Lines 2,500
Methods 85
Dependencies 20
Unit Test Coverage 35%
Cyclomatic Complexity Very High

Metrics After Refactoring

Metric After
OrderService Lines 250
Methods 12
Dependencies 5
Unit Test Coverage 90%
Cyclomatic Complexity Low

Design Patterns Used

Pattern Purpose
Strategy Payment processing
Factory Object creation
Observer Event notifications
Repository Data access
Adapter External integrations
Command Business operations
Builder Complex object creation
Proxy Transactions and security

These patterns naturally complement SOLID.


Benefits of Refactoring

  • Better readability
  • Smaller classes
  • Better testing
  • Easier onboarding
  • Lower technical debt
  • Faster feature delivery
  • Better scalability
  • Easier cloud migration

Common Mistakes

❌ Refactoring without tests.

❌ Trying to rewrite everything at once.

❌ Overusing interfaces.

❌ Breaking backward compatibility.

❌ Mixing infrastructure with business logic.

❌ Ignoring performance after refactoring.


Best Practices

  • Refactor incrementally.
  • Keep changes small.
  • Add tests before refactoring.
  • Apply one SOLID principle at a time.
  • Use constructor injection.
  • Publish events instead of tight coupling.
  • Keep services focused.
  • Review code regularly.
  • Measure improvements.
  • Refactor continuously instead of waiting for a rewrite.

Interview Questions

  1. How would you refactor a God Class?
  2. Which SOLID principle would you apply first?
  3. Why is incremental refactoring safer?
  4. How does Spring Boot simplify refactoring?
  5. Which design patterns help during refactoring?
  6. Why should tests be written before refactoring?
  7. How do events reduce coupling?
  8. What metrics indicate successful refactoring?
  9. When should you avoid introducing new interfaces?
  10. How do you refactor without breaking production?

Key Lessons

A successful refactoring journey usually follows this order:

Identify Problems

↓

Apply SRP

↓

Introduce Interfaces

↓

Apply OCP

↓

Ensure LSP

↓

Split Large Interfaces (ISP)

↓

Use Dependency Injection (DIP)

↓

Introduce Events

↓

Optimize Architecture

Summary

Refactoring is not about rewriting software.

It is the disciplined process of improving internal design while preserving external behavior.

By applying the SOLID principles systematically, a legacy Spring Boot application can evolve from a tightly coupled monolith into a clean, modular, and maintainable architecture.

A production-ready enterprise application should emphasize:

  • Focused responsibilities
  • Stable abstractions
  • Reliable polymorphism
  • Small interfaces
  • Dependency Injection
  • Event-driven communication
  • Layered architecture
  • Continuous refactoring

Mastering SOLID refactoring prepares developers to modernize legacy systems, migrate monoliths to microservices, and build enterprise applications that remain maintainable for years.