SOLID Principles

SOLID Principles

Definition: Five object-oriented design principles, coined by Robert C. Martin, intended to make software designs more understandable, flexible, and maintainable as a codebase grows.

How It Works

  • Single Responsibility (SRP): a class should have one, and only one, reason to change, if a class handles both business logic and how that data gets saved to a database, a change to either forces you to touch the same class.
  • The five letters map to five separate, independently useful principles, a codebase can follow some without following all, though they compound: DIP without ISP still leaves fat interfaces behind the abstraction.
  • Open/Closed (OCP): software entities should be open for extension, but closed for modification, adding new behavior should mean adding new code, not editing and re-testing code that already works and is already in production.
  • Liskov Substitution (LSP): subtypes must be substitutable for their base types without breaking code correctness, if code works correctly with a Bird reference, it must keep working correctly when handed any specific subclass of Bird.
  • Interface Segregation (ISP): a client should not be forced to depend on interfaces it does not use, a fat interface with 15 methods forces every implementer to stub out 12 it doesn’t need, split it into smaller, role-specific interfaces instead.
  • Dependency Inversion (DIP): depend upon abstractions (interfaces), not concrete implementations, high-level business logic should define the interface it needs, and low-level details should implement that interface, not the other way around.
  • “Inversion” refers to who owns the interface: traditionally low-level modules define their own API and high-level code adapts to it, DIP flips that, the high-level module defines the interface it wants, and the low-level module must conform to it.
  • The five principles reinforce each other rather than standing alone, a class that violates SRP is usually also harder to keep Open/Closed, since a class doing too many things has too many unrelated reasons to be modified.
  • SOLID doesn’t require full-blown enterprise architecture to apply, the principles scale down to small codebases too, they’re heuristics for finding a reasonable boundary between responsibilities, not a mandate for maximal abstraction everywhere.
  • The acronym was assembled by Michael Feathers in the early 2000s from five principles Robert C. Martin had written about separately through the 1990s and 2000s, the grouping made them easier to teach and remember together as a coherent set.
  • Each principle can be checked with a concrete question: SRP asks “what’s this class’s one reason to change,” OCP asks “can I add this feature without editing existing code,” LSP asks “does every subclass honor the base type’s contract,” ISP asks “does every client actually use every method it depends on,” DIP asks “does high-level logic know about a concrete low-level class.”

Under the Hood

Dependency Inversion in shape:

High-level module (CheckoutService)
        depends on
Abstraction (PaymentProcessor interface)
        implemented by
Low-level modules (StripeProcessor, PaypalProcessor)

Given: a CheckoutService that directly instantiates new StripeProcessor() inside its pay() method. Step: apply Dependency Inversion: define a PaymentProcessor interface with a charge(amount) method, have CheckoutService accept any PaymentProcessor through its constructor, and StripeProcessor implement that interface. Answer: CheckoutService no longer knows Stripe exists at all, swapping to PayPal, or injecting a fake processor in a unit test, requires zero changes to CheckoutService, only a different object passed into its constructor.

Given: a Rectangle base class with setWidth() and setHeight(), and a Square subclass that overrides both to keep width and height equal, so setWidth(5) on a Square also silently changes its height. Step: check this against Liskov Substitution: code expecting a Rectangle calls setWidth(5) then setHeight(10), expecting area 50. Answer: if handed a Square instead, the actual area is 100, not 50, because setHeight silently overwrote the width too, this breaks LSP, Square is not safely substitutable for Rectangle despite the “is-a” relationship seeming intuitive, the fix is usually to not model Square as a subclass of Rectangle at all.

Given: a single UserManager class that validates user input, saves users to the database, sends welcome emails, and formats users for an API response, roughly 400 lines total. Step: apply Single Responsibility: split it into UserValidator, UserRepository, WelcomeEmailSender, and UserSerializer, each with one clear reason to change. Answer: a change to email copy now only touches WelcomeEmailSender, a change to the database schema only touches UserRepository, and each class can be tested and understood independently, instead of every unrelated change risking a 400-line file with four different responsibilities tangled together.

Given: an IWorker interface with work() and eat() methods, implemented by both HumanWorker and RobotWorker, where RobotWorker.eat() has nothing meaningful to do and just throws NotImplementedException. Step: apply Interface Segregation: split IWorker into IWorkable (with work()) and IFeedable (with eat()), have HumanWorker implement both, RobotWorker implement only IWorkable. Answer: RobotWorker no longer needs a meaningless, exception-throwing eat() stub, and any client code that only cares about work capability can depend on IWorkable alone, without dragging in a feeding concept that’s irrelevant to it.

Why It Matters

  • Prevents fragile codebases where a single small change ripples unpredictably through unrelated parts of the system, a direct consequence of violating SRP and OCP together.
  • Teams that internalize SOLID tend to produce codebases where onboarding a new engineer is faster, responsibilities are legible from class names and boundaries alone, rather than requiring a mental map built from tribal knowledge.
  • Simplifies unit testing dramatically: Dependency Inversion in particular is what makes mocking possible, injecting a fake PaymentProcessor in a test is trivial once CheckoutService depends on an interface instead of a concrete class.
  • Enables easier refactoring and safer extension over a codebase’s lifetime, new features that follow OCP get added without re-testing and re-risking existing, already-verified code paths.
  • Widely used as an interview and code-review lens, “does this violate SRP” or “is this actually substitutable” are concrete, checkable questions that turn vague “this code feels bad” intuitions into specific, actionable feedback.
  • The principles underpin most design patterns from the Gang of Four catalog, Strategy is essentially DIP and OCP applied to swappable algorithms, Decorator is OCP applied to adding behavior without modifying a class.

Common Pitfalls

  • Dogmatic over-engineering: splitting code into dozens of single-method interfaces and tiny classes prematurely, in the name of following SOLID, when the added indirection doesn’t pay for itself in a codebase that doesn’t actually need that flexibility yet.
  • Applying Liskov Substitution only by checking method signatures match, LSP violations are about behavioral contracts (pre/post-conditions, invariants), not just type compatibility, a subclass can have a matching signature and still break callers’ expectations.
  • Treating Dependency Inversion as “always use an interface,” even for a class with exactly one implementation that will realistically never change, adding an interface with a single implementer is pure ceremony with no actual flexibility gained.
  • Confusing Open/Closed with “never touch a class again,” it means closed to modification for its existing behavior, adding genuinely new capability through extension (new subclass, new strategy implementation) is exactly what OCP encourages.
  • Ignoring Interface Segregation and building one giant Repository interface with 20 methods, then wondering why every implementer has 15 methods throwing “not implemented,” a sign the interface should have been split by actual client needs.
  • Believing SRP means “a class should only have one method.” A responsibility is a cohesive reason to change, not a method count, a class can have several small, tightly related methods and still have exactly one responsibility.
  • Applying DIP by adding an interface but still having only one implementation ever, forever, with no test double and no plan for a second implementation, in that case the interface adds indirection without buying any actual flexibility.

Comparison

PrincipleProblem it preventsTypical smell if violated
Single ResponsibilityUnrelated changes forcing edits to the same classGod Object, a class doing too many unrelated things
Open/ClosedModifying working, tested code to add new behaviorA growing switch/if-else chain for each new type
Liskov SubstitutionSubclasses that break callers’ assumptionsOverridden methods that throw, or silently change behavior
Interface SegregationClients forced to depend on methods they never useFat interfaces with many “not implemented” stubs
Dependency InversionHigh-level logic tightly coupled to low-level detailDirect new ConcreteClass() calls scattered through business logic

Origins Timeline

YearMilestone
1988Barbara Liskov introduces the substitution principle that becomes LSP
1996-2000Robert C. Martin publishes SRP, OCP, ISP, and DIP as separate essays
Early 2000sMichael Feathers coins the SOLID acronym, grouping the five together
2017Martin’s “Clean Architecture” extends DIP into a full system-level architecture style

Quick Self-Check Questions

PrincipleAsk yourself
Single ResponsibilityIf I describe this class’s job, does “and” show up in the sentence?
Open/ClosedCan I add a new case without editing this file’s existing logic?
Liskov SubstitutionIf I swap in any subclass here, does the calling code’s assumption still hold?
Interface SegregationDoes every implementer of this interface actually need every method on it?
Dependency InversionDoes this high-level class import a concrete low-level class directly?

Example

Dependency Inversion in practice: injecting a PaymentProcessor interface into a CheckoutService constructor instead of hardcoding new StripeProcessor() directly inside it, the single most commonly cited SOLID example because it directly enables unit testing with mocks.

Django and Ruby on Rails’ ORM layers apply Interface Segregation implicitly, a model class exposes focused, role-specific query methods rather than one massive interface covering every possible database operation, keeping each piece of client code dependent only on what it actually calls.

The Java Collections Framework’s Comparator interface is a widely used Open/Closed example: sorting behavior for any type can be extended by supplying a new Comparator implementation, without ever modifying the sort algorithm itself.

ASP.NET Core and Spring Framework both build dependency injection directly into the framework itself, a concrete, industrial-scale application of Dependency Inversion where the framework’s container, not application code, decides which concrete implementation to hand to which high-level consumer.

Uncle Bob’s (Robert C. Martin) “Clean Architecture” book extends DIP into a full architectural style, arranging an entire application in concentric layers where dependencies always point inward toward business rules, and outer layers (databases, UI, frameworks) depend on inner abstractions, never the reverse.

Dig deeper