skip to content

In a classic three-tier layout where a `controller` package depends on a `service` package, which in turn directly imports concrete classes from a `repository` package that wraps the database, the code compiles and runs fine. Why is this still considered a dependency-direction problem, and how would you restructure the packages to fix it?

level: middleimportance: must knowfreq 80%

answer

  1. interface owned by service, not repository
  2. concrete class implements, doesn't get imported directly
  3. composition root does the wiring
  4. compiles fine but still wrong
  5. Spring Data base interface still a framework type

basics

~20 s

It compiles, but the business-logic layer is now stuck to one specific way of talking to the database — you can't swap it or test the logic without the real database. Fix: put an interface next to the business logic and make the database code implement that interface instead of the other way around.

solid answer

~40 s

The bug isn't compilability, it's coupling direction: the service layer, which should hold the stable business rules, holds a compile-time reference to a concrete, swappable implementation detail. That means every persistence change ripples into the service layer, and unit-testing the service requires either a real database or awkward subclassing/mocking of a concrete class. The fix is to invert the dependency: define a narrow repository interface owned by (or next to) the service package expressing only the operations the use case needs, move the concrete implementation into the repository/infrastructure package, and have that package implement the interface. The service now imports only the interface; a composition root wires the concrete class in via dependency injection, so runtime behavior is identical but the compile-time arrow now points from repository to service, not the reverse.

go deeper

for a junior

Can point out that service shouldn't hold a field typed as the concrete repository class and can name 'use an interface' as the fix, even without full DIP vocabulary.

for a middle

Can actually perform the restructuring — write the interface, move the concrete class to implement it, and explain where wiring happens.

for a senior

Proactively raises the Spring-Data-base-interface nuance, discusses when the extra hand-rolled interface is/isn't worth it, and connects it to concrete testing benefits.

for a principal

Frames it as a recurring organizational pattern, discusses how to encode the rule so every future repository follows it (templates, arch tests, review checklist) rather than fixing this one instance ad hoc.

## The setup that compiles Picture three packages: `controller`, `service`, and `repository`, arranged so: - `controller` imports from `service` - `service` imports concrete classes from `repository` — say `OrderServiceImpl` directly holds a field typed `JpaOrderRepository` Every import statement resolves, the build succeeds, and requests flow correctly end to end. Nothing about this trips a compiler error, which is exactly why the problem is easy to miss: **dependency direction is a design property, not a syntactic one**, and the language happily lets you couple a stable layer to a volatile one. ## Why it still counts The reason this still counts as a dependency-direction problem is what the coupling costs you later, not whether it works today. `service` is meant to hold the rules that make the application what it is. `repository`, specifically the concrete `JpaOrderRepository`, is an implementation detail of how data happens to be persisted right now. By importing the concrete class, `service` inherits every constraint of that detail: - it needs a real or embedded database to be constructible at all - any change to the ORM's API surface is a compile error in business-logic code - there is no seam at which a test double can be substituted without reaching for a heavier mocking framework that mocks concrete classes (which is brittle — it breaks on refactors to methods the test never called) ## The fix The fix is the same inversion move regardless of language: 1. Define an interface that expresses only the operations the use case actually needs — not everything the concrete class happens to expose — and place that interface in or next to the `service` package. Something like `interface OrderRepository { findById(id): Order?; save(order) }`. 2. Rename the concrete class to implement it. `service` now has an import of `OrderRepository`, an interface it effectively owns, and zero imports of anything persistence-specific. 3. `repository`, in turn, imports `OrderRepository` from `service` in order to implement it — **the arrow has reversed**. A composition root (a configuration class, a manual wiring function, whatever the framework's entry point is) is the one place that knows about both the interface and the concrete implementation and wires them together; that file is allowed to violate the rule because its entire job is to bridge the two worlds. ## What it buys - **A unit test.** This is worth doing because it changes what a unit test looks like: testing `OrderService` now means providing a hand-written or generated fake `OrderRepository` (an in-memory map, for instance) with no database and no application context, running in milliseconds instead of seconds. - **A persistence migration.** It also changes what a persistence migration looks like: swapping the ORM, or adding a caching decorator around persistence, touches only the `repository` package and the composition root — `service` is untouched, because it never depended on the concrete detail to begin with. ## The nuance worth knowing There's a real nuance worth knowing, especially in Spring/JPA codebases: **Spring Data repository interfaces look like they already satisfy this pattern** because they're interfaces — but the base type they extend is itself a Spring Data library type, so the interface, even with no concrete JPA import, still carries a compile-time dependency on that library. For most teams this is an accepted, pragmatic compromise, but teams practicing strict hexagonal architecture sometimes add one more layer — a hand-rolled port interface with no framework supertype, implemented by an adapter class that internally delegates to the Spring Data interface — specifically to keep the use-case-facing interface framework-free. ## What it costs The restructuring isn't free: it adds a file, a rename, and a wiring step, and for a one-off script or a service that will only ever have one persistence technology, that ceremony buys little. But for any service expected to be unit-tested in isolation, evolve its persistence technology, or run against a fake/in-memory implementation in some environments, inverting this dependency is what makes those things possible without a rewrite.

  • If `service` already imports an interface `OrderRepository`, but that interface itself extends a Spring Data base interface, has the dependency direction problem actually been fixed?
    Only partially — the concrete implementation class dependency is gone, but the interface now carries a compile-time dependency on the Spring Data library, so `service` still can't be compiled or unit-tested without that library on the classpath. Full isolation requires a hand-rolled interface with no framework supertype, implemented by an adapter that internally uses the Spring Data interface.
  • Where should the mapping between the domain `Order` object and a persistence entity class live after this restructuring?
    In the repository/infrastructure package, typically alongside the repository implementation — that package already depends on both the domain type (to implement the interface) and the entity type, so it's the natural place for the translation, keeping the domain model free of persistence annotations.
  • Does this restructuring change anything about runtime behavior or performance?
    No — the same concrete class executes the same database calls at runtime; only the compile-time reference direction and the seam available for substitution (tests, alternate implementations) change. Any perceived overhead is one virtual dispatch through an interface, which is negligible.

Like a company's CEO holding a direct personal phone number for one specific delivery driver instead of calling 'the courier service' — if that driver quits, the CEO's own rolodex has to change. Define a 'courier' role (interface) the CEO calls, and swap drivers behind it freely.

saying these in an interview costs you the question

  • Says the code is fine because 'it compiles and the tests pass'
  • Can't name what would have to change if the persistence technology were replaced
  • Doesn't know how to unit-test the service without a running database once shown the coupled version
  • Thinks a Spring Data interface fully isolates the service layer from the framework
  • Proposes fixing it by mocking the concrete class instead of introducing an interface

context