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?
answer
- interface belongs to the client, not the supplier
- role interface vs header interface
- ports in the core, adapters outside, arrows point inward
- placement in the build module is what flips the arrow
- domain vocabulary in signatures — no SQL/vendor types
basics
~20 sThe 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.
solid answer
~50 sDIP is only achieved when the abstraction is owned by the *client*. The high-level module declares a *role interface* describing what it needs — `OrderRepository`, `PaymentGateway` — in its own vocabulary and its own package; the low-level adapter imports that package and implements it. This is the port-and-adapter shape of hexagonal/clean architecture: ports belong to the core, adapters live outside and point inward. Placement matters because dependency direction is a property of the *deployable/compilable unit*, not of the source file: if the interface ships in the infrastructure module, the core must depend on that module to compile, so the arrow never flipped. Consequences of getting it right: the core builds and tests with no drivers present, adapters can be swapped or added without recompiling the core, and teams can release on separate cadences. Getting it wrong yields "interfaces everywhere" with the original coupling intact.
code
pseudocode · 15 lines// core module — build file declares NO dependency on infra
package core.orders
interface OrderRepository { // role interface, domain terms
fun save(o: Order)
fun awaitingShipment(limit: Int): List<Order>
}
class ShipOrders(private val repo: OrderRepository) { ... }
// infra module — build file DOES depend on core
package infra.persistence
import core.orders.OrderRepository
class SqlOrderRepository(private val db: Db) : OrderRepository {
override fun awaitingShipment(limit: Int) =
db.query("select ... limit ?", limit).map { it.toDomainOrder() } // SQL never escapes
}go deeper
Say the consumer defines the interface and the implementation depends on it; the interface goes in the consumer's package.
Add the role- vs header-interface distinction and note that the interface's placement in the build module is what determines the real dependency direction.
Connect to ports and adapters / the Dependency Rule, discuss designing signatures in domain terms, when a shared contract module is justified, and enforcement via architecture tests.
Weigh boundary cost against volatility and team ownership; address transaction and error-semantics leakage, batch/performance leakage, and organizational effects (independent build/release cadence, Conway's law).
## The claim: interfaces belong to their clients A widely quoted formulation (Martin Fowler, echoing Robert Martin) is that **an interface should be owned by the module that uses it, not by the module that implements it**. The client knows what it needs; the supplier knows how to provide it. If the supplier defines the contract, the client is once again shaped by the supplier — the coupling has just acquired an extra hop. Two vocabularies for the same idea: - **Role interface** — defined from the caller's need (`Notifier`, `ClockSource`, `OrderRepository`). Small, motivated by a use case, named in domain terms. - **Header interface** — a mechanical mirror of an existing concrete class (`IEmailSender` with the same 14 methods as `EmailSender`). Defined from the supplier's shape. Almost never inverts anything. ## Placement is what actually creates the boundary Dependency direction is enforced at the granularity of the unit your build treats as a node: a package with enforced boundaries, a Gradle/Maven module, an npm workspace package, a .NET assembly, a service. ``` GOOD BAD (looks the same in a class diagram!) core/ core/ OrderService OrderService «interface» OrderRepository infra/ infra/ «interface» OrderRepository SqlOrderRepository ──▶ core SqlOrderRepository core ──▶ infra (arrow never flipped) ``` In the BAD layout the UML looks identical — a class depends on an interface, another class implements it — yet `core`'s build file still lists `infra` as a dependency. The core cannot compile without the driver library on the classpath; deleting `infra` breaks it; and every infra release forces a core rebuild. ## Where the shape has names - **Hexagonal / Ports & Adapters (Cockburn)**: the application defines *ports*; *adapters* on the outside implement driven ports (database, mail) and drive the driving ports (HTTP, CLI). All dependencies point inward. - **Clean Architecture (Martin)**: the Dependency Rule — source dependencies point only toward higher-level policy. Crossing outward requires DIP. - **Onion Architecture (Palermo)**: interfaces for infrastructure live in the domain/application layers. - **Separated Interface (Fowler, P of EAA)**: place the interface in a package separate from its implementation, usually with the client. - **Plugin architecture**: the host defines the plugin contract; plugins depend on the host. They are all the same move at different scales. ## When a third "contract" module is justified Sometimes several clients need the same abstraction, or the abstraction is a genuinely shared language (a `Clock`, a `Logger`, an event schema). Then a small, extremely stable **contract module** that both sides depend on is acceptable — dependencies still point at something more stable than either side, which is the underlying goal (cf. the Stable Abstractions/Stable Dependencies principles). The failure mode is a bloated `common`/`shared` module that everything depends on and that changes weekly: it becomes a global coupling point and inverts nothing. ## Designing the abstraction well (clause two of DIP) The interface must be expressed in the client's terms, or the detail leaks upward: - ❌ `fun query(sql: String): ResultSet` — SQL and driver types in the signature. - ❌ `fun send(m: MimeMessage): SmtpResponse` — vendor types. - ❌ `fun findAll(): Page<JpaOrderEntity>` — persistence entities. - ✅ `fun ordersAwaitingShipment(limit: Int): List<Order>` — domain vocabulary, domain types. Also keep it *narrow*: this is where DIP meets the Interface Segregation Principle. One consumer's port should expose only what that consumer uses; a 30-method `IRepository<T>` forces every adapter to implement (or throw on) methods nobody calls. ## Sizing and error semantics — the hard parts - **Leaky failure modes**: if the port's contract implicitly assumes `SQLException` semantics or HTTP status codes, an in-memory or queue-based adapter can't honour it. Define boundary-level failures (`NotFound`, `Conflict`, `Unavailable`) and let adapters translate. - **Transactions**: a naive per-method repository port can make multi-entity atomicity impossible. Options: a Unit-of-Work port, or expose use-case-shaped operations rather than CRUD. - **Performance leakage**: an abstraction that hides paging or batching can make a chatty adapter unavoidable. Sometimes the port must expose batch operations because the *client* legitimately needs them. - **The Cost**: every port you add is code, indirection, and a place for the abstraction to be wrong. Invert where volatility or ownership differs, not everywhere. ## How to enforce it Make the rule mechanical: module boundaries in the build tool, plus an architecture test (ArchUnit, dependency-cruiser, import-linter, NetArchTest, Spring Modulith's allowed dependencies) that fails CI when `core` imports `infra`. Documentation alone erodes; a failing build does not.
- When is it acceptable to put the interface in a separate shared module rather than in the client?When several independent clients need the same contract, or when the contract is a stable shared language (clock, logger, event schema, an API contract between services). The shared module must stay tiny and highly stable — if it accumulates helpers and changes constantly it becomes a coupling hub and defeats the purpose.
- How do you keep transactions working when persistence sits behind a port?Either express operations at use-case granularity so one port call is one atomic unit, or introduce an explicit Unit-of-Work / transaction-boundary port the application controls, with the adapter mapping it to the real mechanism. Avoid leaking framework transaction annotations or session objects into the core.
- How do you stop the rule from rotting over time?Enforce it in the build: real module boundaries plus an automated architecture test that fails when the core imports infrastructure. Reviews and docs are not sufficient at team scale.
A restaurant writes its own order ticket format and suppliers must fit it. If instead each supplier handed the kitchen its proprietary form, the kitchen would be rebuilt around whichever supplier it uses — the contract must be written by the party whose stability you care about.
saying these in an interview costs you the question
- Defining the interface next to (or inside) the implementation module and calling it dependency inversion.
- Interfaces named after the implementation (IEmailSender for EmailSender) mirroring every method.
- Port signatures containing SQL strings, ORM entities, HTTP types, or vendor exceptions.
- One giant generic repository interface forced on every adapter, ignoring Interface Segregation.
- A sprawling `common`/`shared` module that everything depends on, sold as "the abstraction layer".
- Relying on convention rather than build-enforced boundaries to keep the arrows pointing inward.