skip to content

Dependency Direction in Code

Applying dependency inversion to the package tree: interfaces live with their consumers, adapters depend on abstractions, and nothing in the core points outward at infrastructure. It is where an architectural principle becomes a concrete decision about which package a file goes in.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

In a layered application, what does it mean for source-level dependencies to 'point inward', and why does this matter for the codebase's design?

level: juniorimportance: must knowfreq 65%

answer

  1. arrows point inward
  2. domain doesn't import framework
  3. stable core, volatile edges
  4. swap infra without touching logic

basics

~20 s

Code in the core business logic should not need to know about outer layers like databases or web frameworks. Only the outer layers should import and depend on the inner ones, never the other way around.

solid answer

~30 s

'Pointing inward' means the import/reference arrows in your source code all flow from outer, detail-heavy layers (UI, database, frameworks) toward the inner, stable layer (business rules/domain). The domain package never imports a database driver or a web-framework class; instead, the database adapter imports the domain's interfaces and implements them. This keeps the core logic testable and framework-agnostic, and it lets you swap infrastructure (e.g., switch database or HTTP framework) without touching business rules, because change flows toward the layers designed to absorb it.

go deeper

for a junior

Should recognize the pattern name and give the 'outer depends on inner' summary with one concrete example (e.g., domain shouldn't import a web framework).

for a middle

Should be able to point to specific package examples and explain how dependency injection wires the runtime call in the opposite direction from the compile dependency.

for a senior

Connects it to the Dependency Inversion Principle and Stable Dependencies idea, discusses cost/benefit, and knows when it's worth it.

for a principal

Discusses this as an architectural policy enforced across teams/modules, ties it to long-term evolvability, and knows the failure modes at scale (boundary erosion, big ball of mud).

## What the arrows mean **"Dependency direction"** describes which way the compile-time references point between the parts of a codebase — literally, which files `import` or reference which other files. In a well-structured application you can draw the packages as concentric rings: - an **inner ring** holding business rules and domain models - a **middle ring** holding application/use-case orchestration - an **outer ring** holding technical details — web controllers, database access code, message-queue clients, third-party SDKs **"Pointing inward"** means every arrow you'd draw for an import statement goes from an outer ring toward an inner one, never the reverse. A controller class may import a use-case class; a use-case class may import a domain interface; but the domain package itself imports nothing from the controller or database packages. If you inspect the domain package's import statements and find `org.springframework` or `javax.persistence` or an HTTP client library, dependency direction has been violated, no matter how good the underlying business logic is. ## Why the rule exists This rule exists because it encodes a bet about what changes and what doesn't. | Stays put | Moves | |---|---| | Business rules — how a loan is approved, how a cart total is computed — tend to be relatively stable and are the reason the software exists. | Frameworks, databases, and external APIs are comparatively volatile: they get upgraded, replaced, or swapped for a competitor's product for reasons that have nothing to do with the business rules. | The **Dependency Inversion Principle** formalizes this as "depend on abstractions, not concretions," and the related **Stable Dependencies** idea adds the corollary that things should depend in the direction of increasing stability. If the domain package depends on the database package, then every time the database technology changes, the change pressure flows into the one part of the system you most want to protect. If instead the database package depends on interfaces defined by the domain, the database can be swapped, mocked, or reimplemented and the business rules never need to be touched, recompiled for a different reason, or even re-read. ## How the inversion works The mechanism that makes this possible despite runtime control flow going the "wrong way" is **dependency injection through an interface owned by the inner layer**. Say the `orders` use case needs to persist an order. 1. Instead of importing a `JpaOrderRepository` class directly, the use-case package declares an `OrderRepository` interface expressing only what it needs (`save(order)`, `findById(id)`). 2. The persistence package then depends on the use-case package to implement that interface with `JpaOrderRepository`. 3. At runtime, something at the edge of the system — a configuration class, a manual factory, a DI container — wires the concrete `JpaOrderRepository` into the use case. So calls still flow outward (the use case ends up invoking JPA code indirectly), but the source dependency points inward: the use-case package has zero import statements referencing JPA. ## The cost The cost of this discipline is real and worth naming honestly. Every port you introduce is: - an extra file - an extra layer of indirection - often an extra mapping step between the domain's shape and the outer layer's shape For a small CRUD app with one database and no plausible reason to swap it, this ceremony can slow a team down for no payoff — the abstraction has a cost but no realized benefit if nothing ever plugs in besides the one implementation. Teams need to weigh the likelihood of actually needing to substitute an implementation against the tax of the extra indirection on every change. ## Failure modes Failure modes show up gradually rather than as a single build error, because a single misplaced import doesn't break anything today. - The **classic symptom** is that unit-testing the core business logic becomes impossible without spinning up a real database, an application context, or a mocked HTTP server, because the "unit" under test transitively pulls in the framework. - **Another symptom** is that a seemingly small technology decision — upgrading an ORM, switching a payment provider — turns into a sprawling refactor that touches domain classes nobody expected to change. - **At the extreme**, enough inward-pointing violations accumulate that the dependency graph has cycles between what were meant to be layers, and the codebase degrades into what's colloquially called a "big ball of mud," where no part can be understood, tested, or changed in isolation. ## Ports and adapters A concrete, widely used embodiment of this idea is **Hexagonal (Ports and Adapters) Architecture**: the domain/application core defines "ports" (interfaces) for everything it needs from the outside world, and "adapters" — a REST controller, a JPA repository, a message consumer — live in outer packages and implement or call those ports. The core package can be compiled, tested, and reasoned about with zero framework dependencies on the classpath at all.

  • What's a concrete code smell that signals dependency direction has been violated in a backend codebase?
    A domain class annotated with a persistence-framework annotation, or extending a JPA base class, or a domain interface method that takes a servlet request type — these show the core layer directly referencing framework types, meaning outer-layer types have leaked inward and can't be tested or swapped without dragging the framework with them.
  • How does dependency direction differ from data flow / call flow direction?
    Call flow can be bidirectional — infrastructure calls into the domain to invoke a use case, and the domain 'calls' infrastructure indirectly through an interface it defines. Compile-time dependency (import) direction stays inward even when runtime control flow moves outward, because the interface implementation is injected, not directly referenced.
  • Does 'dependency direction' apply only to layers, or also across features/modules?
    It applies to any dependency boundary, not just horizontal layers — feature modules should also avoid one module's core logic depending on another module's implementation details; the appropriate abstraction (a port/API) is owned by whichever side is meant to be stable.

Think of it like a restaurant kitchen: the head chef's recipe (business logic) shouldn't need to know which brand of oven is installed; the oven (infrastructure) is built to fit the recipe's requirements (a port), not the other way around.

saying these in an interview costs you the question

  • Says 'just import the DB class directly, it's simpler'
  • Can't explain why unit tests need mocking/DB spin-up when direction is violated
  • Confuses call/runtime direction with source dependency direction
  • Thinks placing an interface always in the same package as its implementation is fine regardless of who calls it

context

open as a page

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%

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.

open as a page

What concrete costs does strictly enforcing inward-pointing dependencies (ports/interfaces for every outward call) impose on a codebase, and under what circumstances would you deliberately not enforce it?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Every interface you add 'just in case' is extra code to write, read, and navigate, and it can slow a small team down for no real benefit if there's genuinely only ever going to be one implementation. Skip the strict discipline for small, short-lived, or truly single-technology apps where the 'swap' you're protecting against will never happen.

open as a page

Two feature modules, `orders` and `inventory`, are each internally well-layered — inside each, controllers depend on services depend on domain, all pointing inward. But `orders`' domain package imports a class from `inventory`'s domain package to check stock, and separately `inventory`'s domain package imports a class from `orders`' domain package to look up order history. What problem does this create, and how would you fix it while keeping both modules internally clean?

level: middleimportance: should knowfreq 55%

basics

~20 s

Even though each module looks fine inside itself, the two modules now depend on each other in both directions, so you can't understand, build, or test one without the other. Fix: pick which module is allowed to know about the other, or use an event so neither core has to import the other's internals directly.

open as a page

A codebase started with clean inward-pointing dependencies, but eighteen months later a code review finds a domain class importing a REST client class from the outer web layer. Nothing in the build pipeline caught it before merge. What allowed this drift to happen, and what would you put in place to catch it automatically going forward?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Nobody was checking; the compiler happily allows any import as long as types resolve, so 'clean architecture' without an automated check is just a convention people can forget under deadline pressure. Fix: add an automated test that fails the build if a package in the inner layer imports anything from an outer-layer package.

open as a page

Beyond an ArchUnit-style test that checks package dependency direction within one compilation unit, some teams split layers into physically separate build modules (e.g., separate Gradle modules or JARs) with explicit `dependencies {}` declarations. What additional guarantee does a physical module boundary provide that a same-source-tree architecture test does not, and what does it cost?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

A test only catches a wrong-direction import when someone runs the test suite. A separate build module makes it flat-out impossible to compile a wrong-direction import in the first place, because the compiler can't even see the other module's code unless it's declared as a dependency.

open as a page