skip to content

Clean/Onion/Hexagonal architectures all state a Dependency Rule: source dependencies may only point one way. Which way, why that way, and how does it turn infrastructure into a plugin?

level: middleimportance: must knowfreq 68%

answer

  1. arrows inward, toward policy
  2. inner may not name anything outer
  3. outward calls cross via inside-owned ports
  4. core = host, framework/DB = plugins
  5. enforce with separate artifacts or arch tests

basics

~20 s

Source dependencies point inward, toward policy: UI and database depend on use cases, use cases depend on the domain, and nothing inner knows anything outer. Because the inner circles define the interfaces, outer pieces plug into them and can be swapped like plugins.

solid answer

~50 s

The Dependency Rule says every source dependency must point toward higher-level policy — outer rings (frameworks, controllers, database, message brokers) may name inner rings (use cases, entities), never the reverse. Nothing in an inner circle may mention a name declared in an outer circle: no ORM annotations on entities, no HTTP request types in use-case signatures, no framework types leaking inward. Because calls often need to go outward (a use case needs to persist), the outward calls cross the boundary through an interface (a 'port') declared inside and implemented outside, inverting that one dependency. The consequence is a plugin architecture: the business core is the host, and the web layer, the database, and the CLI are interchangeable plugins that depend on the core, while the core has no compile-time knowledge that they exist. Practically this yields independent testability, deferred framework/DB decisions, parallel work across boundaries, and blast-radius control — bought with more types, more mapping between inner and outer models, and more indirection.

go deeper

for a junior

Say dependencies point inward toward business rules, and give one example: the database depends on the domain, not the other way round.

for a middle

Add the corollary (inner code must not name outer things), explain how outward calls cross via inside-owned interfaces, and list concrete violations like ORM annotations on entities.

for a senior

Connect the rule to level = distance from I/O, discuss boundary data mapping costs, and describe automated enforcement plus where you would deliberately relax it.

for a principal

Frame the rule economically — protecting decision reversibility and blast radius — and discuss artifact/build topology, team boundaries along ports, and the failure mode where 'shared' modules re-couple everything.

## Vocabulary - **Ring / layer / circle**: a grouping of code by *level* — how far it is from input/output. Entities and domain rules are innermost; frameworks, drivers, UI, and DB are outermost. - **Level** (precise definition): distance from the inputs and outputs of the system. Higher level = further from I/O = closer to why the system exists. - **Source dependency**: A names B, so A cannot compile/load without B. - **Port**: an interface declared by the inside, describing something the inside *needs* (driven/outbound port) or *offers* (driving/inbound port). - **Adapter**: an outside implementation of a port — a Postgres adapter, an SMTP adapter, an HTTP controller. ## The rule > Source code dependencies must point only inward, toward higher-level policy. And its sharp corollary: **a name declared in an outer circle must not be mentioned by an inner circle** — not a class, not a function, not a variable, not an annotation, not a data structure with the outer layer's shape. Why inward? Because inner code is more *stable* and more *valuable*: business rules change on business timescales; frameworks, drivers, and delivery mechanisms change on vendor timescales. Making the volatile depend on the stable means the frequent changes stop at the boundary. If you have the arrow backwards, an ORM upgrade can force you to touch the pricing rules. ## The tension the rule creates, and the fix Use cases must *call* outward — save an order, send an email, publish an event. That call direction is outward, but the source dependency must be inward. Resolve it exactly as DIP prescribes: ``` [use case] ──calls──▶ «port: OrderRepository» ◀──implements── [Postgres adapter] inner ring inner ring outer ring source arrow points inward ▲ ``` Every outward call crosses the boundary through an inside-declared interface. This is why people say Clean Architecture is "DIP applied at architectural scale". ## From layers to a plugin architecture Once all arrows point in, the picture inverts from a stack to a host + plugins: - **Host**: entities + use cases. Compiles alone. Has no idea whether it is driven by HTTP, a CLI, a test harness, or a queue consumer, nor whether it is backed by Postgres, an in-memory map, or a file. - **Plugins**: web framework, DB adapter, external clients, schedulers, UI. Each depends on the host; none is depended upon. Consequences worth naming in an interview: 1. **Independent testability** — use cases run with fakes, in milliseconds, no containers. 2. **Deferred/reversible decisions** — you can start on an in-memory store and choose the database later; you can add a gRPC front end without touching policy. 3. **Blast radius** — a plugin's failure or churn cannot ripple inward; the compiler enforces it. 4. **Independent development** — teams work either side of a port once the port is agreed. 5. **Build/deploy structure** — if rings are separate artifacts, the core artifact has zero framework dependencies, which also cuts build times. ## Crossing boundaries with data Data that crosses a boundary must be in the *inner* layer's shape, or a simple structure convenient to it — never a row object, an HTTP request, or an ORM-managed entity. Hence mapping/DTO code at each boundary. This is a real, recurring cost: many teams reasonably relax it for CRUD-heavy modules and keep it strict where the rules are rich. Say so out loud rather than pretending mapping is free. ## Common violations (interviewers probe for these) - ORM/serialization annotations on domain entities — an inner class naming an outer framework. - Use cases returning framework response types or accepting request objects. - A repository interface that returns a query builder, lazy proxy, or cursor — the abstraction depends on the detail (DIP clause 2). - "Shared/common" utility modules that everyone imports and that themselves import the web framework, quietly re-coupling the core. - Exceptions from the driver escaping inward, so the core must catch `SQLException`. ## Enforcement Wishful thinking does not hold arrows in place. Use physical separation (one build artifact per ring, so a wrong import cannot compile), language module systems, or architecture tests (ArchUnit-style rules, import-linters, dependency-cruiser configs) run in CI. Without automated enforcement the rule decays within a few sprints. ## When to relax it For a small CRUD service with no interesting rules, full ring separation can cost more than it returns — the mapping layers dominate the actual logic. A defensible middle path: keep the *direction* rule (never let the core name the framework) but reduce the *number* of rings and skip DTO mapping where the shapes coincide. Deciding this deliberately is senior behavior; applying four rings by reflex is not.

  • A use case must send an email. How does that call go outward without an outward source dependency?
    The use-case layer declares a `Notifier` port in its own module and calls it; an SMTP adapter in the outer ring implements it and is wired in at the composition root. The call goes out, the source arrow comes in.
  • Are ORM annotations on domain entities a Dependency Rule violation?
    Yes — an inner class names an outer framework, so the core no longer compiles or evolves independently of the persistence library. The strict fix is separate persistence models plus mapping; the pragmatic compromise is a conscious, documented exception.
  • How do you stop the rule from eroding over time?
    Make violations fail the build: separate build artifacts per ring, module systems, or architecture tests in CI. Review alone does not hold; a single 'temporary' import becomes permanent.

A power tool with interchangeable heads. The tool body defines the mount; drills, sanders and saws are built to fit it. The body was designed without knowing which heads would ever exist, and adding a new head requires no change to the body.

saying these in an interview costs you the question

  • Saying the rule is about call direction rather than source-dependency direction.
  • Claiming the core is decoupled while domain entities carry ORM or serialization annotations.
  • Use cases that accept or return the web framework's request/response types.
  • Believing the Dependency Rule removes the runtime need for a database or framework.
  • Treating 'four rings and a DTO per boundary' as mandatory regardless of the system's complexity.
  • A 'common/shared' module that imports the framework and is imported by the core.
  • Relying on code review instead of automated checks to keep arrows pointing inward.

context