Software Engineering

What Are the SOLID Principles of Object-Oriented Design?

Learn how the SOLID principles guide modular, maintainable object-oriented systems, with practical examples, review checklists, and tips for avoiding common anti-patterns.

By Inventive HQ Team

The SOLID principles are five object-oriented design guidelines that keep classes loosely coupled and easy to change: Single Responsibility (one reason to change), Open/Closed (extend without editing), Liskov Substitution (subtypes stay substitutable), Interface Segregation (small focused interfaces), and Dependency Inversion (depend on abstractions, not concrete details). Robert C. Martin articulated them around 2000, building on Barbara Liskov's and Bertrand Meyer's earlier work, and Michael Feathers coined the SOLID mnemonic. Together they push code away from the two failure modes that make OOP brittle: too many responsibilities in one place (low cohesion) and hard-wired dependencies between the parts (high coupling).

That's the definition an AI Overview will hand you. What it can't show you is how the five principles reinforce each other, what each violation actually looks like in a pull request, and where applying them tips over into over-engineering. Below is a diagram that maps all five at a glance, a smell-to-fix table you can use during code review, and worked TypeScript refactors for the two principles teams get wrong most often.

The five SOLID principles Five cards spelling SOLID, each naming a principle and its one-line intent: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. SOLID: five rules for classes that survive change Each principle fights coupling or low cohesion from a different angle S Single Responsibility One reason to change ✗ God objects O Open / Closed Extend, don't edit ✗ if/else bloat L Liskov Substitution Subtypes stay swappable ✗ throws in override I Interface Segregation Small, role- specific contracts ✗ fat interfaces D Dependency Inversion Depend on abstractions ✓ testable seams

Object-oriented programming (OOP) promises reusable components, extensibility, and codebases that grow with evolving business requirements. In practice, OOP projects often become brittle, tightly coupled, and resistant to change. The SOLID principles provide a time-tested guide for designing classes and modules that remain understandable and adaptable as applications scale.

This article revisits each SOLID principle with modern development contexts in mind. You'll see how the ideas extend beyond academic definitions, how to recognize violations in real code reviews, and how to apply the guidance without over-engineering solutions.

Why SOLID Still Matters

  • Software lifecycles are longer than ever; features are expected to evolve continuously.
  • DevOps and continuous delivery demand safe, incremental changes.
  • Teams rotate frequently, so clarity and predictability trump clever shortcuts.
  • Stacks may mix microservices, event-driven flows, and traditional monoliths—SOLID is one of the few frameworks that applies across them all.

SOLID at a Glance

PrincipleIntentAnti-Patterns When Ignored
Single ResponsibilityOne reason to change per class/moduleGod objects, massive controllers
Open/ClosedExtend behavior without modifying existing codeFlag bloat, copy-paste branching
Liskov SubstitutionSubtypes must honor base type contractsSurprising runtime exceptions
Interface SegregationDepend on small, specific interfacesStubs throwing UnsupportedOperationException
Dependency InversionHigh-level policies depend on abstractionsHard-coded infrastructure logic

Code-Review Cheat Sheet: Smell → Principle → Fix

This is the table to keep open during a pull-request review. Each row is a concrete symptom you can grep or eyeball, the principle it violates, and the smallest fix that resolves it.

Symptom you see in the diffPrinciple at stakeSmallest fixWhen to leave it alone
Class name has "And"/"Manager"; one class touches DB and formattingSRPExtract the second concern into its own collaboratorTrivial script with no tests and no reuse
A switch/if-else you edit for every new typeOCPReplace with polymorphism or a strategy registryOnly two cases, and a third is genuinely unlikely (YAGNI)
Overridden method throws NotImplementedExceptionLSP / ISPSplit the interface or drop the inheritance for compositionNever — this one is always a real bug
Fat interface where each caller uses 2 of 12 methodsISPSplit into role interfaces (Readable, Writable)Interface is internal and has a single implementer
Business logic imports a concrete driver (new PgClient())DIPInject an abstraction at the composition rootComposition root itself — that's where concretes belong

Which should I reach for first? If you can only enforce one rule on a growing codebase, enforce SRP — focused classes create the seams that make the other four principles cheap to apply later.

1. Single Responsibility Principle (SRP)

Definition: A class should have one and only one reason to change. That "reason" usually maps to a business capability or cohesive technical concern.

What to look for:

  • Classes orchestrating unrelated concerns, such as both persistence and UI formatting.
  • Methods longer than what fits on a screen or that require excessive scrolling to follow.
  • Interfaces whose names include conjunctions (UserAndReportService).

Refactoring approach:

  1. Identify distinct responsibilities via commit history or feature requests.
  2. Extract behavior into focused collaborators (e.g., InvoiceCalculator, InvoiceSerializer).
  3. Introduce clear seams for testing—smaller classes are easier to mock and verify.

TypeScript Example:

// Anti-pattern: handles parsing, validation, and persistence.
class UserProfileManager {
  save(rawJson: string) {
    const parsed = JSON.parse(rawJson);
    if (!parsed.email?.includes('@')) {
      throw new Error('Invalid email');
    }
    database.insert('users', parsed);
  }
}

// Refined responsibilities.
class UserParser {
  parse(rawJson: string) {
    return JSON.parse(rawJson);
  }
}

class UserValidator {
  validate(user: { email: string }) {
    if (!user.email.includes('@')) {
      throw new Error('Invalid email');
    }
  }
}

class UserRepository {
  constructor(private db = database) {}
  save(user: unknown) {
    this.db.insert('users', user);
  }
}

The refactored composition allows teams to swap the repository for a mocked data store or reuse validation logic in APIs and CLI tooling.

Advertisement

2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification. In practice, you should add new behavior by adding code, not editing stable, tested modules.

Signals you are violating OCP:

  • Feature toggles accumulating in a single class, causing cascades of if/else logic.
  • Regression risk every time an evolved feature touches a shared switch statement.
  • Hotfixes duplicating logic because extending existing modules is risky.

Strategies to honor OCP:

  • Favor polymorphism or strategy patterns over enumerations of behavior.
  • Use composition via dependency injection to provide new implementations.
  • Abstract cross-cutting concerns (logging, caching) behind decorators to layer behavior.

Example: Instead of toggling between file-based and cloud storage via if/else, define a StorageProvider interface and register new providers without editing client code.

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without breaking correctness.

Common violations:

  • Overriding methods to throw NotImplementedException.
  • Subclasses narrowing method preconditions (e.g., requiring non-null where base accepts null).
  • Returning stronger postconditions or violating expected invariants.

Practical guidance:

  • Document behavioral contracts—what does a method promise?
  • When a subtype cannot meet the base contract, reconsider the hierarchy; use composition.
  • For TypeScript/Java/Kotlin, rely on type systems and tests to enforce substitution via interface implementations, not deep inheritance trees.

Test to add: For each subclass, run the same integration tests written for the base type. If tests require branching logic to accommodate the subtype, LSP is probably broken.

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on methods they do not use. Instead of massive "god interfaces," favor smaller, role-specific contracts.

Why it matters:

  • Makes mocking simpler—tests only need to emulate the methods they interact with.
  • Encourages a language that matches the domain: Auditable, Versioned, Searchable.
  • Prevents changes requested by one consumer from breaking others.

Implementation patterns:

  • Split large interfaces into focused fragments with clear intent.
  • In TypeScript, use intersection types or mixins to compose behavior.
  • Adopt command/query separation: write-only interfaces separate from read-only ones.

Caution: Excessive micro-interfaces can confuse collaborators. Use naming conventions and module documentation to help teammates understand how fragments fit together.

5. Dependency Inversion Principle (DIP)

Definition: 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.

Symptoms of DIP violations:

  • Application services importing concrete database drivers or HTTP clients directly.
  • Infrastructure changes forcing widespread edits in business logic classes.
  • Difficulty writing unit tests because collaborators cannot be swapped easily.

Modern application of DIP:

  • Use IoC containers or dependency injection frameworks sparingly—constructor injection in plain classes often suffices.
  • Provide default infrastructure implementations at composition roots (e.g., web controllers).
  • Keep abstractions in core modules; place details (ORM, file system) in outer layers.

Illustration:

interface NotificationSender {
  send(subject: string, message: string): Promise<void>;
}

class EmailSender implements NotificationSender {
  constructor(private client: SmtpClient) {}
  send(subject: string, message: string) {
    return this.client.sendEmail(subject, message);
  }
}

class IncidentAlerter {
  constructor(private sender: NotificationSender) {}
  async alert(message: string) {
    await this.sender.send('Incident', message);
  }
}

IncidentAlerter depends on the abstraction, making it testable with an in-memory sender while letting production code wire an SMTP or Slack implementation.

The diagram below shows the "inversion" that gives the principle its name: instead of the high-level policy pointing down at a concrete detail, both point at an interface the policy owns.

Dependency Inversion: before and after On the left, a high-level IncidentAlerter class depends directly on a concrete EmailSender, coupling policy to detail. On the right, both the alerter and the sender depend on a NotificationSender interface, so implementations can be swapped freely.

Before: policy depends on detail IncidentAlerter high-level policy EmailSender concrete detail (SMTP)

After: both depend on abstraction IncidentAlerter high-level policy

NotificationSender interface (owned by policy) EmailSender · Slack swappable implementations

Applying SOLID Without Over-Engineering

  • Start with clarity, evolve to abstractions. Avoid introducing interfaces until duplication or volatility justifies them.
  • Keep feedback loops short. Write tests before refactors to ensure behavior remains consistent.
  • Measure complexity. Track metrics like cyclomatic complexity, class dependencies, and test coverage to decide where SOLID refactoring delivers ROI.
  • Pair with domain-driven design (DDD). Bounded contexts and ubiquitous language reinforce SRP and DIP decisions.

Common Pitfalls

  • Pattern cargo culting: Introducing factories, service locators, or dependency injection frameworks without actual variability needs.
  • Hyper-fragmentation: Splitting responsibilities so finely that understanding control flow becomes harder than before.
  • Ignoring performance: Additional indirection can add overhead—profile critical paths and collapse indirection where necessary.
  • Lack of documentation: SOLID-compliant code should still explain collaborators and invariants via clear naming and short docstrings.

SOLID Review Checklist

Use this list during design reviews or refactoring sessions:

  • Does each class have a narrow, testable responsibility?
  • When extending behavior, can you add a new class or strategy without editing core logic?
  • Can a subclass replace its base type in tests without special casing?
  • Are interfaces expressing cohesive roles, or are there unused methods?
  • Can high-level policies run with alternative implementations (in-memory, mocked, different vendor SDK)?

Beyond SOLID

SOLID is foundational, not exhaustive. Combine it with:

  • DRY (Don't Repeat Yourself): Avoid identical logic across modules while respecting SRP.
  • YAGNI (You Aren't Gonna Need It): Resist speculative abstractions; refactor when change arrives.
  • Clean Architecture and Hexagonal Architecture: These extend DIP to entire application boundaries, aligning with modern microservice and modular monolith strategies.

Final Thoughts

The SOLID principles endure because they frame timeless architectural trade-offs: coupling versus cohesion, abstraction versus concreteness, stability versus flexibility. When applied pragmatically, they help teams deliver features faster, reduce regression risk, and keep codebases adaptable years after the initial launch. Integrate SOLID into code reviews, documentation, and onboarding, and your engineering organization will find it easier to scale both people and products.

Frequently Asked Questions

What does SOLID stand for?

SOLID is an acronym for five object-oriented design principles: Single Responsibility (a class has one reason to change), Open/Closed (open for extension, closed for modification), Liskov Substitution (subtypes must be substitutable for their base type), Interface Segregation (many small interfaces beat one large one), and Dependency Inversion (depend on abstractions, not concrete details). Robert C. Martin introduced the principles around 2000, and Michael Feathers later coined the SOLID mnemonic to make them memorable.

Who invented the SOLID principles?

Robert C. Martin ("Uncle Bob") articulated the five principles in his 2000 paper "Design Principles and Design Patterns," drawing on earlier work by Barbara Liskov (the Liskov Substitution Principle, 1987) and Bertrand Meyer (the Open/Closed Principle). Michael Feathers arranged the initials into the SOLID acronym a few years later.

Are SOLID principles still relevant in 2026?

Yes. SOLID targets coupling and cohesion, which are language- and paradigm-independent problems. The principles apply to microservices, event-driven systems, and modular monoliths just as they do to classic class hierarchies. What has changed is emphasis: modern teams favor composition and small interfaces over deep inheritance trees, but the underlying goals — one reason to change, extend without editing, depend on abstractions — are as useful as ever.

What is the difference between the Single Responsibility Principle and separation of concerns?

Separation of concerns is a broad architectural idea: keep unrelated parts of a system apart (UI, business logic, data access). The Single Responsibility Principle is the class-level application of that idea, with a sharper test — a class should have exactly one reason to change, meaning one actor or stakeholder whose changing requirements would force you to edit it. SRP gives you a concrete question to ask during code review; separation of concerns is the philosophy behind it.

Can SOLID principles be over-applied?

Absolutely. Introducing interfaces, factories, and dependency-injection frameworks before you have real variability leads to indirection that is harder to follow than the duplication it replaces. The safe rule is to start concrete and refactor toward abstraction when a second implementation or a volatile requirement actually appears — this pairs SOLID with YAGNI ("You Aren't Gonna Need It").

Do SOLID principles apply to functional programming?

The letters were framed for object-oriented design, but the goals translate. Single Responsibility maps to small, focused functions; Open/Closed maps to higher-order functions and pattern matching over new cases; Dependency Inversion maps to passing behavior as arguments rather than importing concrete modules. Interface Segregation and Liskov Substitution have looser analogues in structural typing and total functions.

What is the easiest way to spot a SOLID violation in code review?

Look for four smells: class or interface names containing "And" or "Manager" (SRP), a growing switch/if-else chain that you edit for every new case (Open/Closed), overridden methods that throw NotImplementedException (Liskov and Interface Segregation), and business logic that imports a concrete database driver or HTTP client directly (Dependency Inversion). Any one of these is worth a comment.

Which SOLID principle should I learn first?

Start with the Single Responsibility Principle. It is the foundation the others build on — once classes have one job, applying Open/Closed and Dependency Inversion becomes natural, because a focused class has clear seams to extend and inject. SRP also delivers the fastest payoff in testability and readability with the least risk of over-engineering.

solid principlesobject-oriented designsoftware architectureclean codedesign patterns