SOLID Principles · The Five Principles

SOLID Principles Fundamentals

Recognizing which SOLID principle a design violates, and why that violation will hurt later.

  • 4 min read
  • 9 practice questions
On this page
  1. Description
  2. The five principles
  3. One example, three smells
  4. How to use the principles, step by step
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Most code doesn't fail because it's wrong on day one. It fails because it becomes hard to change: every new feature touches five files, every fix breaks something unrelated, and nobody dares refactor. SOLID is five practical guidelines - collected by Robert C. Martin - for keeping object-oriented code easy to change. This skill is the map of all five; the next five skills take one principle each.

We'll go from simple to more technical:

  1. The five principles in one table, each with the kind of decay it prevents.
  2. One small class that shows three of the smells at once.
  3. How to use the principles as questions, not laws.
  4. When applying them makes code worse.

The five principles

Each principle guards against one specific way code decays:

PrincipleShort versionThe decay it prevents
S - Single ResponsibilityA class should have one reason to change.Unrelated changes colliding in the same class.
O - Open/ClosedExtend behavior by adding code, not editing it.Every new case editing (and re-testing) working code.
L - Liskov SubstitutionSubtypes must be usable wherever the parent is.Subclasses that surprise code written for the parent.
I - Interface SegregationMany small interfaces beat one fat one.Classes forced to implement methods they can't support.
D - Dependency InversionDepend on abstractions; inject the details.Business logic welded to databases, APIs and frameworks.

One example, three smells

Java

class InvoiceService {
    double total(Invoice inv, String customerType) {
        double t = inv.subtotal();
        if (customerType.equals("VIP")) t *= 0.9;             // O: a new customer type edits this method
        else if (customerType.equals("STAFF")) t *= 0.7;
        return t;
    }
    String toPdf(Invoice inv) { /* ... */ }                    // S: formatting lives here too...
    void save(Invoice inv) {
        new MySqlConnection("prod-db").insert(inv);            // D: ...and a concrete database
    }
}

Python

class InvoiceService:
    def total(self, inv: Invoice, customer_type: str) -> float:
        t = inv.subtotal()
        if customer_type == "VIP":                 # O: a new customer type edits this method
            t *= 0.9
        elif customer_type == "STAFF":
            t *= 0.7
        return t

    def to_pdf(self, inv: Invoice) -> bytes: ...   # S: formatting lives here too...

    def save(self, inv: Invoice) -> None:
        MySqlConnection("prod-db").insert(inv)     # D: ...and a concrete database

The code shows three of the smells (S, O and D). The other two need a hierarchy or an interface to appear: a subclass that throws UnsupportedOperationException (Python: NotImplementedError) for a method it inherited (L), and a client that must depend on a 25-method interface when it calls two of its methods (I). The five skills in this module each take one smell, explain the principle behind it, and practice the fix.

How to use the principles, step by step

  1. Treat them as diagnostic questions, not laws: "How many reasons does this class have to change?", "What do I have to edit to add a new case?"
  2. Apply them where change actually happens. A part of the code that never changes doesn't need extension points.
  3. Refactor towards them when a second or third variation appears - not in anticipation of ten.
  4. Use them together: DIP makes OCP possible, ISP keeps LSP achievable, SRP keeps classes small enough to reason about at all.

Which tool for which job?

You noticeAskPrinciple
Unrelated changes keep landing in the same class"Who asks for changes to this?"Single Responsibility
Every new case edits a working if/switch"What must I edit to add a case?"Open/Closed
A subclass throws for, or quietly changes, inherited behavior"Can it stand in for its parent everywhere?"Liskov Substitution
Implementations stub out methods they can't support"Does every client need every method?"Interface Segregation
Business logic constructs databases, clients or clocks itself"Can I test this without the real thing?"Dependency Inversion

Common mistakes

MistakeWhat happensDo this instead
Applying all five to every class from day onetwenty one-method classes and single-implementation interfacesapply them where change actually happens
Adding extension points "just in case"complexity for variations that never arriverefactor when the second or third variation appears
Treating the principles as lawsarguments about rules instead of about the codeuse them as diagnostic questions
Expecting SOLID to find the right abstractionswell-structured code around the wrong conceptsunderstand the domain first

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Which principle is violated when adding a new payment type means editing a working switch statement?

Show answer

Open/Closed - new behavior should be added by adding code (a new implementation of an interface), not by editing existing, tested code.

2. A NotificationService creates new SmtpClient() inside itself. Which principle does that ignore?

Show answer

Dependency Inversion - the high-level service should depend on an abstraction (MessageSender) that is injected, not construct a concrete detail itself.

3. Should you apply all five principles to every class from day one?

Show answer

No. Use them as diagnostics where the code actually changes; premature abstraction is its own maintenance cost.

Key takeaways

  • SOLID = five guidelines for keeping code easy to change, each preventing one kind of decay.
  • Use them as questions to ask about code, especially where change is frequent.
  • Over-applying them creates needless indirection - balance with simplicity.
  • They're coupling and cohesion rules, and they scale up to modules and services.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

PrincipleOne lineThe question to ask
Single Responsibilityone reason to change"Who asks for changes to this?"
Open/Closedadd code, don't edit it"What must I edit to add a case?"
Liskov Substitutionsubtypes honor the parent's contract"Can it stand in for the parent everywhere?"
Interface Segregationsmall, client-shaped interfaces"Does every client need every method?"
Dependency Inversiondepend on abstractions, inject details"Can I test this without the real thing?"

Snippets worth knowing by heart

Java

interface Discount { double apply(double subtotal); }

final class InvoiceService {
    private final InvoiceRepository repository;      // D: injected abstraction
    InvoiceService(InvoiceRepository repository) { this.repository = repository; }
    double total(Invoice inv, Discount discount) {   // O: new discounts are new classes
        return discount.apply(inv.subtotal());
    }
}

Python

class InvoiceService:
    def __init__(self, repository: InvoiceRepository):   # D: injected abstraction
        self._repository = repository

    def total(self, inv: Invoice, discount: Discount) -> float:   # O: new discounts are new classes
        return discount.apply(inv.subtotal())

The traps

  • Dogma - one-method classes and single-implementation interfaces everywhere.
  • Speculative generality - extension points for variations that never come.
  • SOLID instead of domain thinking - tidy code around the wrong concepts.

Staff-level points, one line each

  • The principles are coupling (O, D, I) and cohesion (S, L) at class level.
  • Connascence names coupling: name < type < meaning < position < algorithm < execution order < timing; weaken it, keep strong forms local.
  • Package principles: acyclic dependencies, depend toward stability, stable packages abstract.
  • Instability I = Ce / (Ca + Ce): a shop's domain was 0.00, web 1.00; dependencies should point toward lower I.
  • A topological sort finds dependency cycles - enforce it in CI (ArchUnit, import-linter).
  • Break a package cycle with an interface in the package that needs it.
  • Change coupling from git log shows hidden coupling; hotspots show where it matters.
  • Origins: Martin 2000; Meyer (OCP, 1988); Liskov (1987); acronym by Feathers. Critiques: vagueness, over-application, CUPID.

Say it in an interview

  • "I use SOLID as questions where the code changes most, not as rules for every class."
  • "These two packages form a cycle; I'd invert one dependency with an interface."
  • "The git history shows these files always change together, so the boundary is in the wrong place."
  • "This interface has one implementation and no test double - I'd inline it."

Practice

This skill comes with 9 multiple-choice questions that tell you which misconception a wrong answer reveals. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice SOLID Principles Fundamentals free

Practice SOLID Principles Fundamentals free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime