SOLID Principles Refresher


SOLID is a set of five simple rules that help you write cleaner, safer, and easier-to-maintain object-oriented code. Each principle helps you avoid common headaches as your codebase grows.
These principles are good engineering habits, born from real problems developers kept running into. I already knew the pain from experience, and when I came back to refresh these ideas, they clicked stronger than before. Therefore, I want to offer a summary with concise examples and straight-to-the-point explanations.
In this article, you will find a definition of each principle with a set of quick labs to showcase how to apply them. Finally, there’s a cheatsheet you can reference to reflect if you need to apply any of the principles in your day-to-day work.
I hope this will help you develop the intuition you need to apply them if you are a beginner — or serve as a refresher for more experienced developers.
Remember, understanding doesn't happen by memorizing principles — it happens when you connect them to real problems you're ready to solve. Let’s get started:
S – Single Responsibility Principle (SRP): "A class should have only one reason to change." Each class should do only one thing. Don't mix responsibilities. For example, A class that saves data shouldn't also send emails.
O – Open/Closed Principle (OCP)
"Software entities should be open for extension but closed for modification." You should be able to add new behavior without changing existing code. For example, instead of editing existing classes, you extend them or create new ones that plug into your system.L – Liskov Substitution Principle (LSP)
"Objects of a superclass should be replaceable with objects of its subclasses without breaking the program." If you have a classAnimal
, anyDog
orCat
should behave like anAnimal
without problems. For example, subclasses should not throw errors when using methods they inherit from the parent class.I – Interface Segregation Principle (ISP)
"Clients should not be forced to depend on interfaces they do not use." Prefer many small, specific interfaces over one big, general interface. For example, instead of oneIMultifunctionDevice
withprint()
,scan()
, andfax()
, create a separateIPrinter
,IScanner
, andIFax
interfaces.D – Dependency Inversion Principle (DIP): "Depend on abstractions, not on concrete implementations."
Code should depend on interfaces or abstract classes, not on specific classes. For example, Instead of aOrderService
depending directly on an SQLOrderRepository, a concrete implementation, it should depend on an OrderRepository interface.
SOLID principles often overlap, you may need to apply two or more together to achieve better maintainability and scalability.
Now that we cover the definition, let’s review a set of quick labs to refresh the principles and get you ready to recognize and define them during a code review or an interview.
QuickLab 1: Refactoring Toward SRP + OCP
❌ Before: Hard-to-Maintain Code
public class DiscountCalculator {
public double calculateDiscount(String customerType, double amount) {
if (customerType.equalsIgnoreCase("REGULAR")) {
return amount * 0.1;
} else if (customerType.equalsIgnoreCase("VIP")) {
return amount * 0.2;
} else {
return amount * 0.0;
}
}
}
🔴 What’s wrong with the above code?
At first, it looks simple enough:
just an if-else
to calculate a discount based on customer type.
But the real problem is that every time you need to support a new discount, You have to go back and modify this method.
Even if you intend to only add something, you might accidentally break existing logic.
What starts as a few
else ifs
can quickly turn into messy, fragile code — especially when real business rules get more complex.In practice, discount calculations often involve multiple conditions, exceptions, or special cases, and maintaining a big chain of
if-else
becomes a nightmare.
The risk isn't visible when the code is small. It becomes painful when it grows. If you play the devil’s advocate, like I often do, you may think but what’s the problem with breaking things, shouldn’t your unit test just spot it and you will fix it the right way? Unit tests help catch mistakes, but good design prevents many mistakes from happening at all. When code grows, every edit you avoid is one less risk — and one less reason to break the system. Mic drop!
Now let’s refactor this code to make more compatible wit the SRP and the OCP:
✅ After: A Safer, Cleaner Approach
Step 1: Define Discount Strategies
interface DiscountStrategy {
double calculate(double amount);
}
class RegularDiscount implements DiscountStrategy {
public double calculate(double amount) { return amount * 0.1; }
}
class VIPDiscount implements DiscountStrategy {
public double calculate(double amount) { return amount * 0.2; }
}
Step 2: Select Strategy Dynamically
import java.util.Map;
class DiscountFactory {
private static final Map<String, DiscountStrategy> strategies = Map.of(
"REGULAR", new RegularDiscount(),
"VIP", new VIPDiscount()
);
static DiscountStrategy get(String type) {
return strategies.getOrDefault(type.toUpperCase(), a -> 0.0);
}
}
Step 3: Use It
public class Main {
public static void main(String[] args) {
DiscountStrategy strategy = DiscountFactory.get("VIP");
System.out.println(strategy.calculate(100.0)); // prints 20.0
}
}
Why is this better?
Adding new discount types means creating a new small class, not touching the old ones.
No risk of breaking existing discount logic when expanding.
Keeps changes isolated and safe, even as the system grows.
Short code today, complicated business rules tomorrow — this structure protects you early.
QuickLab 2: Refactoring Toward LSP + ISP
❌ Before: Subtypes That Don’t Behave Properly
class Bird {
void fly() {
System.out.println("Flying high!");
}
}
class Penguin extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
What's wrong here?
The code assumes all Birds
can fly — but Penguin
can't. Instead of flying, it crashes at runtime.
Now you can't safely treat all Birds the same way, and every place that uses a Bird has to worry about special cases. What looked simple becomes fragile and full of workarounds.
✅ After: A Safer, Cleaner Design
Step 1: Redefine the Abstraction
interface Bird {
void eat();
}
interface FlyingBird extends Bird {
void fly();
}
Step 2: Implement Only What Makes Sense
class Sparrow implements FlyingBird {
public void eat() {
System.out.println("Sparrow eating.");
}
public void fly() {
System.out.println("Sparrow flying.");
}
}
class Penguin implements Bird {
public void eat() {
System.out.println("Penguin eating.");
}
}
Step 3: Use Safely
public class Main {
public static void main(String[] args) {
Bird penguin = new Penguin();
penguin.eat(); // OK
FlyingBird sparrow = new Sparrow();
sparrow.eat();
sparrow.fly(); // OK
// No surprises, no runtime crashes!
}
}
QuickLab 3: Refactoring Toward DIP
❌ Before: High Coupling, Hard to Change
class MySQLRepository {
void save(String data) {
System.out.println("Saving '" + data + "' to MySQL");
}
}
class ReportService {
private MySQLRepository repository = new MySQLRepository();
void generateReport(String data) {
repository.save(data);
}
}
What's wrong here?
ReportService
is tightly locked to MySQLRepository
.
If tomorrow you want to save reports somewhere else (e.g., InMemoryRepository, PostgreSQLRepository, FileRepository),
you have to edit ReportService — and risk breaking it.
You're gluing your business logic to one specific storage choice, and it kills flexibility.
✅ After: Depending on Abstractions
Step 1: Introduce a Repository Interface
interface Repository {
void save(String data);
}
Step 2: Implement Different Repositories
class MySQLRepository implements Repository {
public void save(String data) {
System.out.println("Saving '" + data + "' to MySQL");
}
}
class InMemoryRepository implements Repository {
public void save(String data) {
System.out.println("Saving '" + data + "' to In-Memory storage");
}
}
Step 3: Make Service Depend on Abstraction
class ReportService {
private final Repository repository;
ReportService(Repository repository) {
this.repository = repository;
}
void generateReport(String data) {
repository.save(data);
}
}
Step 4: Use It Flexibly
public class Main {
public static void main(String[] args) {
Repository repository = new MySQLRepository();
ReportService service = new ReportService(repository);
service.generateReport("Monthly Report");
}
}
🧠 Notes Worth Mentioning:
ReportService
no longer knows or cares about how or where the data is saved.You can easily swap
MySQLRepository
forInMemoryRepository
,PostgresRepository
, etc. without changing the service.Flexibility is built in right from the start.
🎯 Quick Guide: How to Spot Problems and Fix Them (Prescriptive SOLID Cheatsheet)
Over time, as you refactor/write more and more code, you start to build intuition.
You get faster at spotting when something feels wrong — big classes, messy inheritance, hard-to-extend code — and you instinctively reach for the right design improvement.
But when you're starting (or even when you're experienced but moving fast), it helps to have a quick checklist.
Here's a small cheatsheet you can refer to anytime you need a quick reminder:
Situation | Problem | SOLID Principle | Quick Action |
Big class or method | Too many responsibilities | SRP (Single Responsibility Principle) | Break into smaller classes or methods, each doing one thing. |
Need to change old code to add new | Code not extendable | OCP (Open/Closed Principle) | Add behavior via new classes instead of editing existing ones. |
Subclasses have methods that don't make sense | Bad inheritance structure | LSP (Liskov Substitution Principle) | Redesign abstractions so subclasses fit behavior naturally. |
Interface has too many unrelated methods | Interface too fat | ISP (Interface Segregation Principle) | Split interfaces by role — avoid forcing classes to implement unused methods. |
Hardcoded dependencies | Tight coupling | DIP (Dependency Inversion Principle) | Program to abstractions (interfaces) and inject dependencies from outside. |
Conclusion
SOLID is a toolbox, not a checklist. Good design means knowing when to use these principles — and when to keep things simple. Flexibility is valuable when change is expected, but unnecessary abstractions only add complexity. The real skill is balancing simplicity with foresight.
Keep in mind that clean code matters, but so does collaboration. Every solution has trade-offs, and not every situation will let you apply the "perfect" design. Favor simplicity, respect team decisions, and focus on continuous improvement.
I hope this article refreshed the SOLID principles for you and encouraged a mindset of thoughtful, intentional design.
This article is 🚀 part of the "Clean Code" series! If you enjoyed it, stay tuned for more!
Subscribe to my newsletter
Read articles from Mirna De Jesus Cambero directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Mirna De Jesus Cambero
Mirna De Jesus Cambero
I’m a backend software engineer with over a decade of experience primarily in Java. I started this blog to share what I’ve learned in a simplified, approachable way — and to add value for fellow developers. Though I’m an introvert, I’ve chosen to put myself out there to encourage more women to explore and thrive in tech. I believe that by sharing what we know, we learn twice as much — that’s precisely why I’m here.