Enterprise Architecture Patterns Tutorial 0/65 lessons ~6 min read Lesson 21

    Hexagonal Architecture

    Hexagonal architecture (ports and adapters) places the domain at the center, surrounded by ports (interfaces) and adapters (implementations) for every external system.

    Course progress0%
    Focus
    18 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Hexagonal architecture (ports and adapters) places the domain at the center, surrounded by ports (interfaces) and adapters (implementations) for every external system. Uber adopted it widely in Go and Java services so ride-matching logic stays testable without Postgres, Kafka, or gRPC fixtures.

    Real production story

    In 2018, Uber's Marketplace team shipped a fare-estimation change that passed unit tests but failed in staging — tests mocked a simplified pricing API while production called a legacy Surge service with different rounding semantics. The bug cost $800K in driver incentives over a weekend.

    The team rewrote fare estimation as a hexagon: PricingPort interface in the domain core, SurgeAdapter and FlatRateAdapter in infrastructure, and contract tests against Surge's sandbox as CI gates. Domain tests ran in 4 seconds with zero network. Adapter swaps for city launches became configuration changes, not rewrites.

    Business problem

    Business pressure: Uber launches in new cities weekly; each market has different payment rails, mapping providers, and regulatory rules. Core dispatch logic cannot be recompiled for every adapter variant.

    • Revenue at risk: Incorrect fare display erodes driver and rider trust — surge miscalculations trigger support tickets at 10× normal volume.
    • Engineering velocity: Integration-test suites taking 45 minutes block deploys; teams need fast domain feedback loops.
    • Compliance / trust: GDPR erasure must propagate through adapters without domain knowing storage details.

    Architecture overview

    The hexagon is the application boundary. Primary ports are driving (API, CLI); secondary ports are driven (DB, messaging). Adapters translate between port contracts and external technology.

    • Definition: Domain + ports inside; adapters outside — dependency arrow always points inward.
    • When to adopt: Complex domain logic with many integrations; high test coverage requirements.
    • When to defer: Simple CRUD with one database — ports add ceremony without payoff.
    • Operability: Adapter health checks separate from domain metrics; circuit breakers live in adapters.

    Architecture motivation

    Why architects care: Hexagonal architecture inverts the dependency: infrastructure depends on domain, not vice versa. External systems are plug-in details; the domain defines what it needs via ports.

    • Force: Multiple external systems per use case with high swap/churn rate.
    • Constraint: Domain experts must review fare and matching rules without reading gRPC boilerplate.
    • Outcome: Testable core, swappable adapters, clear contract ownership.

    Internal architecture

    Uber fare estimation hexagon — ports as explicit seams:

    text
    ┌─────────────┐ ┌─────────────┐
    │ REST/gRPC │ │ CLI/batch │
    │ Adapter │ │ Adapter │
    └──────┬──────┘ └──────┬──────┘
    │ primary ports │
    ▼ ▼
    ┌─────────────────────────────────┐
    │ DOMAIN CORE │
    │ EstimateFareUseCase │
    │ FarePolicy, SurgeMultiplier │
    │ ─ ─ ─ ports ─ ─ ─ │
    │ PricingPort (out) │
    │ TripRepository (out) │
    │ FareEventPublisher (out) │
    └──────────┬──────────┬───────────┘
    │ │
    ┌──────────▼──┐ ┌────▼──────────┐
    │ SurgeAdapter│ │ PostgresTrip │
    │ (HTTP) │ │ Adapter (JPA) │
    └─────────────┘ └───────────────┘
    ┌──────────▼──────────┐
    │ KafkaFarePublisher │
    └─────────────────────┘

    Data flow

    Primary path: gRPC adapter receives EstimateFareRequest, maps to domain command, use case loads trip context via TripRepository port, calls PricingPort for surge multiplier, domain computes fare, adapter maps response.

    • Write path: Domain emits fare events; Kafka adapter serializes; Postgres adapter persists audit log.
    • Read path: Repository port returns domain objects, never ORM entities crossing the hexagon edge.
    • Async path: Inbound Kafka adapter invokes use case; idempotency key checked in domain before side effects.

    System design diagram

    Two diagrams show the Hexagonal Architecture topology and the primary request/event path used in production at scale.

    Hexagonal Architecture — system view
    REST adapter
    Edge
    Domain core
    Core
    Postgres adapter
    Data
    Kafka adapter
    Async
    High-level topology for Hexagonal Architecture.
    Hexagonal Architecture — request / event flow
    Primary port
    Ingress
    Use case
    Store
    Secondary port
    Store
    Adapter I/O
    Emit
    Follow this path when reviewing production designs.

    Production code example

    Go hexagonal service — Uber-style port/adapter layout:

    go
    // domain/port/pricing.go — driven port (outbound)
    type PricingPort interface {
    SurgeMultiplier(ctx context.Context, geohash string, at time.Time) (decimal.Decimal, error)
    }
    // domain/usecase/estimate_fare.go
    type EstimateFareUseCase struct {
    trips TripRepository
    pricing PricingPort
    events FareEventPublisher
    }
    func (uc *EstimateFareUseCase) Execute(ctx context.Context, cmd EstimateCommand) (Fare, error) {
    trip, err := uc.trips.FindByID(ctx, cmd.TripID)
    if err != nil { return Fare{}, err }
    surge, err := uc.pricing.SurgeMultiplier(ctx, trip.Geohash(), cmd.RequestedAt)
    if err != nil { return Fare{}, fmt.Errorf("pricing port: %w", err) }
    fare := trip.ComputeFare(surge) // pure domain
    if err := uc.events.Publish(ctx, fare.EstimatedEvent()); err != nil {
    return Fare{}, err
    }
    return fare, nil
    }
    // adapter/surge_http.go — driven adapter
    type SurgeHTTPAdapter struct { client *http.Client; baseURL string }
    func (a *SurgeHTTPAdapter) SurgeMultiplier(ctx context.Context, gh string, at time.Time) (decimal.Decimal, error) {
    // HTTP call, retry, circuit breaker — all adapter concern
    }

    Enterprise case study

    Uber Marketplace fare engine (2018–2020): Hexagonal rewrite cut integration test time 70% and enabled city-specific pricing adapters via feature flags.

    • Before: Domain coupled to gRPC stubs; 45-min CI; surge bugs in production.
    • Decision: Ports in domain, adapters per integration, contract tests on adapter boundary.
    • After: Domain tests <5s; new city pricing adapter shipped in 3 days average.

    Trade-offs

    • Testability vs boilerplate: Every external system needs port + adapter + fake — upfront cost, long-term test speed.
    • Mapping vs leakage: Adapters must translate DTOs; lazy teams leak protobuf types into domain.
    • Single vs multi hexagon: One hexagon per bounded context; sharing ports across contexts creates coupling.

    Security considerations

    Security is architectural: Adapters sanitize and validate all external input before domain; PII never logged in domain layer.

    • Identity: Primary adapters validate mTLS/JWT; domain receives authenticated principal as value object.
    • Data: Encryption happens in persistence adapter; domain handles tokenized IDs only.
    • Supply chain: Adapter dependencies (SDK versions) isolated from domain build — smaller domain audit surface.

    Scalability analysis

    Scale dimensions: Uber scales adapters independently — Surge adapter gets its own connection pool and circuit breaker tuning without touching domain code.

    • Horizontal scale: Stateless primary adapters behind load balancers; domain is in-process.
    • Hot spots: PricingPort adapter becomes bottleneck — cache surge multipliers in adapter layer, not domain.
    • Cost: Adapter-per-integration clarifies which vendor API drives infra spend.

    Failure scenarios

    What breaks: Adapter timeout without domain-level degradation policy; fake ports in tests diverge from production adapters.

    • Contract drift: Surge API changes rounding; unit tests pass, production fails — mandate adapter contract tests in CI.
    • Port god-interface: PricingPort grows 40 methods — split ports by use case (EstimatePort vs SettlePort).
    • Hexagon sharing: Two teams import same adapter impl — extract shared adapter lib with versioned contracts.

    Staff engineer insights

    • The hexagon is not a folder structure — it is a dependency rule. If domain imports gRPC, you have folders named ports, not hexagonal architecture.
    • Uber staff interview tip: draw primary vs secondary ports before adapters — interviewers test whether you know driving vs driven sides.
    • Invest in fake adapters for tests that behave like production (same validation, stricter timeouts) — happy-path mocks lie.

    Interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionExplain primary vs secondary ports in hexagonal architecture.+

    Answer

    Primary (driving) ports are entry points — API, UI, CLI — they invoke the application. Secondary (driven) ports are what the application calls — database, message bus, external APIs. Domain defines both as interfaces; adapters implement secondary and invoke primary.

    Follow-up

    Where does a Kafka consumer adapter sit?
    2AdvancedQuestionHow do hexagonal architecture and clean architecture differ?+

    Answer

    Same dependency rule, different vocabulary. Hexagonal emphasizes ports/adapters and symmetric external interfaces; Clean Architecture names concentric rings (entities, use cases, gateways). Uber teams often say hexagonal for services, clean for modular monoliths — interchangeable at staff level if dependency direction is correct.

    Follow-up

    Which pattern did Uber use for new Go services in 2019?
    3AdvancedQuestionHow do you test hexagonal architecture without integration tests?+

    Answer

    Domain tests use in-memory fakes implementing ports — fakes enforce port contracts (reject invalid input like production). Adapter tests are thin contract tests against vendor sandboxes. One golden-path integration test per adapter, not per use case.

    Follow-up

    What is a contract test vs a unit test on a fake?

    Architecture review questions

    • Does domain code import zero infrastructure packages (HTTP, ORM, SDK)?
    • Are all external systems accessed through named ports (interfaces)?
    • Do primary adapters only map DTOs and delegate to use cases?
    • Are adapter contract tests in CI for each secondary port implementation?
    • Are fakes used in domain tests behaviorally strict, not permissive mocks?
    • Is there one hexagon per bounded context, not one mega-hexagon for the whole company?

    Summary

    Hexagonal architecture at Uber scale keeps ride-matching and pricing logic pure while Postgres, Kafka, and Surge APIs swap via adapters. Staff engineers draw ports before adapters, enforce dependency inversion in CI, and treat adapter contract tests as production gates.

    Ready to mark this lesson complete?Track your journey across the entire course.