What is the dependency inversion principle, and what exactly is being inverted?
High-level policy should not depend on low-level detail; both should depend on an abstraction. What inverts is ownership of the interface - it belongs to the module that consumes it, not to the one that implements it, which is what reverses the direction of the source dependency.
What the interviewer is scoring
- Does the candidate identify that the interface belongs to the consumer rather than the implementer
- Whether they separate dependency inversion from dependency injection instead of treating them as one idea
- That an interface with exactly one implementation is recognised as sometimes pointless
- Whether they can describe the compile-time or package-level consequence, not just the runtime one
- Does the candidate name a case where inverting is not worth it
Answer
The direction of the arrow
Take an order service that needs to save orders to Postgres. The obvious structure has OrderService importing PostgresOrderRepository, so the dependency arrow points from policy to detail: the business logic knows about the database.
That arrangement has a specific problem, and it is not primarily about testing. It is that the thing you care most about protecting — the rules of the business, which are the reason the software exists — is now downstream of the thing most likely to change for reasons that have nothing to do with the business. Swap the database and you recompile the order logic.
Inverting means OrderService declares what it needs, as an interface, and PostgresOrderRepository implements it. The arrow from the concrete class now points up at the abstraction, and the high-level module points at nothing below it.
What inverts is ownership
The part that separates a memorised answer from an understood one: the interface belongs to the consumer.
OrderRepository is not the database layer's public API. It is the order service's statement of its own requirements, expressed in the order service's language, and it lives in the order service's package. The persistence module imports it in order to implement it. That is why the source dependency reverses — the arrow at build time now runs from the detail toward the policy, the opposite of the runtime call direction.
Put the interface in the persistence package and you have achieved nothing structural. The high-level module still imports the low-level one; you have simply put an interface in the way, and every consumer still recompiles when the persistence package changes. This is the most common way the principle is applied wrongly, and it is invisible unless you look at which package the file sits in.
The consequence follows in the method signatures. If OrderRepository exposes findByCriteria(SqlPredicate) or returns a database cursor, the abstraction has leaked and the inversion is cosmetic — the caller is still coupled to the storage model, just through one more layer. A repository owned by the domain speaks in domain terms: findUnfulfilledOrdersFor(CustomerId).
Inversion is not injection
These get conflated constantly and they are different things at different levels.
Dependency inversion is about which direction the source dependency points, and it is a structural property you could check by looking at the import graph. Dependency injection is a technique for supplying a collaborator from outside rather than constructing it internally. You can inject a concrete class, which is injection without inversion. You can invert and then instantiate the implementation in a factory with no container anywhere, which is inversion without a framework.
A candidate who answers "dependency inversion means using a DI container" has named a tool and missed the principle.
When not to bother
The honest limit. An interface with one implementation, no prospect of another, and no test that needs to substitute it is a file you maintain for nothing. Every call now needs one extra jump to read, and the navigability cost is real in a large codebase.
The useful test is whether the boundary is one across which change genuinely arrives. Persistence, third-party APIs, clocks, message brokers, anything with a network or a vendor behind it — these change on someone else's schedule and are worth insulating. An internal PriceCalculator used in one place, changing only when the business rules change, is not: the interface would just be a copy of the class, and when the rules change you will change both.
Testability is a legitimate reason to invert on its own, and it is worth saying so directly rather than pretending the second implementation is coming. A fake repository that a unit test can drive is a real second implementation, and it justifies the abstraction by itself.
The interface is owned by the code that uses it, expressed in that code's vocabulary. Put it in the implementer's package and you have written an abstraction without inverting anything.
Likely follow-ups
- You have one implementation and no plans for a second. Is the interface still justified?
- Where should the interface file physically live, and why does that matter?
- How is this different from dependency injection?
- Your repository interface returns a database-specific cursor type. What has gone wrong?
Related questions
- Give me a real single-responsibility violation you have seen, and how would you refactor it?mediumAlso on solid5 min
- How would you implement a singleton, and why do many teams treat it as an anti-pattern?mediumAlso on testability4 min
- The business insists on a requirement that nobody can test — "the system must be intuitive". What do you write instead?mediumAlso on testability4 min
- What are the four pillars of OOP, and what is the difference between abstraction and encapsulation?easyAlso on abstraction4 min
- How does your order placement change on a venue that allocates pro rata within a price level instead of first in, first out?hardSame kind of round: concept6 min
- Adapter, decorator and facade - what concrete problem does each one solve, and where would reaching for it be over-engineering?mediumSame kind of round: concept6 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?mediumSame kind of round: concept6 min