skip to content

You're designing a 'TransferFunds' use case between two bank accounts using Hexagonal Architecture. Walk through what the driving port, the application core's responsibilities, and the driven ports would be, and explain why the application core is described as the 'dependency anchor' of the whole design.

level: seniorimportance: must knowfreq 60%

answer

  1. all arrows point INTO the core
  2. driving adapters depend on driving port; driven adapters depend on driven port
  3. core = only thing that knows the business rule
  4. port granularity should match real invariants (e.g. atomic two-write)
  5. core replaceable-around without modification

basics

~20 s

The core holds the transfer rules (enough balance? valid accounts?). A driving port lets something like an API call 'do this transfer.' Driven ports let the core ask for things it needs, like reading and saving account balances, without knowing how those things are actually done.

solid answer

~50 s

The driving port would be something like `TransferFundsUseCase.transfer(command): TransferResult`, called by a REST controller or CLI adapter. The application core implementing it owns all business rules — validating both accounts exist and are active, checking sufficient balance, computing new balances, enforcing invariants like a daily transfer limit — purely against its own domain types. To do its job it depends on driven ports it defines: an `AccountRepository` (load/save account state) and possibly an `AuditLogPort` and `NotificationPort`. The core is the 'dependency anchor' because every arrow in the system points at it — driving adapters depend on the driving port the core exposes, and driven adapters depend on (implement) the driven ports the core defines — so the core is the one thing nothing else drives out of, and it can be understood, changed, and tested without reference to any adapter.

go deeper

for a junior

Should be able to say, roughly, which parts belong in the core (the rules) vs which belong in adapters (HTTP, database) for a simple version of this example.

for a middle

Should correctly identify a driving port and at least one sensible driven port for the scenario, unprompted.

for a senior

Should design port shapes that reflect real business invariants (like transfer atomicity) rather than mirroring CRUD/ORM operations, and justify the design out loud.

for a principal

Should judge when this level of architectural investment is and isn't warranted across a portfolio of services, and set conventions for port granularity that hold up across teams.

## The dependency anchor The "dependency anchor" framing captures a specific structural property: draw every dependency arrow in a hexagonal system, and every single one of them points toward the application core, never away from it. This is true for both families of adapters, and the TransferFunds example makes both directions concrete. - **On the driving side**, a REST controller adapter depends on the `TransferFundsUseCase` port — it imports that interface, builds a `TransferCommand` from the incoming HTTP request body, and calls `transfer(command)`. The core's concrete implementation of that interface lives inside the core module and is the only thing that knows how the business rule actually executes; the controller has zero knowledge of how balances are validated or stored. - **On the driven side**, the relationship is the same directionally even though the runtime call flows the other way: the core defines an `AccountRepository` port with methods like `findById(accountId): Account` and `save(account: Account)`, and a database-backed adapter implementing that interface depends on (imports) the core's port definition to know what contract to fulfill. In both cases, the arrow of source-code dependency points at the core; only the arrow of runtime control flow differs (driving adapters call into the core, the core calls out to driven adapters, but "calls out to" does not mean "depends on" — the core calls a port it owns, not the adapter's concrete class). | Adapter side | Source-code dependency | Runtime control flow | |---|---|---| | Driving (a REST controller) | imports the `TransferFundsUseCase` port | calls into the core | | Driven (a database-backed adapter) | imports the core's `AccountRepository` port definition | the core calls out to it | ## Inside the core Inside the core, the actual work for TransferFunds is pure business logic expressed against domain types with no infrastructure awareness: 1. load account A and account B via `AccountRepository`; 2. verify both are found and active; 3. verify account A's balance is greater than or equal to the transfer amount (and any other invariant, e.g. a daily limit check against a value object like `Money`); 4. compute new balances; 5. and — critically for a financial operation — decide how the two writes are made consistent. In a naive layered design, this consistency decision often gets pushed down implicitly to whatever the persistence framework's transaction manager happens to do; in a well-designed hexagonal core, the invariant that both balances change together or neither does is something the core is explicit about, even though the actual transactional mechanism — a database transaction, a saga, a two-phase commit — is an adapter concern. This is a subtlety worth naming: a poorly drawn `AccountRepository` port that only exposes `save(single account)` can make it hard for the core to express "save these two updates atomically" without leaking transaction management into the core; a well-drawn port might instead expose `saveTransfer(fromAccount, toAccount)` as one coherent, atomically-implied operation, keeping the atomicity requirement expressed at the core level while its mechanism stays in the adapter. ## Two properties that matter operationally Being the dependency anchor gives the core two properties that matter operationally. 1. **First, it can be understood in total isolation.** A reviewer or new hire can read the core module, understand every business rule for money transfers, and never need to open a database migration file or an HTTP routing config to know what the system does. 2. **Second, it can be replaced-around without being touched.** Swapping the REST API for a gRPC API (new driving adapter), or one database vendor for another (new driven adapter), touches zero lines inside the core, because neither adapter was ever a dependency of it. This is the payoff that offsets the indirection tax specifically for domains with real business complexity — a funds-transfer rule set is exactly the kind of logic worth insulating this way, unlike a trivial CRUD "update user's display name" endpoint where the anchor pattern's ceremony rarely earns its keep. ## The failure mode specific to this design The failure mode specific to this design task is drawing the driven port at the wrong granularity relative to the actual business invariant, as illustrated above with the two-write atomicity problem. Get the port shape wrong and either: - the core silently trusts an adapter to do something it never asked for, a landmine for future adapter authors who don't know the invariant exists; or - the core is forced to leak a transactional concept like "begin transaction" across the port boundary to compensate, which erodes the very isolation the anchor pattern exists to provide. A real-world parallel: banking core-systems built around a ledger-service core commonly express a transfer as a single `postDoubleEntry(debit, credit)` operation at the port level specifically so the atomicity invariant is a first-class part of the contract, rather than something callers must remember to arrange themselves.

  • Why is it a mistake to give `AccountRepository` only a `save(Account)` method for a two-account transfer, and not something transfer-specific?
    With only single-account save, the core (or worse, the adapter) has to decide separately how to make two independent saves atomic, which either forces transaction-management logic to leak into the core or leaves the atomicity invariant unenforced and implicit. A port shaped around the actual business operation, like saveTransfer(from, to), keeps the atomicity requirement visible in the contract itself.
  • Does the application core know that a REST controller is calling it, versus a CLI or a scheduled job?
    No — and that's the point. The core only sees a call against TransferFundsUseCase's method with a TransferCommand value; it has no way to know or care what triggered that call, which is exactly what lets the same core logic be reused across multiple driving adapters unchanged.
  • What would make TransferFunds a poor candidate for this level of architectural investment?
    If it's a one-off internal admin tool with a single caller, no plans for a second UI or persistence technology, and low business-rule complexity, the port/adapter ceremony adds files and indirection without ever paying off — a simpler direct-call layered design would ship faster and be easier to navigate for that specific, narrow case.

The core is like the hub of a bicycle wheel — every spoke (adapter) attaches to the hub, and the hub doesn't know or care which specific spokes are attached, but every spoke's position and job is defined relative to the hub, not the other way around.

saying these in an interview costs you the question

  • Thinks the core can call the controller or the concrete repository class directly for 'convenience'
  • Designs a driven port that mirrors CRUD/ORM method names instead of the business operation
  • Doesn't notice that a multi-write invariant (atomicity) needs to be reflected in the port's shape
  • Believes the core needs to know which specific adapter (REST vs CLI) triggered a call
  • Applies full hexagonal ceremony to a trivial single-caller CRUD operation without justification

context