Hexagonal Architecture
Hexagonal architecture (ports and adapters) places the domain at the center, surrounded by ports (interfaces) and adapters (implementations) for every external system.
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:
┌─────────────┐ ┌─────────────┐│ 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.
Production code example
Go hexagonal service — Uber-style port/adapter layout:
// 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.gotype EstimateFareUseCase struct {trips TripRepositorypricing PricingPortevents 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 domainif err := uc.events.Publish(ctx, fare.EstimatedEvent()); err != nil {return Fare{}, err}return fare, nil}// adapter/surge_http.go — driven adaptertype 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.
1AdvancedQuestionExplain primary vs secondary ports in hexagonal architecture.+
Answer
Follow-up
2AdvancedQuestionHow do hexagonal architecture and clean architecture differ?+
Answer
Follow-up
3AdvancedQuestionHow do you test hexagonal architecture without integration tests?+
Answer
Follow-up
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.