Why this matters
- These five names come up in every design discussion and most interviews, and reciting the acronym without an example is transparently shallow.
- Each principle has a concrete code smell attached, which makes them usable while you type rather than only during review.
- Applied mechanically they produce over-engineered code, so knowing when not to apply one is part of knowing them.
The five at a glance
Single Responsibility
The question: if this class had to change, how many different reasons could cause it?
A class should have one reason to change — one group of stakeholders whose decisions affect it.
// Three reasons to change: report format, tax rules, email delivery
public class InvoiceService {
public void process(Order order) {
BigDecimal tax = order.total().multiply(new BigDecimal("0.18"));
String html = "<h1>Invoice</h1>" + order.id();
sendEmail(order.customerEmail(), html);
}
private void sendEmail(String to, String body) { }
}
// One reason each
public class TaxCalculator {
public BigDecimal taxFor(Order order) { }
}
public class InvoiceRenderer {
public String render(Order order, BigDecimal tax) { }
}
public class EmailSender {
public void send(String to, String body) { }
}
public class InvoiceService { // coordinates, decides nothing itself
private final TaxCalculator tax;
private final InvoiceRenderer renderer;
private final EmailSender mailer;
public InvoiceService(TaxCalculator tax, InvoiceRenderer renderer, EmailSender mailer) {
this.tax = tax;
this.renderer = renderer;
this.mailer = mailer;
}
public void process(Order order) {
BigDecimal amount = tax.taxFor(order);
mailer.send(order.customerEmail(), renderer.render(order, amount));
}
}
Open/Closed
The question: to add a new case, do I have to edit existing code or only add new code?
Open for extension, closed for modification. The smell is a growing if/else or switch on a
type.
// Every new shipping method edits this method
public class ShippingCalculator {
public BigDecimal cost(String method, BigDecimal weight) {
if (method.equals("STANDARD")) return weight.multiply(new BigDecimal("2"));
if (method.equals("EXPRESS")) return weight.multiply(new BigDecimal("5"));
throw new IllegalArgumentException(method);
}
}
// Every new method is a new class; nothing existing is touched
public interface ShippingMethod {
BigDecimal cost(BigDecimal weight);
}
public class Standard implements ShippingMethod {
public BigDecimal cost(BigDecimal weight) { return weight.multiply(new BigDecimal("2")); }
}
public class Overnight implements ShippingMethod { // added later, nothing else changed
public BigDecimal cost(BigDecimal weight) { return weight.multiply(new BigDecimal("12")); }
}
Liskov Substitution
The question: can I pass a subclass anywhere the parent is expected without anything breaking?
A subtype must honour the parent's promises. The smell is an override that throws, returns something the contract forbids, or demands more of its caller.
public class Rectangle {
protected int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
public class Square extends Rectangle {
@Override public void setWidth(int width) { this.width = width; this.height = width; }
@Override public void setHeight(int height) { this.width = height; this.height = height; }
}
void resize(Rectangle rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
assert rectangle.area() == 20; // holds for Rectangle, fails for Square (16)
}
A Square is a rectangle in geometry but not a valid substitute for this mutable Rectangle,
because it silently violates the independence of width and height. The fix is to drop the
inheritance — make both immutable implementations of a Shape interface, where no such assumption
exists.
Interface Segregation
The question: does every implementer need every method on this interface?
The smell is an implementation full of methods that throw UnsupportedOperationException.
// A read-only data source is forced to implement writes
public interface DataStore {
String read(String key);
void write(String key, String value);
void delete(String key);
void beginTransaction();
void commit();
}
// Small interfaces, composed where a type genuinely supports more
public interface ReadableStore {
String read(String key);
}
public interface WritableStore {
void write(String key, String value);
void delete(String key);
}
public interface TransactionalStore extends ReadableStore, WritableStore {
void beginTransaction();
void commit();
}
A cache implements ReadableStore honestly. A database implements TransactionalStore. Nobody
throws to satisfy a signature they cannot honour.
Dependency Inversion
The question: does my business logic name a concrete class it could do without?
High-level code should depend on abstractions, not on details — and the abstraction should be owned by the high-level code, which is where the word "inversion" comes from.
// Business logic welded to one database
public class OrderService {
private final MySqlOrderRepository repository = new MySqlOrderRepository();
public void place(Order order) {
repository.insert(order); // cannot be tested without MySQL
}
}
// The interface belongs to the business layer; the database implements it
public interface OrderRepository {
void save(Order order);
}
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) { // injected
this.repository = repository;
}
public void place(Order order) {
repository.save(order);
}
}
Now OrderService can be tested with an in-memory implementation in microseconds, and switching
database is one class. This is the principle every dependency-injection framework exists to support.
Three more worth the same attention
- DRY — Don't Repeat Yourself. One piece of knowledge, one place. Note the qualifier: identical code expressing two different rules that happen to coincide today should stay separate.
- KISS — Keep It Simple. The simplest thing that satisfies the requirement, not the most impressive. Clever code has a maintenance cost paid by someone else.
- YAGNI — You Aren't Gonna Need It. Build for the requirement in front of you. Speculative generality is the most common source of complexity that never pays off.
| Aspect | Smell you notice | Principle it points at |
|---|---|---|
| A class with 800 lines and three unrelated jobs | Multiple reasons to change | Single Responsibility |
| A switch that grows with every feature | Modification instead of extension | Open/Closed |
| A subclass whose override throws | Broken parent contract | Liskov Substitution |
| UnsupportedOperationException in an implementation | Interface too wide | Interface Segregation |
| new ConcreteThing() inside business logic | Depending on a detail | Dependency Inversion |
A class with 800 lines and three unrelated jobs
Smell you noticeMultiple reasons to changePrinciple it points atSingle ResponsibilityA switch that grows with every feature
Smell you noticeModification instead of extensionPrinciple it points atOpen/ClosedA subclass whose override throws
Smell you noticeBroken parent contractPrinciple it points atLiskov SubstitutionUnsupportedOperationException in an implementation
Smell you noticeInterface too widePrinciple it points atInterface Segregationnew ConcreteThing() inside business logic
Smell you noticeDepending on a detailPrinciple it points atDependency Inversion
Working backwards from the smell is faster than working forwards from the acronym.
Common misreadings
- "Single responsibility means one method." It means one reason to change. Cohesive classes are often large.
- "Open/closed means never editing a class." Bug fixes are modifications. The principle is about new behaviour not requiring edits to working code.
- "A Square is a Rectangle, so Liskov is violated by geometry." The violation comes from mutability. Immutable versions substitute fine.
- "SOLID always improves a design." Applied pre-emptively it produces interfaces with one implementation and indirection with no purpose. These are diagnostics for real smells.
- "Dependency inversion requires Spring." A constructor parameter is enough.
Quick recall
Everything you need if you only revisit this box.
- S — one reason to change per class. Split by reason, not by size.
- O — new behaviour should mean new classes, not edits. Triggered by a growing
switchon type. - L — a subtype must keep the parent's promises. Triggered by an override that throws or weakens a guarantee.
- I — no implementer should be forced into methods it cannot honour. Triggered by
UnsupportedOperationException. - D — depend on an abstraction your layer owns, injected rather than constructed. Inversion is the principle; injection is the technique.
- DRY, KISS, YAGNI matter just as much, and YAGNI is the brake on over-applying the other five.
- Work from the smell to the principle, not the acronym to the code.
Test yourself
Answer these before moving on — recall is what makes it stick.