Appearance
Writing Quality Code (SOLID Principles)
5 articles · ~2.5 hours · All developers
The SOLID principles are five design guidelines for object-oriented code that, taken together, produce systems that are easier to change, test, and reason about. This path walks through each principle in the sequence that makes each one easiest to understand — because the principles are not independent; they reinforce each other.
Prerequisites
Before starting this path, you should be comfortable with:
- Basic object-oriented programming: classes, inheritance, interfaces, and polymorphism
- Writing and calling methods across multiple classes
- A general sense of what "tight coupling" means in code (even if you haven't solved it yet)
No specific language is required. Examples in the articles use Java, but the principles apply equally to C#, Kotlin, TypeScript, Python, and any other OO language.
The Path
Step 1: Single Responsibility Principle (SRP)
Article: SRP — Single Responsibility Principle
Definition: A class should have only one reason to change.
Why start here SRP is the entry point because it addresses the most universal code quality problem: classes that do too much. Before you can apply any of the other principles effectively, you need to practice the discipline of identifying distinct responsibilities and separating them. SRP trains that instinct.
Key anti-pattern to avoid The "God Class" — a single class that handles data access, business logic, validation, formatting, and notification all at once. Any change to any one concern risks breaking the others.
What to learn next Once your classes have a single responsibility, the next question is: how do you extend them without touching code that already works? That's OCP.
Step 2: Open/Closed Principle (OCP)
Article: OCP — Open/Closed Principle
Definition: Software entities should be open for extension but closed for modification.
Why this builds on SRP SRP gives you small, focused classes. OCP tells you how to grow behavior without editing those classes — instead, you extend them. This is only practical once your classes are small enough that adding a new extension point doesn't require understanding hundreds of lines of tangled logic.
Key anti-pattern to avoid The if/else or switch chain that grows every time a new variant is needed. Each addition to the chain is a modification to existing, tested code — exactly what OCP exists to prevent. The fix is usually a strategy or template method pattern.
What to learn next OCP depends heavily on polymorphism and inheritance. LSP defines the contract that makes that polymorphism safe to use.
Step 3: Liskov Substitution Principle (LSP)
Article: LSP — Liskov Substitution Principle
Definition: Objects of a subclass must be substitutable for objects of the superclass without altering program correctness.
Why this builds on OCP OCP asks you to extend behavior via subclasses and abstractions. LSP sets the rules for how those subclasses must behave: they must honor the contract of the type they extend, not just its method signatures. Without LSP, the polymorphism that OCP relies on becomes a source of subtle runtime bugs.
Key anti-pattern to avoid Throwing UnsupportedOperationException (or an equivalent) from an overriding method. This tells callers that the subtype cannot actually be substituted for the base type — a direct violation of LSP.
What to learn next LSP governs how you implement a type. ISP governs how you design the interfaces that types implement.
Step 4: Interface Segregation Principle (ISP)
Article: ISP — Interface Segregation Principle
Definition: Clients should not be forced to depend on interfaces they do not use.
Why this builds on LSP LSP ensures subclasses honor their contracts. ISP ensures those contracts aren't broader than they need to be. A "fat" interface forces every implementor to provide methods it may not need — leading to hollow stub implementations that signal an LSP violation. ISP prevents that problem at the interface design stage.
Key anti-pattern to avoid The single large interface with fifteen methods that every implementing class only partially fulfills. Split interfaces along the lines of actual client usage: each client depends only on the methods it calls.
What to learn next ISP shapes your interfaces. DIP determines how classes obtain their dependencies — the final piece of the SOLID puzzle.
Step 5: Dependency Inversion Principle (DIP)
Article: DIP — Dependency Inversion Principle
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Why this builds on ISP ISP produces lean, focused interfaces. DIP tells you to depend on those interfaces — not on concrete implementations. This is the principle that makes dependency injection, mocking in tests, and runtime behavior swapping possible. It ties the entire SOLID model together: you have single-responsibility classes (SRP), extensible via abstractions (OCP), with safe substitution (LSP), and clean interface boundaries (ISP), wired together through inversion of control (DIP).
Key anti-pattern to avoid Instantiating dependencies with new inside a class — especially a service instantiating a repository or a logger. This hard-codes the dependency, making the class untestable in isolation and impossible to reconfigure without modifying its source.
The SOLID Connection
The five principles are not a checklist — they form a self-reinforcing system:
- SRP ensures classes are small enough to be extended cleanly.
- OCP uses polymorphism to add behavior without modifying existing classes.
- LSP ensures that polymorphism is safe and predictable.
- ISP keeps the interfaces that enable polymorphism lean and purposeful.
- DIP wires the whole system together through abstractions, keeping high-level logic independent of low-level details.
Violating one principle typically creates pressure that violates another. A God Class (SRP violation) almost always ends up with a sprawling interface (ISP violation) and hard-coded dependencies (DIP violation). The principles are most powerful when applied together.
After This Path
Having completed this sequence, you will be able to:
- Identify and decompose God Classes into focused, single-responsibility units
- Design extension points that let behavior grow without touching tested code
- Write subclasses that are genuinely substitutable for their base types
- Define lean interfaces aligned to actual client needs
- Decouple high-level logic from low-level implementation details using dependency injection
A natural next step is to apply these principles in the context of a larger system. The Building Scalable APIs path gives you the opportunity to practice them while building real API infrastructure.