SOLID and its companions DRY, KISS and YAGNI are the principles that keep code maintainable as it grows. They do not tell you what to build — they tell you how to structure it so change stays cheap. This reference gives each principle’s definition, the anti-pattern it guards against, and the smell that reveals a violation.
How it works
The five SOLID principles target object-oriented structure:
- S — Single Responsibility: one reason to change per class.
- O — Open/Closed: extend behavior with new code, don’t edit tested code.
- L — Liskov Substitution: subtypes must honor their base type’s contract.
- I — Interface Segregation: many small interfaces beat one fat one.
- D — Dependency Inversion: depend on abstractions, not concretions.
The related principles restrain complexity: DRY (one source of truth), KISS (simplest design that works), YAGNI (don’t build it until needed), plus Separation of Concerns and the Law of Demeter.
Example
A ReportService that formats HTML, runs SQL, and sends email violates Single
Responsibility — three unrelated reasons to change. Splitting it into a
formatter, a repository, and a mailer, each behind an interface the service
depends on, also satisfies Dependency Inversion and makes the service unit
testable without a real database or SMTP server.
Detailed principle breakdown
Single Responsibility Principle (SRP)
A class should have one reason to change. “Reason to change” roughly means one business actor or team cares about it. The test: if different stakeholders (the finance team, the ops team, the UX team) could all independently demand changes to the same class, it is carrying multiple responsibilities.
Smell: Class names with “And”, “Manager”, “Handler”, or “Service” that do more than one distinct thing. A class over 200–300 lines is a strong hint. A method with three if branches targeting different states is often a smell too.
Fix: Extract each responsibility into its own class. The original class can still compose them if needed.
Open/Closed Principle (OCP)
A module should be open to extension but closed to modification. You should be able to add new behaviour by adding code, not by editing existing tested code.
Smell: A switch statement or long if/else if chain that must be extended every time a new type is added. Each edit risks breaking the existing cases.
Fix: Strategy pattern, polymorphism, or plugin hooks — add new types as new classes implementing a shared interface rather than adding new branches to an existing one.
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without breaking callers. A subclass must honour the base class’s contract: it may not strengthen preconditions, may not weaken postconditions, and must not throw on methods the base type supports.
Classic violation: A Square extending Rectangle that overrides setWidth to also change the height. Code that sets width and then expects height to be unchanged breaks when handed a Square.
Smell: A subclass that throws UnsupportedOperationException, returns null where the base type guarantees a value, or documents that “this method does nothing in this subtype.”
Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use. Large “fat” interfaces that combine unrelated methods force implementers to stub methods with empty bodies or throw new Error('not implemented').
Smell: An interface with eight methods but every implementing class only uses three of them. Stub implementations with empty bodies are almost always an ISP violation.
Fix: Split the fat interface into smaller role interfaces. Classes implement only the roles they actually fulfil.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
Smell: A business-logic class that does new DatabaseConnection() or new SmtpMailer() inside a constructor. Any change to the concrete dependency forces changes to the business class.
Fix: Accept the dependency as a constructor parameter typed to an interface. The low-level concrete class is wired up at the composition root (app startup), not inside the business logic.
DRY, KISS, YAGNI and their relationship to SOLID
DRY (Don’t Repeat Yourself) is about having a single source of truth. Duplicated logic is a violation: two copies diverge silently. The fix is extraction, not copy-paste. Note that DRY applies to logic, not to layout or accidental similarity — forcing two genuinely different things into one abstraction because they look alike today is premature DRY.
KISS (Keep It Simple, Stupid) pushes back against speculative complexity. The simplest design that solves the actual problem is the right one. KISS directly counterweights DIP and ISP, which can produce indirection for its own sake.
YAGNI (You Aren’t Gonna Need It) stops you building for requirements you don’t have. If you are writing a plugin system for a feature that has exactly one use today, YAGNI says don’t — build the simplest thing, refactor when the second use case actually arrives.
The tension is healthy. SOLID without KISS and YAGNI produces over-engineered systems with seventeen interfaces for a 50-line domain. KISS and YAGNI without SOLID produce a big ball of mud. The skill is knowing when change is likely enough to justify the abstraction upfront.
Notes
- A telling smell for SRP is a class name containing “And” or a vague “Manager”.
- An Open/Closed smell is a growing
switchyou must edit for every new type; replacing it with polymorphism or Strategy fixes both. - Interface Segregation is violated when implementers stub methods with empty bodies or “not implemented” throws.
- Apply them to diagnose real pain, not as universal quotas — over-applying DIP or ISP creates indirection that KISS and YAGNI exist to prevent.