Courses 0%
20
Architectural Patterns · Chapter 20 of 51

Hexagonal Archiecture

Akhil
Akhil Sharma
20 min

Hexagonal Architecture

The architecture that lets you swap databases, frameworks, and APIs without touching business logic — by making your core domain completely independent of infrastructure.

Hexagonal Architecture (Ports & Adapters) for Distributed Systems — An Interactive Guide

Audience: senior engineers building services in distributed environments (microservices, event-driven systems, multi-tenant SaaS, hybrid cloud). Goal: make Hexagonal Architecture practical under network failures, evolving dependencies, and production constraints.


:dart: Challenge: Your service is “simple”… until it isn’t

You run Order Service for a delivery platform.

Today it:

  • Accepts HTTP requests from a web app
  • Calls Payment Provider A
  • Writes to Postgres
  • Publishes OrderPlaced events to Kafka

Tomorrow, product asks for:

  • A second payment provider (regional)
  • A CLI tool for batch imports
  • A background worker that retries failed payments
  • A gRPC API for mobile
  • A new event bus for one region

And ops adds:

  • Circuit breakers for flaky providers
  • Chaos testing
  • Multi-region failover

Pause & think

If your domain logic is currently tangled with HTTP handlers, ORM entities, Kafka producers, and payment SDK calls…

What changes will be hardest?

  1. Adding another API protocol (HTTP -> gRPC)
  2. Swapping payment provider
  3. Changing DB schema
  4. Adding retries and idempotency

Don’t answer yet—hold your intuition.

The core problem

In distributed systems, dependencies change and fail. If your business rules are coupled to delivery mechanisms (HTTP, messaging) and infrastructure (DB, external providers), every change becomes a risky refactor.

Hexagonal Architecture (a.k.a. Ports and Adapters) is a disciplined way to:

  • Keep the domain model insulated from I/O and frameworks
  • Make dependencies replaceable
  • Make failure handling explicit
  • Enable multiple inputs/outputs without rewriting business logic

Key insight: In a distributed system, network calls are not “implementation details”. They are sources of latency, partial failure, retries, and inconsistency—Hexagonal Architecture helps you surface and manage that complexity.

Key insight: Hexagonal Architecture isn’t about “pretty folders.” It’s about dependency direction: the domain depends on abstractions (ports), while infrastructure depends on the domain.


:handshake: Section: The restaurant analogy (the fastest mental model)

Scenario

Imagine your service is a restaurant.

  • The kitchen = domain logic (rules, invariants)
  • Waiters taking orders = inbound adapters (HTTP controllers, gRPC handlers, consumers)
  • Suppliers delivering ingredients = outbound adapters (DB, payment gateway, email, Kafka)

The kitchen shouldn’t care who brought the order (waiter, phone call, app). It also shouldn’t care which supplier delivered tomatoes—only that tomatoes arrive with a certain contract.

Interactive question (pause & think)

If the restaurant switches from phone orders to app orders, should the chef rewrite recipes?

  • A) Yes, the chef must learn the app’s API
  • B) No, the chef only needs the order ticket format

Answer (progressive reveal):

Reveal **B.** The chef shouldn’t know the app’s API. That’s the waiter’s job.

Real-world parallel

In services:

  • Domain logic shouldn’t know HTTP headers, JSON shapes, Kafka partitioning, or ORM sessions.
  • It should operate on domain concepts: Order, PaymentAuthorization, InventoryReservation.

Key insight: Ports are the “order ticket format” and “supplier purchase order format.” Adapters translate between the outside world and those formats.


:rotating_light: Section: Common Misconception — “Hexagonal means no frameworks”

Misconception

“If we use Hexagonal Architecture, we can’t use Spring, Django, Rails, or ORM libraries.”

Reality

You can use any framework. Hexagonal says:

  • Framework code should live in adapters
  • The domain should be framework-agnostic

Why it matters in distributed systems

Frameworks are great until:

  • You need to run the same domain logic in a worker and an API
  • You need to test failure behavior without starting Kafka/Postgres
  • You need to swap a dependency (payment provider, event bus)

Hexagonal doesn’t ban frameworks—it contains them.

Key insight: Hexagonal Architecture is a boundary management strategy, not a framework boycott.


:mag: Section: What “Ports” and “Adapters” actually are (with distributed systems twist)

Scenario

Your Order Service has:

  • inbound: HTTP POST /orders, Kafka CartCheckedOut events
  • outbound: Postgres, Payment API, Kafka OrderPlaced events

Ports

A port is an interface (or abstract contract) that describes:

  • What the domain needs from the outside (outbound port)
  • What the outside can ask the domain to do (inbound port)

In distributed systems, ports are where you encode:

  • Idempotency expectations
  • Timeouts and cancellation semantics
  • Error taxonomy (retryable vs non-retryable)
  • Consistency requirements

Adapters

An adapter implements a port by translating:

  • protocol <-> domain model
  • SDK <-> domain model
  • DB rows <-> aggregates

Adapters are where you handle:

  • serialization/deserialization
  • retries, circuit breakers
  • authentication
  • schema evolution
  • backpressure

Interactive: classify these

Match each item to Port or Adapter.

  1. PaymentGateway.authorize(paymentRequest)
  2. StripePaymentAdapter using Stripe SDK
  3. HTTP controller parsing JSON into PlaceOrderCommand
  4. PlaceOrderUseCase interface

Pause & think.

Reveal 1) **Port** (outbound) 2) **Adapter** (outbound) 3) **Adapter** (inbound) 4) **Port** (inbound)

Key insight: Ports are stable contracts. Adapters are replaceable translators.


:jigsaw: Section: The hexagon diagram (and what it hides)

Hexagonal Architecture is often drawn as a hexagon with:

  • domain in the center
  • ports on the edges
  • adapters outside

[IMAGE: A hexagon diagram labeled “Domain” in center; left side inbound adapters (HTTP, gRPC, Kafka consumer, CLI); right side outbound adapters (Postgres repository, Kafka producer, Payment provider, Email). Arrows show dependencies pointing inward from adapters to ports and domain. Include “dependency inversion” note.]

Pause & think

In real distributed systems, what’s missing from the simple hexagon picture?

  • A) Observability (logs/metrics/traces)
  • B) Failure modes (timeouts, retries)
  • C) Data consistency patterns (outbox, sagas)
  • D) All of the above
Reveal **D.** The basic diagram is conceptual; production systems require explicit patterns for these concerns.

The distributed-systems upgrade

For distributed systems, treat “adapters” as policy-bearing components:

  • outbound adapters enforce timeouts, retries, circuit breakers
  • inbound adapters enforce idempotency keys, rate limits, auth

Key insight: In distributed systems, adapters are not “dumb glue.” They are where you implement resilience policy.


:video_game: Section: Decision game — Which dependency direction is correct?

Scenario

You have a domain use case PlaceOrder that needs to:

  • persist an order
  • call payment provider
  • publish an event

Which structure is hexagonal?

Option 1

  • PlaceOrderService imports StripeSDK, KafkaProducer, OrderJpaRepository

Option 2

  • PlaceOrderService depends on PaymentPort, OrderRepositoryPort, EventPublisherPort
  • Adapters implement those ports using Stripe/Kafka/JPA

Pause & think.

Reveal **Option 2** is hexagonal. The domain depends on ports (abstractions), not concrete infrastructure.

Why Option 1 fails harder in distributed systems

Because network and infrastructure concerns leak into business logic:

  • retry loops inside domain methods
  • HTTP status mapping inside domain
  • Kafka partition keys computed deep in aggregates

Key insight: If your domain imports an SDK for something that can fail over the network, you’ve coupled your business rules to failure mechanics.


:potable_water: Section: Inbound side — Ports for use cases (not controllers)

Scenario

Your current code:

  • Controller validates JSON
  • Controller calls repository
  • Controller calls payment
  • Controller returns response

That’s a “transaction script” living at the edge.

The hexagonal move

Define an inbound port per use case (application service boundary):

  • PlaceOrder
  • CancelOrder
  • GetOrder

The controller becomes an adapter that:

  • maps HTTP request -> command
  • calls the port
  • maps result -> HTTP response

Think about it

If you add a Kafka consumer that triggers PlaceOrder from CartCheckedOut, what changes?

  • A) You rewrite business logic for Kafka
  • B) You add another inbound adapter that calls the same port
Reveal **B.** Same use case, different adapter.

[CODE: JavaScript, demonstrate an inbound port interface PlaceOrderUseCase, a command DTO, and two adapters: HTTP controller and Kafka consumer calling the same port.]

javascript

Key insight: Inbound adapters multiply; inbound ports stay stable.


:mag: Section: Outbound side — Ports for dependencies (DB, messaging, providers)

Scenario

Your domain needs:

  • store/retrieve orders
  • authorize/charge payment
  • publish events

In hexagonal architecture, the domain defines what it needs:

  • OrderRepositoryPort
  • PaymentPort
  • EventPublisherPort

Adapters implement those ports:

  • Postgres adapter (SQL/ORM)
  • Stripe adapter (HTTP SDK)
  • Kafka adapter (producer)

Pause & think: where do retries belong?

Retries for payment authorization should live in:

  • A) Domain use case
  • B) Payment adapter
  • C) HTTP controller
Reveal **B** most often. Retries are a policy for interacting with an unreliable dependency.

But: the domain may still need to decide whether to retry (business policy) vs adapter deciding how (technical policy). We’ll split that later.

Key insight: Distributed systems force you to distinguish business policy (should we retry?) from resilience mechanism (how do we retry?).


:jigsaw: Section: The “two-layer” mental model — Domain vs Application vs Adapters

Mental model

Hexagonal is easiest when you separate:

  1. Domain layer (pure):

    • entities/aggregates
    • value objects
    • domain services
    • invariants
  2. Application layer (use cases):

    • orchestrates domain objects
    • manages transactions
    • calls outbound ports
    • defines inbound ports (use case interfaces)
  3. Adapters/Infrastructure:

    • HTTP/gRPC endpoints
    • Kafka consumers/producers
    • DB repositories
    • external clients

Restaurant mapping

  • Domain: recipes and food safety rules
  • Application: expediter coordinating dishes, timing, plating
  • Adapters: waiters, delivery apps, suppliers

Interactive exercise: spot the leak

Which is a sign your domain layer is leaking infrastructure?

  1. Domain entity has @JsonProperty annotations
  2. Use case takes HttpServletRequest
  3. Repository adapter imports domain types
  4. Domain throws PaymentGatewayTimeoutException

Pause & think.

Reveal 1) leak (serialization concerns) 2) leak (transport concerns) 3) fine (adapters depend inward) 4) likely leak (infrastructure failure types leaking into domain)

Key insight: Domain should speak in domain terms. Failure types should be mapped at the boundary.


:dart: Section: Distributed systems reality — failures are part of the contract

Scenario

Payment provider sometimes:

  • times out
  • returns 500
  • returns 409 duplicate charge
  • succeeds but your network drops before you receive response

Pause & think

Which statement is true?

A) “Hexagonal architecture eliminates failures by isolating them.” B) “Hexagonal architecture makes failures explicit by isolating where they are handled.”

Reveal **B.** It doesn’t remove failures; it makes your codebase honest about them.

Failure taxonomy as port design

Design ports so failure modes are explicit and testable:

  • return Result<T, Error>
  • or throw typed domain-level errors
  • include idempotency keys
  • include timeout/cancellation semantics

[CODE: JavaScript, demonstrate a PaymentPort.authorize() returning a sealed result with retryable/non-retryable errors, plus idempotency key.]

javascript

Key insight: In distributed systems, a port is not just “a method signature.” It’s a failure contract.


:handshake: Section: Timeouts, cancellation, and backpressure (ports must carry them)

Scenario

Your PlaceOrder use case calls Payment and Inventory. Under load, requests pile up.

If your port methods don’t accept context/deadlines, your adapters may:

  • block threads
  • keep retrying after the client gave up
  • overload downstream services

Pause & think

Where should deadlines live?

  • A) Only in HTTP layer
  • B) Only in adapters
  • C) Propagated from inbound adapter -> use case -> outbound adapters
Reveal **C.** Deadline propagation is end-to-end.

Practical patterns

  • Pass a Context / CancellationToken / CoroutineContext
  • Include timeoutMs in command
  • Use structured concurrency where possible

[CODE: Python, show deadline propagation from inbound adapter to use case to outbound port using sockets with timeouts.]

python

Key insight: Hexagonal boundaries help you propagate distributed-systems concerns without coupling to specific transports.


:rotating_light: Section: Common Misconception — “Ports are just interfaces, so we’re done”

Misconception

“We created interfaces for repositories. That’s hexagonal.”

Reality

Interfaces alone don’t buy much unless:

  • dependency direction is correct
  • adapters are outside
  • ports represent use cases and dependency contracts
  • failure semantics are modeled

Distributed systems angle

If your PaymentPort exposes Stripe-specific fields (like paymentIntentId) everywhere, you’ve created a “Stripe-shaped domain.”

Key insight: Ports should be domain-shaped, not vendor-shaped.


:mag: Section: Event-driven systems — inbound adapters aren’t just HTTP

Scenario

You consume CartCheckedOut events and create orders.

Where does deserialization and schema evolution live?

  • In the consumer adapter.

Where does deduplication/idempotency live?

  • Split: adapter enforces delivery semantics; domain enforces business semantics.

Pause & think

Kafka delivers at-least-once. Your consumer may see duplicates.

Which statement is true?

A) “Hexagonal architecture guarantees exactly-once processing.” B) “Hexagonal architecture lets you implement idempotency in one place and reuse it across transports.”

Reveal **B.** The architecture helps structure the solution; it doesn’t change Kafka semantics.

Pattern: Idempotency as an outbound port

A robust approach:

  • Use case receives an idempotency key (eventId or requestId)
  • Use case uses an IdempotencyStorePort (backed by DB/Redis)
  • If already processed, return prior result

[CODE: Python, show an idempotent PlaceOrder use case using an IdempotencyStorePort and OrderRepositoryPort.]

python

Key insight: In event-driven systems, idempotency is a first-class dependency—model it as a port.


:jigsaw: Section: Outbox pattern — where it fits in hexagonal

Scenario

You need to:

  • save order in DB
  • publish OrderPlaced event

If you do DB then Kafka directly, you risk:

  • DB commit succeeds, Kafka publish fails -> missing event
  • Kafka publish succeeds, DB commit fails -> phantom event

Pause & think

Which is the safer approach?

A) “Try/catch and retry Kafka publish” B) “Transactional outbox: write event to DB in same transaction, publish asynchronously”

Reveal **B.** Outbox provides atomicity with the database.

Hexagonal placement

  • Domain/use case calls EventOutboxPort.append(event)
  • DB adapter writes to outbox table transactionally
  • Separate publisher adapter reads outbox and publishes to Kafka

[IMAGE: Sequence diagram: PlaceOrderUseCase -> DB transaction writes Order + Outbox row; OutboxPublisher polls table -> Kafka publish; marks outbox row sent. Show failure points and retries.]

[CODE: Python, show outbox publisher loop with idempotent publish using event id (socket-based publisher stub).]

python

Key insight: Hexagonal architecture doesn’t replace distributed-systems patterns like Outbox—it gives them a clean home.


:video_game: Section: Matching exercise — pick the right port for the job

Match the distributed-systems concern to the port/adapter location.

ConcernBest home
JSON schema validationInbound adapter
Retry with exponential backoff for Payment APIOutbound adapter
Business rule: “don’t retry payment after 24h”Use case / domain policy
Event deduplication based on eventIdUse case + idempotency port
Mapping domain error to HTTP 409Inbound adapter
Circuit breaker stateOutbound adapter

Pause & think—then check.

Reveal All rows as shown are correct.

Key insight: Put protocol translation at the edges, business policy in the center, and resilience mechanics near outbound calls.


:potable_water: Section: Trade-offs — what you pay for hexagonal

Scenario

Your team is under pressure to ship. Someone says:

“This is too much ceremony. We can just put everything in controllers.”

The real trade-offs

Hexagonal Architecture increases:

  • number of types (ports, adapters, DTOs)
  • indirection
  • upfront design effort

But decreases:

  • coupling
  • change cost
  • integration-test dependence
  • blast radius of dependency swaps

Comparison table

DimensionLayered MVC (typical)Hexagonal (ports/adapters)
Multiple inbound protocolsOften duplicated logicReuse use cases via multiple adapters
Swapping DB/providerHigh refactor riskAdapter replacement (if port is stable)
Testing business logicOften needs DB/frameworkPure unit tests around ports
Failure modelingOften ad hocExplicit at port boundaries
Cognitive loadLower initiallyLower over time (if disciplined)

Pause & think

When is hexagonal not worth it?

  • A) One-off scripts
  • B) Short-lived prototypes
  • C) Tiny services with no external dependencies
  • D) All of the above
Reveal **D.** But beware: distributed systems tend to grow dependencies quickly.

Key insight: Hexagonal Architecture is an investment. It pays off when dependencies and change rates are high.


:mag: Section: Real-world usage patterns (microservices, modular monoliths, edge services)

Pattern 1: Microservice with multiple adapters

  • Inbound: HTTP + Kafka consumer
  • Outbound: Postgres + Kafka + Payment provider

Hexagonal helps you:

  • keep one domain model
  • reuse use cases
  • test without spinning infra

Pattern 2: Modular monolith

Each module is a “hexagon” with:

  • internal ports
  • adapters between modules

Distributed-systems twist:

  • modules may later become services; ports become service contracts.

Pattern 3: Edge service / BFF

Mostly adapters:

  • orchestrates calls to downstream services
  • little domain logic

Hexagonal still helps:

  • isolate protocol translation
  • standardize resilience and observability

Key insight: Hexagonal is not tied to microservices. It’s a modularity technique that scales from monolith to distributed.


:jigsaw: Section: Observability as an adapter concern (but with domain-friendly hooks)

Scenario

You need traces across:

  • inbound HTTP
  • use case execution
  • outbound payment call
  • DB write

Pause & think

Where should you create spans and record metrics?

  • A) Only in domain
  • B) Only in adapters
  • C) Adapters create/propagate context; domain emits semantic events
Reveal **C.** Domain shouldn’t import OpenTelemetry, but it can expose meaningful events.

Practical approach

  • Inbound adapter starts trace/span and passes context
  • Outbound adapters create child spans for network calls
  • Domain/use case logs semantic events via a DomainEventsPort or returns structured outcomes

[CODE: Python, show an adapter wrapping a call to PlaceOrderUseCase and outbound adapters creating spans (minimal tracer shim).]

python

Key insight: Keep telemetry libraries at the edge; keep semantics in the center.


:rotating_light: Section: Common Misconception — “Domain events = Kafka events”

Misconception

“If the domain emits OrderPlaced, that must be the Kafka message.”

Reality

  • Domain events are internal facts for your model.
  • Integration events are messages for other services.

They can align, but they often differ:

  • integration events need versioning, backward compatibility, PII filtering
  • domain events may be richer and not stable externally

Hexagonal placement

  • Domain raises domain events
  • Application layer decides which integration events to publish
  • Outbound adapter publishes to Kafka

Key insight: Don’t let external contracts dictate your internal model.


:dart: Section: Designing ports for evolution (versioning, compatibility, migrations)

Scenario

Payment Provider A returns riskScore. Provider B doesn’t.

If your port requires riskScore, you can’t adopt Provider B.

Pause & think

What’s better?

A) Put every provider field into the port contract B) Keep port minimal; expose domain-level concepts; allow optional metadata

Reveal **B.** Ports should be stable and domain-focused.

Techniques

  • Use domain value objects instead of vendor DTOs
  • Add optional metadata: Map<String,String> only when necessary
  • Prefer additive changes
  • Create separate ports for separate use cases (authorization vs capture)

Key insight: Port design is API design—treat it with the same rigor as public service APIs.


:video_game: Section: Quiz — Identify the adapter boundary violations

Code smell scenarios

Which one violates hexagonal boundaries most?

  1. Order entity has toJson()
  2. PaymentAdapter returns StripePaymentIntent
  3. PlaceOrderUseCase returns a domain OrderId
  4. Kafka producer adapter imports OrderPlacedIntegrationEvent

Pause & think.

Reveal **2** is the biggest violation: the outbound adapter is leaking vendor types through the port into the application/domain.

1 is also a violation (serialization in domain), but it’s often easier to fix.

Key insight: Vendor types should not cross port boundaries.


:jigsaw: Section: A full walk-through — Place Order in a flaky world

We’ll build a mental implementation with:

  • inbound HTTP adapter
  • PlaceOrderUseCase (application)
  • domain model with invariants
  • outbound ports: repository, payment, outbox, idempotency
  • adapters: Postgres, Stripe, Kafka outbox publisher

Step 1: Define the inbound port

  • placeOrder(cmd, ctx) -> result

Step 2: Define outbound ports

  • OrderRepositoryPort
  • PaymentPort
  • OutboxPort
  • IdempotencyStorePort

Step 3: Implement the use case

Orchestration responsibilities:

  • validate business rules
  • enforce idempotency
  • call payment with idempotency key
  • store order + outbox event transactionally

Step 4: Implement adapters

  • HTTP adapter maps request/response
  • Payment adapter handles retries/timeouts/circuit breaker
  • Repository adapter handles mapping to tables
  • Outbox publisher handles eventual publish

[CODE: Python, show simplified but complete skeleton: ports, use case implementation, HTTP adapter, payment adapter stub, repository adapter stub, outbox append.]

python

Pause & think: where does the transaction live?

  • A) In the repository adapter
  • B) In the use case
  • C) In an application-layer transaction boundary (unit-of-work) invoked by adapter
Reveal **C** is common: the inbound adapter (or a framework integration adapter) starts a transaction around the use case call.

But some teams keep transaction handling in the use case. The key is: the domain entities should not manage transactions.

Key insight: Transactions are infrastructure. Keep them out of the domain; coordinate them at the application boundary.


:potable_water: Section: Testing strategy — unit tests that simulate distributed failures

Scenario

You want to test:

  • payment timeout
  • duplicate event delivery
  • DB transient failure

Hexagonal makes this easy because ports are mockable.

Pause & think

What should be the default test pyramid for a hexagonal service?

A) Mostly end-to-end tests with real Kafka/Postgres B) Mostly unit tests for use cases + a few contract/integration tests per adapter

Reveal **B.** Use cases can be tested with fake ports; adapters need integration tests.

Suggested test matrix

LayerTest typeWhat to validate
Domainunit/property testsinvariants, state transitions
Use case (application)unit tests with fakesorchestration, error mapping
Adaptersintegration testsDB schema mapping, SDK behavior
Systemend-to-endwiring, configuration, deployment

[CODE: Python, show a unit test where PaymentPort fake times out, verifying use case returns retryable error and writes nothing.]

python

Key insight: Hexagonal architecture turns “distributed failure tests” into deterministic unit tests—when modeled correctly.


Section: Handling consistency and sagas with ports

Scenario

Placing an order requires:

  • reserve inventory (Inventory Service)
  • authorize payment (Payment Provider)
  • confirm order

This is a distributed transaction.

Pause & think

Which is true?

A) Hexagonal architecture solves distributed transactions by design B) Hexagonal architecture helps you implement sagas by keeping orchestration in the application layer and side-effects behind ports

Reveal **B.** You still need patterns like sagas/compensation.

Where saga logic lives

  • Application layer use case orchestrates steps
  • Outbound ports represent steps
  • Adapters implement calls
  • Compensation is domain/application policy

[IMAGE: Saga flow diagram with steps and compensations: ReserveInventory -> AuthorizePayment -> ConfirmOrder; compensation arrows for failures.]

[CODE: JavaScript, show saga orchestration in PlaceOrderUseCase with compensation on failure.]

javascript

Key insight: Ports make saga steps explicit and mockable; adapters keep network chaos out of the core.


:rotating_light: Section: Common Misconception — “Hexagonal = Clean Architecture = DDD = same thing”

Reality

They overlap but differ:

  • Hexagonal: focus on ports/adapters and dependency direction
  • Clean Architecture: similar, emphasizes concentric circles and use cases
  • DDD: modeling approach (bounded contexts, aggregates)

You can use Hexagonal without DDD, and DDD without Hexagonal.

Distributed systems note:

  • DDD helps define service boundaries
  • Hexagonal helps implement each service cleanly

Key insight: Use DDD to decide what services should be; use Hexagonal to decide how each service integrates with the world.


:video_game: Section: “Which statement is true?” — trade-off edition

  1. “Ports should expose CRUD methods for every entity.”
  2. “Ports should model capabilities needed by use cases.”

Pause & think.

Reveal **2**. CRUD ports often leak persistence concerns and encourage anemic domains.
  1. “Adapters can depend on domain types.”
  2. “Domain can depend on adapter types.”
Reveal **3** is true; **4** breaks dependency direction.

Key insight: Ports align to use cases, not tables.


:jigsaw: Section: Practical folder/module layout (language-agnostic)

A common layout:

  • domain/
    • model/ (entities, value objects)
    • policy/ (domain services)
  • application/
    • ports/in/ (use case interfaces)
    • ports/out/ (dependency interfaces)
    • usecases/ (implementations)
  • adapters/
    • in/http/
    • in/kafka/
    • out/postgres/
    • out/kafka/
    • out/payment/
  • bootstrap/ (wiring/DI)

Pause & think

Where does configuration live (URLs, credentials, topic names)?

  • A) Domain
  • B) Application
  • C) Bootstrap/adapters
Reveal **C.** Configuration is infrastructure.

Key insight: If your domain knows topic names, you’ve built a Kafka-shaped domain.


:dart: Final synthesis challenge: Design a hexagon under pressure

Scenario

You are building Notification Service.

Requirements:

  • Inbound: HTTP API POST /notify, plus Kafka UserSignedUp events
  • Outbound: Email provider (sometimes down), SMS provider (rate-limited), Postgres for audit log
  • Must be idempotent (events can be duplicated)
  • Must support “dry-run” mode for testing campaigns
  • Needs observability and dead-lettering for poison messages

Your task (pause & think)

  1. List 3 inbound ports (use cases).
  2. List 4 outbound ports (dependencies).
  3. Decide which concerns belong in adapters vs use cases:
    • retries
    • rate limiting
    • idempotency
    • message schema evolution
    • mapping errors to HTTP

Write your answer as if you were explaining to a teammate.


Progressive reveal: one possible solution

Reveal a reference design

Inbound ports

  • SendNotificationUseCase (for HTTP)
  • HandleUserSignedUpUseCase (for Kafka)
  • PreviewNotificationUseCase (dry-run)

Outbound ports

  • EmailSenderPort
  • SmsSenderPort
  • AuditLogPort
  • IdempotencyStorePort

Concern placement

  • Retries: outbound adapters (email/sms) with policy knobs from application
  • Rate limiting: outbound adapter (SMS client) + application policy (when to fallback)
  • Idempotency: application/use case using IdempotencyStorePort
  • Schema evolution: inbound Kafka adapter (versioned deserialization)
  • HTTP error mapping: inbound HTTP adapter

Failure strategy

  • Non-retryable errors -> dead-letter topic with reason
  • Retryable errors -> exponential backoff + bounded retries, then DLQ
  • Provider outage -> fallback (email->sms) if business allows

Key insight: A good hexagon design reads like an operational plan: clear contracts, clear failure handling, replaceable dependencies.


:wave: Closing: What to do next in your codebase

Challenge questions

  1. Identify one place where domain imports an infrastructure dependency. How would you extract a port?
  2. Pick one flaky outbound call. Can you move retry/circuit breaker logic into an adapter?
  3. Add a second inbound adapter (CLI or consumer) that reuses an existing use case. What breaks?

Practical next step

Start with one use case (e.g., PlaceOrder).

  • Define an inbound port
  • Define outbound ports for the dependencies it truly needs
  • Move translation and resilience into adapters
  • Add tests that simulate timeouts and duplicates

You’ll feel the architecture “click” when:

  • domain tests run without Docker
  • you can add a new adapter without touching domain logic
  • failure handling becomes explicit and consistent

Key Takeaways

  1. Hexagonal architecture separates business logic from external dependencies — the core domain has no knowledge of databases, APIs, or frameworks
  2. Ports define interfaces, adapters implement them — swapping a database or API requires only a new adapter, not changes to business logic
  3. Inbound adapters handle incoming requests (HTTP, CLI, events) — they translate external input into domain operations
  4. Outbound adapters handle external integrations (database, messaging, APIs) — the domain calls port interfaces, adapters provide implementations
  5. This architecture makes testing easy — mock the ports and test business logic in complete isolation from infrastructure
Chapter complete!

Course Complete!

You've finished all 51 chapters of

System Design Advanced

Browse courses
Up next Thundering Herd Problem
Continue