skip to content

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%

answer

  1. header interface = mirror of the class, changes with it
  2. role interface = shaped by the caller's need
  3. one impl + identical signatures ⇒ noise
  4. invert across volatility / process boundaries
  5. extracting later is cheap; deleting a bad abstraction isn't

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.

solid answer

~50 s

These are *header interfaces* — mechanical mirrors of a concrete class, defined by the supplier and evolving in lockstep with it. They give the *appearance* of DIP with none of the benefit: any change to the class changes the interface, so the client still recompiles and still can't substitute anything meaningful. Signs of a genuine inversion: (a) the abstraction is declared in and owned by the consumer's module, (b) it is expressed in the consumer's vocabulary and mentions no vendor/persistence/transport types, (c) it is narrow — only what this consumer uses (Interface Segregation), (d) it crosses a *volatility* or ownership boundary — policy vs. mechanism, our code vs. a third party, and (e) it plausibly has, or will have, more than one implementation: a real alternative adapter, a test fake, an in-memory variant. If none of those hold, delete the interface and depend on the class directly; modern languages and tooling let you extract an abstraction later cheaply.

code

pseudocode · 8 lines
pseudocode
// NOISE — supplier-shaped mirror, changes whenever the class changes
interface IEmailSender { fun sendHtml(to:String, subj:String, html:String, cc:List<String>, attachments:List<File>) }
class EmailSender : IEmailSender { ... }   // the one and only implementation

// REAL INVERSION — caller-shaped role, owned by policy, narrow, domain terms
package billing
interface ReceiptDelivery { fun deliver(receipt: Receipt, to: Customer) }
// implementations: SmtpReceiptDelivery, QueueReceiptDelivery, InMemoryReceiptDelivery(tests)

go deeper

for a junior

Say that an interface mirroring one class doesn't decouple anything, because both change together; interfaces are worth it when there's a real alternative implementation.

for a middle

Introduce header vs role interfaces and the criteria: owned by the client, narrow, crossing an I/O or vendor boundary.

for a senior

Add the volatility criterion, ISP linkage, the test-fake nuance, and a concrete refactoring plan for an over-abstracted codebase.

for a principal

Frame it as managing the cost of abstraction: reversibility asymmetry, team/build boundaries, review heuristics, and making the rule mechanical instead of cultural.

## Header interface vs role interface - **Header interface**: created by copying a concrete class's public method list into an interface, usually named `IFoo` for `Foo`. The supplier defines it. It is a *restatement* of the implementation. - **Role interface**: created by asking "what does *this caller* need?" It is smaller than any implementation's surface, named after the role played (`Notifier`, `Clock`, `RateLimiter`, `OrderRepository`), and owned by the caller. DIP is satisfied by role interfaces across boundaries. Header interfaces usually satisfy nothing except a linting habit. ## Why header interfaces fail the principle 1. **Coupling survives.** If `UserService` grows a method, `IUserService` grows the same method. The interface's shape is a function of the implementation — this is exactly clause two of DIP ("abstractions should not depend on details") being violated. 2. **No substitutability.** With one permanent implementation, nothing can be swapped; you gained an extra file and a jump in every navigation. 3. **Reading cost.** "Go to definition" lands on a declaration, not behaviour. In large codebases this is a real, cumulative tax. 4. **False signal.** Reviewers see interfaces and assume boundaries exist, so genuine coupling goes unnoticed. 5. **Historical justification is largely gone.** Blanket interface-per-class arose partly because older mocking libraries could only mock interfaces, and some frameworks needed them for proxying. Modern mocking/test tooling can fake concrete types in most ecosystems, so the tail wags the dog less than it used to. ## The "but I need it for tests" argument A test fake *is* a second implementation, so "only one implementation" is not automatically fatal. But interrogate it: - Is the collaborator actually *worth* faking — does it do I/O, is it slow, non-deterministic, or external? Then the port is justified, and it should be a **role** interface shaped by the use case, not a mirror of the class. - Is it pure in-process logic? Then use the real thing in tests; a fake only re-asserts what you already wrote and couples tests to internal structure. ## A practical decision checklist Invert (add an owned abstraction) when **any** of these hold: - The dependency reaches outside the process: database, network, file system, clock, randomness, message broker, third-party SDK. - The dependency is **volatile** — it changes for reasons unrelated to your policy (vendor, framework, regulation-driven mechanism). - You genuinely need or foresee multiple implementations (multi-tenant strategies, region-specific gateways, a feature-flagged rollout). - You must break a compile/deploy dependency between teams or modules. - The dependency is not yet decided and you want to keep writing policy. Do **not** invert when: - It is a stable value type, data structure, or pure function. - It is a stable standard-library or long-lived, rarely-breaking utility. - Both sides live in the same module, change together, and are owned by the same team. - The only motivation is "we always add an interface". ## Reversibility argument Extracting an interface later is a mechanical, tool-assisted refactor with low risk. Deleting a wrong abstraction that clients have grown to depend on is much harder. That asymmetry argues for *withholding* abstraction until a second implementation or a real boundary appears — the same reasoning behind "the rule of three" and YAGNI. The counterweight: at true architectural boundaries (I/O, external vendors) the second implementation is effectively certain, so inverting immediately is not speculative. ## Reviewing an existing codebase Questions to ask per interface: 1. Which module declares it? (Client → good; supplier → suspect.) 2. How many *meaningfully different* implementations exist or are planned? 3. Do its signatures name vendor, ORM, or transport types? 4. Does any single consumer use all of its methods? 5. If the concrete class changed a method, would the interface have to change too? (Yes → it's a header interface.) A failing score on 1, 3 and 5 means the interface is documentation-shaped indirection. Collapse it, or reshape it into a real port.

  • Is an interface with a single production implementation always wrong?
    No. At an I/O or third-party boundary a single production implementation plus a test fake is a legitimate second implementation, and a second production adapter is often a matter of time. The test is whether the abstraction is client-shaped and crosses a volatility boundary — not the raw implementation count.
  • How would you refactor a codebase full of header interfaces?
    Inline the ones with a single implementation that live beside their class and expose no boundary — most IDEs can collapse them safely. Keep and reshape the ones at I/O/vendor boundaries: move the declaration into the consuming module, trim methods down to what that consumer actually uses, and replace leaked infrastructure types in the signatures with domain types.
  • How does the Interface Segregation Principle relate here?
    ISP says no client should be forced to depend on methods it doesn't use. Header interfaces are automatically ISP violations because they carry the whole implementation surface. Splitting them per consumer role produces exactly the narrow ports DIP wants.

Putting a glass door in front of a wall. It looks like an entrance and costs money, but nothing on the other side can be changed independently — you still have exactly one room, now with an extra pane to walk around.

saying these in an interview costs you the question

  • "Every class must have an interface" as a blanket rule.
  • Naming interfaces after implementations (IFoo/Foo) and mirroring all methods.
  • Claiming DIP is satisfied because an interface exists, without checking who owns it.
  • Adding abstractions speculatively "in case we swap the database" for something that will never be swapped.
  • Using "my mocking framework needs it" as a design argument in ecosystems where concrete types can be faked.
  • Confusing implementation count with decoupling — the shape and ownership of the abstraction is what matters.

context