Hexagonal Architecture Diagram: Ports and Adapters with an Example

Hexagonal architecture explained with a diagram: the core, inbound and outbound ports, driving and driven adapters, the dependency rule and a worked order example.

· Founder & developer

Published

Hexagonal Architecture Diagram: Ports and Adapters with an Example

Most codebases start with the framework at the center: controllers call services, services call the ORM, and business rules leak into all three. Testing a pricing rule then needs a web server and a database, and replacing the payment provider means touching the code that decides what an order costs.

Hexagonal architecture, also called ports and adapters, turns that around. This post explains the idea, the vocabulary (ports, driving and driven adapters, the dependency rule), shows a diagram for an order service, and covers when the pattern is worth its extra files.

The idea in one sentence

Put the business logic in a core that knows nothing about HTTP, databases or vendors, and connect everything else to it through interfaces the core owns.

Alistair Cockburn described the pattern in his original hexagonal architecture article in 2005. Its stated intent is to let an application be driven equally by users, programs, automated tests or batch scripts, and to be developed and tested in isolation from its eventual databases and devices. The hexagon shape has no meaning beyond leaving room to draw several ports on its sides.

The vocabulary

Term What it is Order service example
Core Use cases and the domain model PlaceOrder, Order, Money, pricing rules
Inbound (driving) port An interface the core offers to the outside PlaceOrder.execute(command)
Outbound (driven) port An interface the core needs from the outside OrderRepository, PaymentGateway
Driving adapter Turns outside input into port calls REST controller, CLI, message consumer, test
Driven adapter Implements an outbound port with real technology Postgres repository, payment API client

The dependency rule ties it together: source code dependencies point inward. Adapters depend on ports; ports and the core depend on nothing outside the hexagon. At runtime the core calls the payment gateway, but in the code the payment adapter depends on the core's interface, not the other way round.

A worked example

Placing an order needs to save the order and charge the customer. In the core:

// ports, owned by the core
interface OrderRepository { save(order: Order): Promise<void> }
interface PaymentGateway { authorize(amount: Money, token: string): Promise<AuthResult> }

// use case
class PlaceOrder {
  constructor(private orders: OrderRepository, private payments: PaymentGateway) {}
  async execute(cmd: PlaceOrderCommand): Promise<OrderId> {
    const order = Order.create(cmd.items, cmd.customerId)   // domain rules
    const auth = await this.payments.authorize(order.total(), cmd.paymentToken)
    if (!auth.ok) throw new PaymentDeclined(auth.reason)
    await this.orders.save(order.markPaid(auth.id))
    return order.id
  }
}

Outside the core, PostgresOrderRepository implements OrderRepository with SQL, and PaymentApiGateway implements PaymentGateway with HTTP calls, retries and error mapping. A REST controller parses the request, builds a PlaceOrderCommand and calls the use case. A test does the same with in-memory adapters, no web server or database required.

Mapping it to folders

The structure is easier to keep when the folders say it out loud:

src/
  core/
    domain/        Order, Money, pricing rules
    usecases/      PlaceOrder, CancelOrder
    ports/         OrderRepository, PaymentGateway
  adapters/
    http/          REST controllers (driving)
    postgres/      repository implementations (driven)
    payments/      payment API client (driven)
  main.ts          wiring: builds adapters and passes them to use cases

Add a lint rule or an architecture test that fails the build when anything under core/ imports from adapters/ or from a framework package. The rule is easy to break by accident and cheap to enforce automatically.

Common mistakes

  • Ports shaped like the technology. OrderRepository.executeQuery(sql) is a database API with a new name. Name ports after what the core needs: save, findOpenOrdersFor(customer).
  • Domain objects that are ORM entities. Annotations from the persistence framework drag the database into the core. Map between domain objects and persistence models in the adapter.
  • Adapters that hold business rules. A payment adapter that decides refunds over 500 dollars need approval has moved a rule out of the core. Keep adapters to translation and transport.
  • One port per class. Ports exist for boundaries, not for every collaborator inside the core.

The architecture diagram

A hexagon for the application core holds the PlaceOrder use case, the domain model and three ports. A REST controller and a test driver call the inbound port. The use case calls OrderRepository and PaymentGateway ports, implemented by a Postgres adapter and a payment API adapter.

14 components · 10 connections

Open in Archboard
Diagram contents
  • PlaceOrder use case
  • Domain model
  • PlaceOrder port
  • OrderRepository port
  • PaymentGateway port
  • REST controller
  • Test driver
  • Postgres adapter
  • Payment API adapter
  • Postgres
  • Payment provider
  • calls
  • implements
  • SQL
  • HTTPS
An order service in hexagonal architecture. The core holds the PlaceOrder use case and the domain model; it calls outbound ports that adapters implement for Postgres and a payment provider, and is driven through an inbound port by a REST controller or a test driver.

How to read it:

  • The hexagon is the application core: the PlaceOrder use case and the domain model, with no framework or vendor code.
  • Left, driving side: a REST controller and a test driver both call the inbound PlaceOrder port. They are interchangeable from the core's point of view.
  • Right, driven side: the use case calls two outbound ports. The dashed edges with hollow arrowheads show the Postgres adapter and the payment API adapter implementing them, which is the direction of the code dependency.
  • Far right: the real Postgres database and payment provider, which only the adapters know about.
  • The Markdown block states the dependency rule, the one thing reviewers should check.

Testing gets cheaper

Most of the benefit shows up in tests:

  • Core tests use in-memory adapters and run in milliseconds. Pricing rules, state transitions and error handling are tested without I/O.
  • Adapter tests check one adapter against the real technology: the Postgres repository against a real Postgres, the payment adapter against the provider's sandbox.
  • A few end-to-end tests prove the wiring.

That split is the test pyramid made concrete, and it is why teams adopt the pattern even in small services.

Adding a new way in

The inbound port pays off the first time a second channel arrives. Say the business wants orders from a partner's message queue as well as the web. You write a consumer that reads a message, builds the same PlaceOrderCommand and calls the same port. Validation, pricing and payment logic do not change, and neither do their tests. Without the port, the usual result is a second copy of the order logic inside the consumer, which drifts away from the first within a few releases.

When it is not worth it

  • CRUD with no rules. If the service mostly moves data between HTTP and a table, the ports are ceremony.
  • Short-lived code. Prototypes and scripts do not repay the extra interfaces.
  • One adapter forever. If you will never swap the database and you test against a real one anyway, an outbound port adds indirection with little return. Keep the core free of framework code even then.

Hexagonal architecture is close kin to onion and clean architecture. They differ in layering vocabulary, but share the dependency rule.

What to put on the diagram

  • The core boundary, drawn as a shape that clearly encloses only domain and use cases.
  • Ports on the boundary, named as the core names them.
  • Adapters outside, labeled driving or driven, with the technology they wrap.
  • Dependency direction for driven adapters, different from the call direction.

For a whole system rather than one service, the C4 model diagram example shows how to zoom from containers to components, and the microservices architecture diagram shows where services like this one fit. The payment gateway integration diagram covers what happens behind the payment adapter.

Build it in Archboard

  1. Sign in with Google or GitHub and create a diagram.
  2. Add a Shape from the Core pack and set it to a hexagon for the application core, with a Text label.
  3. Inside it, add Component nodes from the C4 Model pack for the use case and domain model, and Component nodes on the boundary for the three ports.
  4. Add Container nodes outside for the driving and driven adapters, plus Postgres from the Data pack and an External System for the payment provider.
  5. Draw implementation edges dashed with hollow arrowheads, and add a Markdown block from the Core pack with the dependency rule.

The example uses 13 diagram nodes, within the Free plan.

FAQ

What is the difference between a port and an adapter? A port is an interface owned by the core. An adapter is the code that connects that interface to a specific technology, such as a REST framework or a database.

What are driving and driven adapters? Driving adapters start interactions with the core, such as controllers, consumers and tests. Driven adapters are called by the core, such as repositories and API clients.

Is hexagonal architecture the same as clean architecture? They share the dependency rule: dependencies point toward the domain. Clean and onion architecture add named layers inside the core; hexagonal architecture only distinguishes the inside from the outside.

Does every service need hexagonal architecture? No. It pays off where there are real business rules and more than one way to drive or store them. For thin CRUD services, it is mostly extra files.

Where do transactions go? Usually in the use case or a unit-of-work port, so the core decides what must commit together while the adapter decides how.

Start free — one seat, no credit card.

Related posts

← Back to blog

Open a canvas in a minute

Sign in with Google or GitHub. Free for a single seat. Upgrade when the team joins.