skip to content

SOLID Principles

The five principles Robert Martin grouped under the SOLID acronym, each with the smell that signals a violation and the refactoring that fixes it. Expect to be asked not just to recite them but to spot which one a piece of ugly code is breaking.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

What does the Dependency Inversion Principle (DIP) state, and what exactly is being "inverted"?

level: juniorimportance: must knowfreq 78%

answer

  1. two clauses: both depend on abstractions; abstractions don't depend on details
  2. source dependency points against flow of control
  3. policy owns the interface, detail implements it
  4. boundary = arrow direction flips
  5. goal, not mechanism (DI/IoC realize it)

basics

~20 s

DIP says important policy code must not depend on low-level detail code; both depend on an abstraction (an interface). What gets inverted is the source-code dependency: the detail now points at the abstraction instead of the policy pointing at the detail.

solid answer

~50 s

DIP has two clauses. (1) High-level modules (business policy) should not depend on low-level modules (details like a database driver, SMTP client, file system); both should depend on abstractions. (2) Abstractions should not depend on details; details should depend on abstractions. "Inversion" is relative to the naive layered design where policy calls and therefore imports the detail. After DIP, the policy declares an interface describing what it needs ("SendNotification"), and the low-level module implements it. Control still flows from policy into the detail at runtime, but the compile-time/source dependency now points the other way — from the detail up to the abstraction. That lets you replace, test-double, or defer the detail without editing or recompiling the policy, and it puts the stable business rules at the centre of the dependency graph rather than at its mercy.

code

pseudocode · 20 lines
pseudocode
// BEFORE — policy names the detail
package billing
import infra.SmtpMailer            // policy -> detail (bad)
class OrderService {
  fun place(o: Order) { SmtpMailer().send(o.email, "...") }
}

// AFTER — policy owns the abstraction, detail depends on it
package billing
interface Notifier { fun notify(to: Address, msg: Message) }
class OrderService(private val notifier: Notifier) {
  fun place(o: Order) { notifier.notify(o.address, receiptFor(o)) }
}

package infra
import billing.Notifier            // detail -> abstraction (inverted)
class SmtpNotifier : Notifier { ... }

// composition root, at the program entry point
main() { OrderService(SmtpNotifier()).place(order) }

go deeper

for a junior

State both clauses in your own words and give one concrete example (service + interface + SMTP implementation). Say that the interface is what both sides depend on.

for a middle

Add the control-flow vs. source-dependency distinction and mention constructor injection plus a composition root as the wiring mechanism.

for a senior

Discuss ownership of the abstraction, package/deployment placement, the second clause (no leaky abstractions), and the testability/build-decoupling payoff.

for a principal

Frame DIP as shaping the dependency graph so volatility flows away from stable policy; connect to boundaries, plugin architecture, independent deployability, and the cost of speculative abstraction.

## The words first - **Module**: any unit of code you can depend on — a class, package, library, service. Nothing language-specific. - **High-level module**: code that expresses *policy* — the rules that justify the system's existence. "An order over $100 gets free shipping." "A user must be notified when their password changes." - **Low-level module**: code that expresses a *detail* — a specific mechanism used to carry out policy. A PostgreSQL driver, an SMTP client, a REST call to a payment provider, a file writer, a UI framework. - **Abstraction**: a declaration of *what* is offered without *how* — an interface, an abstract type, a protocol, a function signature, a trait. Its defining feature is that it can be implemented in more than one way. - **Dependency (source-code sense)**: module A depends on B if A names B — imports it, references its type, calls it by concrete name. If B changes shape, A must be recompiled/rewritten. This is distinct from *runtime* dependency (A needs B present to work) and from *flow of control* (A calls into B while executing). ## The principle Robert C. Martin states it in two clauses: 1. **High-level modules should not depend on low-level modules. Both should depend on abstractions.** 2. **Abstractions should not depend on details. Details should depend on abstractions.** The second clause is the one people forget. It forbids an interface whose *shape* leaks the mechanism: a `UserRepository` interface with a method `executeSql(query: String)` or `getMongoCursor()` is technically an interface, but it is an abstraction that depends on a detail. Change the storage engine and the interface itself must change, so nothing was actually decoupled. ## What is inverted Take the untreated design: ``` OrderService ──imports──▶ SmtpMailer (policy, high-level) (detail, low-level) ``` Both the *call* and the *source dependency* go left→right. `OrderService` cannot be compiled, read, or tested without `SmtpMailer` and everything it drags in. After DIP: ``` OrderService ──uses──▶ «interface» Notifier ▲ │ implements SmtpMailer ``` The *call* still goes `OrderService → SmtpMailer` at runtime. The *source dependency* now goes `SmtpMailer → Notifier`, i.e. upward, against the flow of control. That mismatch between control-flow direction and source-dependency direction is precisely the "inversion". Anywhere it appears, you have crossed an architectural boundary. ## Why anyone cares - **Substitutability**: swap SMTP for a queue, a stub, or a no-op without touching policy. - **Testability**: policy can be exercised with a fake implementation, no network or database. - **Independent deployability / build times**: the policy package no longer transitively depends on driver libraries. - **Decision deferral**: you can write and validate business rules before choosing a database or a vendor. - **Direction of damage**: volatile details change often; stable policy changes rarely. Without inversion, every detail change ripples upward into the most valuable code. With inversion, the ripple stops at the boundary. ## Costs and honest limits Every inversion buys indirection with a price: one more type to name, an extra hop when reading a stack trace, a wiring step (someone must decide which implementation is used), and the risk of a *speculative* abstraction that is wrong. DIP is not "put an interface in front of everything". You invert across boundaries where *volatility* differs — policy vs. mechanism, own code vs. third party — and you leave stable, non-volatile things (a date type, a math routine, the standard library's list) alone. ## Relationship to the mechanisms DIP is the *goal* (a shape of the dependency graph). **Inversion of Control** is the general style where a framework/composition layer, not your code, decides what gets called and when. **Dependency Injection** is the concrete technique for handing a policy its collaborator from outside (constructor, setter, or method parameter), usually assembled in a *composition root* — a single place at the entry point of the program where concrete types are chosen and wired. DI is the most common way to *realize* DIP, but you can use DI and still violate DIP (inject a concrete class), and you can satisfy DIP without a DI container (hand-wire in `main`).

  • If control still flows from the high-level module into the low-level one, in what sense is anything inverted?
    Only the source-code (compile-time) dependency is inverted. At runtime the policy still calls the detail; but the detail's package is the one that names the abstraction, so the detail can be replaced or recompiled without touching the policy.
  • Does adding an interface automatically satisfy DIP?
    No. If the interface is defined in and owned by the low-level module, or if its method signatures expose the mechanism (SQL strings, HTTP status codes, vendor types), the policy still depends on the detail — just indirectly. The abstraction must be shaped by the caller's needs and owned on the caller's side.

A wall socket. Your lamp does not depend on the power station; the power station and the lamp both conform to the plug standard. The socket shape was defined for the benefit of appliances, and generators had to comply — the dependency points at the standard, not at either concrete side, so you can swap the generator or the lamp independently.

saying these in an interview costs you the question

  • "DIP just means use dependency injection" — DI is one mechanism; DIP is a statement about which way source dependencies point.
  • "DIP means every class should have an interface" — that produces header-interface noise with no substitutability gain.
  • Forgetting the second clause and writing abstractions that leak the mechanism (e.g. a repository interface exposing SQL or a driver cursor).
  • Claiming the runtime call direction is what gets inverted.
  • Placing the interface in the low-level module's package and believing the dependency was inverted.

context

open as a page

What does the Interface Segregation Principle (ISP) state, and what problem is it meant to prevent?

level: juniorimportance: must knowfreq 78%

basics

~20 s

ISP — the "I" in SOLID — says no caller should be forced to depend on operations it never uses. Instead of one large interface with many unrelated methods, define several small ones, each matching what a particular caller actually needs.

open as a page

What is the Liskov Substitution Principle (the "L" in SOLID), and what does it require of a subtype?

level: juniorimportance: must knowfreq 85%

basics

~20 s

If type S is a subtype of type T, code written against T must keep working correctly when handed an S. A subtype must honour the promises its supertype made — not just match method names.

open as a page

What does the Open/Closed Principle (the "O" in SOLID) mean by "open for extension, closed for modification"?

level: juniorimportance: must knowfreq 82%

basics

~10 s

You should be able to add new behavior to a module by adding new code (a new class or plug-in), without editing and re-testing the existing, already-working code.

open as a page

What does the Single Responsibility Principle (the "S" in SOLID) state, and what does "one reason to change" actually mean?

level: juniorimportance: must knowfreq 88%

basics

~20 s

SRP says a class or module should do one job, so only one kind of change forces you to edit it. If both a reporting change and a tax-rule change hit the same class, it has two responsibilities.

open as a page

A single class builds a report's text, writes it to a file, and emails it to subscribers. Which SOLID principle does this violate, how do you recognise the violation, and how would you refactor it?

level: juniorimportance: must knowfreq 82%

basics

~20 s

It violates the Single Responsibility Principle: the class changes for three unrelated reasons (report wording, file storage, email delivery). Split it into a formatter, a storage writer, and a sender, and let a small coordinator call all three.

open as a page

A high-level OrderService calls a low-level EmailSender. After applying the Dependency Inversion Principle, which arrows change and which stay the same? Explain the difference between direction of control flow and direction of source-code dependency.

level: middleimportance: must knowfreq 62%

basics

~20 s

Control flow stays the same: OrderService still calls the email code at runtime. The source dependency flips: OrderService declares a Notifier interface, EmailSender implements it, so the low-level code now references the high-level module's abstraction rather than the reverse.

open as a page

How do the Dependency Inversion Principle, Inversion of Control, and Dependency Injection differ? Can you have one without the others?

level: middleimportance: must knowfreq 71%

basics

~20 s

DIP is a design goal about which way source dependencies point. Inversion of Control is a broader style where an outside caller or framework drives your code. Dependency Injection is a concrete technique: hand an object its collaborators from outside instead of it creating them.

open as a page

What concrete symptoms in a codebase tell you an interface violates the Interface Segregation Principle, and what is the step-by-step refactor to fix it?

level: middleimportance: must knowfreq 62%

basics

~20 s

Look for implementations with empty bodies, return null, or "not supported" exceptions; callers that use one or two methods of a large type; and mocks/fakes full of unused stubs. Fix by extracting one small interface per role and letting classes implement several.

open as a page

State the Design-by-Contract rules a subtype must obey to satisfy the Liskov Substitution Principle — preconditions, postconditions, invariants — and give an example of breaking each one.

level: middleimportance: must knowfreq 65%

basics

~20 s

A subtype may not demand more than the base (preconditions can only be weakened), must promise at least as much as the base (postconditions can only be strengthened), and must keep every rule the base always held true (invariants preserved).

open as a page

Explain the classic Rectangle/Square problem: why does making Square a subclass of a mutable Rectangle violate the Liskov Substitution Principle, and how would you fix the design?

level: middleimportance: must knowfreq 75%

basics

~20 s

A mutable Rectangle lets you set width and height independently. A Square must keep them equal, so its setters change both. Code that sets width 5, height 4 and expects area 20 gets 16 — the subclass broke a rule the base promised.

open as a page

You find a billing module where four different functions each contain a `switch` on a `customerType` code (RETAIL, WHOLESALE, GOVERNMENT). Adding a new customer type means editing all four. How do you restructure this to satisfy the Open/Closed Principle, and what exactly becomes "closed"?

level: middleimportance: must knowfreq 68%

basics

~20 s

Create one interface with the four behaviors, and one implementation per customer type holding that type's four branches. The billing code looks up the right implementation and calls it, so a new type is a new class, not four edits.

open as a page

How do you recognize a "god class" (a class that has accumulated far too many responsibilities) in a codebase, and which signals are reliable versus misleading?

level: middleimportance: must knowfreq 72%

basics

~20 s

Look for a class that many unrelated features depend on, has a vague name like Manager or Helper, holds lots of loosely related fields, needs many dependencies to construct in a test, and shows up in almost every commit.

open as a page

What concrete signals in a codebase tell you the Liskov Substitution Principle is being violated, and what refactorings restore substitutability?

level: middleimportance: must knowfreq 68%

basics

~20 s

Signals: an override that throws 'not supported', callers doing type checks before calling, or a subclass that rejects inputs the parent accepts. Fix by narrowing the interface, using composition instead of inheritance, or splitting the hierarchy so each type only promises what it can deliver.

open as a page

Every time a new payment method is added, developers edit the same switch statement in three different files. Which SOLID principle is being violated, and what refactoring restores compliance?

level: middleimportance: must knowfreq 74%

basics

~20 s

It violates the Open/Closed Principle: adding a variant forces editing existing code. Replace the repeated conditionals with polymorphism - one interface per payment method with fee, validate and charge behaviour - so a new method means adding a class, not editing three.

open as a page

How do you decide when applying the Open/Closed Principle is worth it, versus when adding the abstraction is speculative over-engineering? What signals do you use?

level: seniorimportance: must knowfreq 61%

basics

~20 s

You can't be closed against every change, only the ones you predict. Write simple direct code first; add the abstraction when a second real variant shows up or history says that axis changes. One-implementation interfaces built "just in case" are cost with no payoff.

open as a page

Walk through how you would split a class that answers to multiple stakeholders into single-responsibility pieces without breaking its callers, and explain what that split changes about testability.

level: seniorimportance: must knowfreq 63%

basics

~20 s

Add characterization tests first, extract each responsibility into its own class, keep the original class as a thin wrapper that delegates so callers keep compiling, migrate callers gradually, then shrink or delete the wrapper. Each new piece is now testable alone.

open as a page

When does applying SOLID make a codebase worse? How do you decide whether a given piece of code needs an abstraction or should stay concrete?

level: seniorimportance: must knowfreq 58%

basics

~20 s

When the abstraction guesses wrong or is never needed: interfaces with one implementation forever, factories wrapping factories, layers you must step through to read simple logic. Add the abstraction when a second real variant appears, not before.

open as a page

What concrete effects does applying the Interface Segregation Principle have on unit testing, test doubles, and build/deployment coupling?

level: middleimportance: should knowfreq 45%

basics

~20 s

Narrow interfaces make test doubles tiny — you fake one or two methods instead of twenty — so tests are shorter and state the collaboration clearly. They also shrink the blast radius of a change: fewer files reference the type, so fewer things recompile, re-mock, or redeploy.

open as a page

Besides swapping strategy implementations, what other mechanisms let you extend a system's behavior without modifying existing code — and how do the Decorator pattern and plug-in extension points each achieve the Open/Closed Principle?

level: middleimportance: should knowfreq 52%

basics

~20 s

A Decorator wraps an existing object behind the same interface and adds behavior around it (caching, retry, logging) without touching the wrapped class. A plug-in extension point lets the core discover and call implementations it has never heard of, registered at startup or install time.

open as a page

Under the Dependency Inversion Principle, which side should define and own the abstraction — the module that uses it or the module that implements it — and why does the physical placement of that interface (package, module, deployable) matter?

level: seniorimportance: should knowfreq 48%

basics

~20 s

The consumer owns it. The high-level module declares the interface it needs, in its own package, in terms of its own vocabulary; the implementing module depends on that package. If the interface lives with the implementation, the policy still imports infrastructure and nothing is inverted.

open as a page

A codebase defines an interface for every concrete class — each with exactly one implementation and an identical method list (IUserService/UserService, IEmailSender/EmailSender). Does this satisfy the Dependency Inversion Principle? How do you distinguish a real inversion from interface noise?

level: seniorimportance: should knowfreq 45%

basics

~20 s

No. Mirroring every class with a same-shaped interface adds indirection without decoupling: the interface changes whenever the class does, so the client is still tied to the implementation. Real inversion means the abstraction is client-shaped and crosses a meaningful boundary.

open as a page

Under the Interface Segregation Principle, who should define an interface — the component that implements it or the component that calls it? Explain the difference between a role interface and a header interface.

level: seniorimportance: should knowfreq 34%

basics

~20 s

The caller should define it. An interface written from the caller's needs (a "role interface") stays small and stable. An interface that just mirrors an existing class's full public surface (a "header interface") adds a file but removes no coupling.

open as a page

How are the Interface Segregation Principle and the Liskov Substitution Principle related, and how can violating interface segregation push a design into a substitutability violation?

level: seniorimportance: should knowfreq 38%

basics

~20 s

ISP is about not exposing callers to operations they don't use; LSP is about subtypes being usable anywhere the supertype is. A too-wide interface forces implementers to stub or throw on operations they can't support — and a caller that invokes one then breaks, which is an LSP failure.

open as a page

An interface has an `add(item)` method, and one implementation overrides it to throw an "operation not supported" error (as immutable collections often do). Is that a Liskov Substitution violation, and what are the alternatives?

level: seniorimportance: should knowfreq 55%

basics

~10 s

Yes — clients told the interface supports add now crash at runtime. The implementation delivers less than the interface promised. The fix is to split the interface so read-only types never advertise mutation.

open as a page

How do covariance and contravariance in method signatures relate to the Liskov Substitution Principle, and what do mainstream languages actually enforce?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Overrides may return a more specific type (covariant returns) because promising more is safe, and could in theory accept a more general parameter type (contravariant parameters) because demanding less is safe. Most languages enforce only the return-type rule.

open as a page

An architect claims a design is "fully closed for modification." Why is that claim impossible in general, and what trade-off does interface-based Open/Closed force you to accept about adding new types versus adding new operations?

level: seniorimportance: should knowfreq 38%

basics

~20 s

You can only be closed against changes you predicted. Using interfaces makes adding a new implementation cheap but adding a new method to the interface expensive — every implementation must change. A big switch statement has the opposite trade-off.

open as a page

How do the five SOLID principles reinforce one another? Give a concrete chain showing how violating one causes violations of the others.

level: seniorimportance: should knowfreq 52%

basics

~20 s

They are not independent. A fat interface (ISP violation) forces implementations to stub methods they cannot support (LSP violation), so callers add type checks, which means adding a type forces editing callers (OCP violation) - and callers now depend on concrete types (DIP violation).

open as a page

When is applying the Single Responsibility Principle harmful, and how do you decide that a module with several capabilities should be left as it is?

level: principalimportance: should knowfreq 41%

basics

~20 s

SRP is harmful when you split by guesswork. Extra classes add indirection, wiring and reading cost. If one team owns all the behavior and changes always arrive together, leaving it as one module is the cheaper, clearer design.

open as a page

showing 1–30 of 35