In Hexagonal Architecture, why does the application core never hold a compile-time dependency on any adapter, and what mechanism makes that possible given that at runtime the core's logic still has to run inside a real web server or database transaction?
answer
- composition root wires adapters in
- core imports zero framework classes
- constructor injection carries the port
- DIP: abstraction owned by high-level side
- IoC = core doesn't `new` its own dependencies
basics
~20 sThe core only knows about interfaces it defines itself, never concrete tech classes. A separate wiring step (like a dependency-injection setup) plugs the real adapters in at startup, so the core's code never imports database or web code directly.
solid answer
~30 sThe core depends only on the ports it defines — interfaces — never on adapter implementations. This is dependency inversion applied at the architecture level: high-level policy (business logic) defines the interface; low-level detail (a framework, a database driver) implements it, so the arrow always points inward. At runtime, a composition root (main() plus a DI container/config) instantiates concrete adapters and injects them wherever the core's constructor asks for a port — inversion of control means the core never does `new StripeClient()` itself; something external hands it an object that satisfies `PaymentPort`.
go deeper
Should grasp that the core doesn't create its own adapters directly, even if 'composition root' and 'DIP' aren't yet familiar terms.
Should be able to name constructor injection and a composition root as the mechanism, and explain why a build-time import from core to adapter is a violation.
Should be able to design the wiring for a multi-adapter system and set up an architecture-test rule that enforces the boundary automatically.
Should reason about composition-root complexity at scale (multi-module systems, per-environment wiring), and weigh when to relax strict enforcement against team velocity.
## Dependency inversion, applied to a whole boundary Dependency inversion is the principle that high-level modules (the code encoding business policy) should not depend on low-level modules (the code encoding technical detail); both should depend on abstractions, and the abstraction should be owned by the high-level side. Hexagonal Architecture is essentially dependency inversion applied consistently to an entire application's boundary, on both sides at once. Concretely: the application core module contains interfaces (**ports**) and the logic that calls them, but it imports zero framework classes — no persistence-library annotations, no HTTP client library, no message broker SDK. Every place the core needs to reach outward, it reaches through an interface it declared itself. ## The mechanism at runtime The mechanism that makes this survivable at runtime — because a real running system obviously does need an actual database connection and an actual HTTP server — is a **composition root**, sometimes called the wiring or bootstrap layer. This is a thin, separate piece of code whose only job is to construct concrete adapter instances and hand them to the core's constructors wherever a port type is required. - The core class might have a constructor `OrderService(OrderRepository repo, PaymentPort payment)` where both parameters are interfaces. - The composition root is the only code in the whole system that knows `repo` is actually a `JpaOrderRepository` and `payment` is actually a `StripeAdapter`. This is **inversion of control (IoC)**: instead of the core actively creating its own dependencies — which would force it to import their concrete classes — control over which implementation is used is inverted and handed to an external assembler. **Dependency injection** is the specific technique (constructor injection, in most idiomatic hexagonal code) used to carry out that inversion. ## Why bother Why go to this length: the problem it solves is **architectural erosion under maintenance pressure**. In a layered architecture without this discipline, it's extremely common for a service-layer method to directly call a repository's ORM-specific method, or for business logic to peek at request headers, because nothing structurally prevents it — the layers are a naming convention, not an enforced boundary. Over years, business rules end up smeared across controllers, services, and repository classes, and a change like "add a second delivery channel" or "switch database vendors" requires an archaeology dig through infrastructure code to find the actual business rule buried in it. By making the core physically unable to compile against adapter code — the module doesn't have that dependency on its classpath — hexagonal architecture converts a discipline problem into a **build-time guarantee**: if someone tries to import a concrete repository class into the core module, the build fails (this can be enforced automatically, e.g., with architecture-test rules or a module system). ## Where the cost lands The cost is real and shows up in two places. 1. **First, indirection tax:** every port needs a caller and at least one implementer, and tracing a call from an HTTP request to the database now means jumping through an interface rather than reading a straight call stack, which can slow down newcomers navigating the codebase and adds files/boilerplate for simple CRUD paths that will realistically never get a second implementation. 2. **Second, composition-root complexity:** as the number of ports grows, the wiring code becomes its own maintenance surface, and misconfigured wiring — forgetting to bind an interface to an implementation, or accidentally wiring the wrong adapter, such as a test double left wired in production config — is a class of bug that pure layered code doesn't have, because in layered code there's no indirection to misconfigure. ## Failure modes - **A failure mode particular to this mechanism is "fake hexagonal"**: teams introduce port interfaces but each interface has exactly one implementation ever, wired 1:1, purely as ceremony — no test double, no swap ever happens — so the team pays the indirection tax for a promise of flexibility that's never exercised. - **Another is composition-root sprawl**, where wiring logic creeps into places other than the designated root (e.g., a controller directly instantiating an adapter "just this once"), silently reintroducing the coupling the whole design was meant to prevent. ## How it plays out in production A concrete real-world case: a payments team building a service that must support both a legacy on-prem database deployment and a newer cloud database deployment for different clients defines a single `AccountRepository` port in the core; the composition root, driven by an environment-specific configuration profile, wires in the on-prem implementation or the cloud implementation at startup. The core's business logic — **interest calculation, overdraft rules** — is identical bytecode running against either database, and was tested once against an in-memory fake, entirely decoupled from either concrete database technology.
- What would you look for in a code review to catch a core module that's secretly coupled to a framework?Scan the core module's imports for anything from a persistence, web, or messaging library — an ORM annotation, a framework stereotype annotation, a servlet type. Any such import is a violation because the core should only reference its own port interfaces and plain domain types. Tools like ArchUnit can automate this as a build-breaking rule rather than relying on manual review.
- What breaks if the composition root wires the wrong adapter to a port, e.g., binds a `PaymentPort` to an in-memory test fake in the production build?The application runs and even passes shallow smoke tests, but no real payment is ever charged — money silently doesn't move while the app reports success, because the fake adapter fulfills the interface contract without doing real work. This is a runtime wiring bug, not a compile error, which is exactly the class of risk composition-root discipline and integration tests targeting the wiring itself are meant to catch.
- Is dependency inversion at the architecture level the same thing as the classic Dependency Inversion Principle (the 'D' in SOLID)?Yes in substance — hexagonal architecture is DIP applied at a coarser, whole-application granularity rather than between two classes. The core, as the high-level policy, owns the abstraction (port); the adapter, as low-level detail, implements it and depends on the core's interface definition, matching DIP's direction exactly.
The core is like a recipe that calls for 'a sweetener' rather than naming a brand — the recipe (core) doesn't care if you use honey or sugar (adapters), and it's the shopper (composition root) who decides at the grocery store which one actually goes in the pantry before cooking starts.
saying these in an interview costs you the question
- Thinks the core can import a persistence/web-framework annotation as long as it 'only uses' one method
- Believes IoC/DI is only about unit testing convenience, not about compile-time decoupling
- Can't describe what a composition root is or where wiring code should live
- Assumes a layered architecture already gets you the same guarantee without extra tooling/discipline
- Doesn't distinguish 'depends on an abstraction' from 'happens to be an interface with one impl'