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:
- Add unit tests.
- Identify code smells.
- Extract responsibilities.
- Introduce interfaces.
- Apply dependency injection.
- Replace conditional logic with polymorphism.
- Extract events.
- Verify behavior.
- Optimize performance.
- 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
- How would you refactor a God Class?
- Which SOLID principle would you apply first?
- Why is incremental refactoring safer?
- How does Spring Boot simplify refactoring?
- Which design patterns help during refactoring?
- Why should tests be written before refactoring?
- How do events reduce coupling?
- What metrics indicate successful refactoring?
- When should you avoid introducing new interfaces?
- 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.