skip to content

In Onion Architecture, how exactly does the Dependency Inversion Principle let the domain and application rings avoid depending on the database, and what would a violation of that rule look like in real code?

level: middleimportance: must knowfreq 60%

answer

  1. interface owned by inner ring, implemented by outer ring
  2. composition root wires concretes at startup
  3. control flow still goes outward; import direction inverts
  4. leaky interface = infra types in the contract
  5. ORM entity doubling as domain model is a common violation

basics

~20 s

The inner code defines an interface saying what it needs (like 'save this order'); the outer database code writes a class that fulfills that interface. The inner code never imports database code directly — it just calls the interface, and something else plugs in the real database class at startup.

solid answer

~50 s

The mechanism is: the inner ring declares an abstraction (an interface/port) expressed purely in domain terms, e.g. OrderRepository.save(order: Order). The outer infrastructure ring implements that interface with technology-specific code (JpaOrderRepository, MongoOrderRepository), and a composition root or DI container wires the concrete class into the abstraction at startup. Source-level imports point inward — infrastructure imports the domain's interface and types — even though the runtime call still flows outward from application service to database. A violation looks like an application service or domain class directly instantiating or importing an ORM/DB-specific type: an EntityManager, a JDBC Connection, a Mongo Document, or an ORM-generated entity class used as the actual domain model. Another common violation is a repository interface that leaks infrastructure concepts — e.g., a method returning a JPA-specific Page<T> or accepting a raw SQL Specification — which technically satisfies 'there's an interface' but still couples the inner ring's contract to outer-ring types.

go deeper

for a junior

Should grasp that there's an interface the inner code calls and a real implementation that plugs in later, without needing to name composition roots or leaky-abstraction subtleties.

for a middle

Should correctly explain that the interface is owned by the inner ring, implemented by the outer ring, and wired via DI/composition root, and should be able to spot an obvious violation like a domain class importing a JDBC type.

for a senior

Should recognize subtler violations such as ORM entities doubling as domain objects or repository interfaces leaking framework-specific types like paging classes, and explain why those defeat the pattern's purpose even though 'an interface exists'.

for a principal

Should be able to set and justify a team-wide policy on where the line is drawn (e.g., whether extending a framework repository interface directly is acceptable for a given project's risk profile) and decide how strictly to enforce it with tooling versus review, given the project's actual likelihood of infrastructure churn.

## What the principle actually states The **Dependency Inversion Principle** (DIP), as stated by Robert Martin, says two things: 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. Onion Architecture is DIP applied at the scale of an entire application's module graph rather than at the scale of two classes. - The **'high-level module'** is the business logic — the domain model and the application services that orchestrate use cases. - The **'low-level module'** is everything that talks to the outside world: SQL, HTTP clients, message brokers, file systems, third-party APIs. ## Inverting the arrow, step by step Naively, you'd expect the high-level module to depend on the low-level one, because conceptually 'placing an order' needs 'writing a row to the orders table.' DIP inverts this: 1. Instead of the application service importing a `SqlOrderWriter` class from the persistence package, the application service declares an interface it owns — commonly called a **port**, or a repository/gateway interface — such as `interface OrderRepository { fun save(order: Order); fun findById(id: OrderId): Order? }`. This interface is written entirely in domain vocabulary: it takes and returns domain types, never SQL rows, JSON, or ORM entities. 2. The persistence package, sitting in the outer infrastructure ring, then provides a class like `PostgresOrderRepository` that implements `OrderRepository`, translating between domain objects and whatever the database actually stores. Crucially, `PostgresOrderRepository` has to import `OrderRepository` and `Order` from the inner ring to implement the interface — so the compile-time import arrow points from outer to inner. The inner ring's source code never imports anything from the persistence package. 3. At runtime, calls still have to reach the database — that's what a dependency injection container or a manual **composition root** (a small piece of code, typically in `main`, that wires concrete implementations to abstractions) is for: it constructs `PostgresOrderRepository` and hands it to the application service's constructor as the `OrderRepository` it was asking for. The direction of control flow is unchanged; only the direction of the compile-time dependency is inverted. ## Two intertwined reasons for the split This split exists for two intertwined reasons. 1. **First, it isolates the cost of infrastructure change.** If you migrate from Postgres to a different database, or swap an ORM, you write a new class implementing the same interface and change wiring in the composition root — you do not touch the application service, the domain model, or any of their tests. 2. **It makes the business logic unit-testable in isolation** — second, and just as important in practice. Tests can supply an in-memory fake implementing `OrderRepository` (a simple HashMap-backed class) instead of standing up a real database, which is the difference between a test suite that runs in milliseconds and one that needs Testcontainers or a shared test database. ## The recognizable shapes of a violation Violations happen in a handful of recognizable shapes, and recognizing them is a core code-review skill for this pattern. 1. **The most blatant** is a domain or application-layer class directly importing an infrastructure library type — an `EntityManager`, a JDBC `Connection`, a Mongo `Document`, an HTTP client, or a message broker's `Channel` — anywhere in its method signatures or bodies. 2. **A subtler and more common violation** is using the ORM-generated entity class as if it were the domain model itself: many Java/Kotlin codebases let a JPA-annotated class double as the business object, which quietly wires JPA into every piece of code that touches that type, including the domain layer. 3. **Subtler still** is 'leaky' abstraction: the interface technically exists in the inner ring, satisfying the letter of the rule, but its shape is dictated by the persistence technology rather than by what the business logic actually needs — e.g., a repository method that takes an ORM-specific `Specification` object as a filter parameter, or returns a `Page<T>` type defined by a data-access framework. These leaks mean that swapping the database implementation still requires touching the interface (and therefore the inner ring), which defeats the purpose even though 'there's an interface' checks the box superficially. ## What this looks like on a real team A concrete production example: teams using Spring Data JPA commonly extend `JpaRepository<Order, Long>` directly as their domain-facing repository interface. This is convenient — you get a working implementation with zero code — but it means the domain-facing contract now exposes Spring Data method names, paging types, and query-derivation conventions, which is a low-grade violation of the inversion even though no import statement in the domain package names the framework directly. Teams that care about a clean boundary instead hand-write a narrow domain-owned interface and implement it with a thin adapter class that internally delegates to a `JpaRepository`, paying a small amount of boilerplate for a contract that stays purely in domain terms.

  • What is a composition root, and why does Onion Architecture need one?
    A composition root is the single place — usually near application startup, e.g. a main function or a DI container's configuration — where concrete infrastructure implementations get wired to the abstractions the inner rings declared. Onion Architecture needs it because inversion only removes the compile-time dependency; something still has to supply a real PostgresOrderRepository object to satisfy the OrderRepository interface at runtime, and doing that wiring inside the domain or application code would reintroduce the very dependency the pattern is trying to avoid.
  • Why is it a violation to let a repository interface expose a type like a data-access framework's Page<T>, even though the interface technically lives in the inner ring?
    Because Page<T> is a type defined by the persistence framework, so any inner-ring code calling that method now transitively depends on that framework's types through the interface's signature. If you swap persistence technology, you have to change the interface itself, which forces changes back into the inner ring — defeating the purpose of drawing the boundary there in the first place.
  • How do tests differ for an application service under Onion Architecture versus one that directly calls a database?
    With a properly inverted design, tests supply a fake or in-memory implementation of the repository interface, so the application service's use-case logic is tested in isolation, in memory, in milliseconds, with no database process running. Without inversion, the same test would need a real or containerized database, making it slower and more brittle, and business-rule bugs get harder to separate from infrastructure/environment failures.

Like a power outlet standard: appliances (inner, high-level) are built against the wall-socket shape (the abstraction) the room defines, not against a specific power plant's wiring; the power company (outer, low-level) has to conform its wiring to fit the socket, not the other way around.

saying these in an interview costs you the question

  • Thinks DIP means 'use interfaces everywhere' without explaining who owns the interface
  • Believes the runtime call direction and the compile-time dependency direction are the same thing
  • Doesn't recognize an ORM entity being used as the domain model as a violation
  • Can't name what wires the concrete implementation to the abstraction at startup
  • Thinks a repository interface satisfies the rule regardless of what types appear in its method signatures

context