Hexagonal architecture

The business core owns its interfaces and depends on nothing outside it.

Assembling the machine…

Step 1 / 8

The application starts

Before any request, the configurator creates each adapter and plugs it into its port. Postgres answers OrderRepository: decided here, once, not in the core.

Event log · 3
  1. 0.00 msCONFIGPlaceOrder ← HttpAdapter
  2. 0.00 msCONFIGOrderRepository ← PostgresAdapter
  3. 0.00 msCONFIGPaymentGateway ← PaymentProviderAdapter

The concept

The business core owns its interfaces and depends on nothing outside it.

Hexagonal architecture, also called ports and adapters, structures an application so that its business core depends on nothing outside it: user interfaces, databases and test suites all plug into ports the core declares. Alistair Cockburn described it in 2005, with one intent: an application should run the same whether a person, another program, a test suite or a batch script drives it, and it should be built and tested away from the databases and devices it will use in production.

The application declares ports, interfaces written in its own vocabulary. Driving ports, like PlaceOrder, are what the outside world can ask of it. Driven ports, like OrderRepository and PaymentGateway, are what it needs from the outside world.

Adapters translate between a technology and a port. The HTTP adapter turns a request into a PlaceOrder call; the Postgres adapter implements OrderRepository with SQL. Source code dependencies point inward: adapters know the core, the core only knows its ports.

That is why the swap works: the in-memory adapter implements the same port, so not a line of the core changes, and the choice is one line in the configurator, at startup. It is also why the counter-example hurts: once the domain imports the Postgres adapter, changing storage means editing business code.

The six sides mean nothing. Cockburn drew a hexagon to leave room for several ports and adapters; an application has as many ports as it has conversations with the outside world.

The run, step by step

Every scenario, in words.

What the machine plays, step by step. Under a step, what plays in its place once a what-if switch is flipped.

Place an order

8 steps
  1. The application starts

    Before any request, the configurator creates each adapter and plugs it into its port. Postgres answers OrderRepository: decided here, once, not in the core.

    • What if · Swap Postgres for InMemory

      The application starts

      This time the configurator plugs InMemory into OrderRepository. That one line at startup is the whole swap: the core is untouched.

    • What if · Drive it from the test suite

      The test suite sets the application up

      Before the test runs, its setup creates the adapters it needs and plugs them in, the way the configurator does in production.

    • What if · Swap Postgres for InMemory + Drive it from the test suite

      The test suite sets the application up

      The test's setup plugs InMemory into OrderRepository: no database to run the test. The core cannot tell.

  2. A request arrives

    POST /orders reaches the HTTP adapter. It turns the request into a PlaceOrder command, nothing more.

    • What if · Drive it from the test suite

      A test calls the application

      A test case builds an order and calls PlaceOrder through the test adapter. No HTTP, no server, no network.

  3. Through the input port

    The adapter calls PlaceOrder, an interface the core owns. The adapter depends on the core, not the other way round.

    • What if · Drive it from the test suite

      Through the same input port

      The test adapter calls PlaceOrder, the very port HTTP uses. Nothing in the core knows a test is calling.

  4. The core applies the rules

    An order needs at least one line and a positive total. Pure business logic: no SQL, no HTTP, no framework.

  5. Out through OrderRepository

    The core saves the order through a port it declares. It does not know which storage will answer.

    • What if · Import Postgres in the core

      The core reaches for Postgres

      The domain imports the Postgres adapter and calls it directly. The dependency now points outward: the core knows SQL.

  6. Postgres answers

    The Postgres adapter implements OrderRepository and runs the INSERT. The core never wrote a line of SQL.

    • What if · Swap Postgres for InMemory

      InMemory answers

      Same port, different adapter: a Map answers in a hundredth of a millisecond. Not a single domain file changed.

    • What if · Import Postgres in the core

      It still works, for now

      The order is saved. Nothing is broken at runtime: the cost shows up the day storage has to change.

    • What if · Swap Postgres for InMemory + Import Postgres in the core

      The swap breaks the core

      InMemory is plugged into OrderRepository, but the core still calls Postgres, now on the bench. Changing storage means editing the domain.

  7. Payment through PaymentGateway

    The core charges the customer through another port. The payment provider is only one adapter behind it.

  8. The response goes back

    201 Created. The HTTP adapter turns the result into a response. The core never saw HTTP.

    • What if · Drive it from the test suite

      The result goes back to the test

      The test adapter hands the result to the test, which checks it. The core returned exactly what HTTP would turn into a response.

Reject an empty order

4 steps
  1. An empty order arrives

    POST /orders with no lines reaches the HTTP adapter. It turns it into a PlaceOrder command, as for any order.

    • What if · Drive it from the test suite

      A test sends an empty order

      A test case calls PlaceOrder with an order that has no lines, to check that the core refuses it.

  2. Through the input port

    The adapter calls PlaceOrder like any other time. Checking the order is not its job.

    • What if · Drive it from the test suite

      Through the same input port

      The test adapter calls PlaceOrder, the port HTTP uses. Checking the order is not its job either.

  3. The core refuses the order

    An order needs at least one line. The rule lives in the core, so it holds whoever calls, and no storage or payment is ever reached.

  4. The refusal goes back

    The HTTP adapter turns the core's refusal into 422 Unprocessable. Postgres and the payment provider were never called.

    • What if · Drive it from the test suite

      The test sees the refusal

      The test adapter hands the refusal back and the test checks it. No database was needed to test a business rule.

Decline a payment

7 steps
  1. An order arrives

    POST /orders reaches the HTTP adapter, which calls PlaceOrder. This customer's card will be refused.

    • What if · Drive it from the test suite

      A test places an order

      A test calls PlaceOrder through the test adapter, for a customer whose card will be refused.

  2. Out through OrderRepository

    The core saves the order through a port it declares. It does not know which storage will answer.

    • What if · Import Postgres in the core

      The core reaches for Postgres

      The domain imports the Postgres adapter and calls it directly. The dependency now points outward: the core knows SQL.

  3. Postgres answers

    The Postgres adapter implements OrderRepository and runs the INSERT. The core never wrote a line of SQL.

    • What if · Swap Postgres for InMemory

      InMemory answers

      Same port, different adapter: a Map answers in a hundredth of a millisecond. Not a single domain file changed.

    • What if · Import Postgres in the core

      It still works, for now

      The order is saved. Nothing is broken at runtime: the cost shows up the day storage has to change.

    • What if · Swap Postgres for InMemory + Import Postgres in the core

      The swap breaks the core

      InMemory is plugged into OrderRepository, but the core still calls Postgres, now on the bench. Changing storage means editing the domain.

  4. The core asks for the payment

    The core charges the customer through PaymentGateway, the port it declares. The payment provider adapter makes the call.

  5. The adapter translates the refusal

    The provider answers in its own terms: HTTP 402, reason insufficient_funds. The adapter turns that into PaymentDeclined, a result the port defines.

  6. The core decides

    What a refused payment means is a business rule: the order is not confirmed. The core decides, in its own words, without the provider's codes.

  7. 402 goes back

    The HTTP adapter turns the core's decision into 402 Payment Required. Neither the core nor HTTP ever saw the provider's codes.

    • What if · Drive it from the test suite

      The test sees the decision

      The test adapter hands the result back and the test checks it: the order is not confirmed.