Vetora logo
Architectural Patterns

Hexagonal Architecture (Ports and Adapters)

Explore hexagonal architecture (ports and adapters), a pattern that isolates core business logic from external concerns like databases, APIs, and frameworks, enabling testability and technology flexibility.

Overview

Hexagonal architecture, coined by Alistair Cockburn in 2005, addresses a pervasive problem in software systems: business logic becomes entangled with infrastructure concerns. Controllers contain validation logic, database queries leak into service layers, and HTTP-specific concepts appear in domain models. The result is code that is hard to test (you need a running database), hard to migrate (switching from MySQL to PostgreSQL touches hundreds of files), and hard to understand (business rules are scattered across layers).

The solution is to place the domain core -- entities, value objects, domain services, and business rules -- at the center of the architecture with zero dependencies on external systems. The core defines ports: interfaces that describe what it needs from the outside world. A 'OrderRepository' port declares methods like 'save(order)' and 'findById(id)' without specifying whether the implementation uses PostgreSQL, MongoDB, or an in-memory hash map. A 'PaymentGateway' port declares 'charge(amount, card)' without knowing whether Stripe or PayPal is behind it.

Adapters are the implementations that bridge the ports to real infrastructure. A 'PostgresOrderRepository' adapter implements the 'OrderRepository' port using SQL queries. An 'InMemoryOrderRepository' adapter implements the same port for unit tests. A 'StripePaymentAdapter' implements the 'PaymentGateway' port using Stripe's API. Adapters are divided into two categories: driving (primary) adapters that invoke the domain core (HTTP controllers, CLI handlers, message consumers) and driven (secondary) adapters that the domain core invokes through ports (databases, external APIs, message publishers).

The key insight is the direction of dependencies. In traditional layered architecture, the domain depends on the database layer. In hexagonal architecture, dependencies point inward: adapters depend on the core, never the reverse. This dependency inversion means the core can be compiled, tested, and reasoned about without any infrastructure. You can run your entire business logic test suite in milliseconds using in-memory adapters, then separately test each adapter against real infrastructure. This separation also means swapping a database, replacing a payment provider, or adding a new entry point (GraphQL alongside REST) requires only writing new adapters -- the core remains unchanged.

Key Points
  • 1The domain core contains all business logic and has zero dependencies on frameworks, databases, or external services. It defines ports (interfaces) for everything it needs from the outside world.
  • 2Driving (primary) adapters push work into the core: REST controllers, GraphQL resolvers, CLI handlers, message consumers, scheduled jobs. They translate external requests into domain commands.
  • 3Driven (secondary) adapters are called by the core through ports: database repositories, external API clients, message publishers, file storage. They translate domain operations into infrastructure calls.
  • 4Dependency inversion is the architectural invariant: adapters depend on the core, never the reverse. The core defines interfaces (ports); adapters implement them. This is enforced through language-level dependency injection.
  • 5Testing becomes dramatically easier. The domain core is tested with in-memory adapters in milliseconds. Each adapter is integration-tested against real infrastructure independently. No more 'start the whole application to test a business rule.'
  • 6Hexagonal architecture is technology-agnostic at the core level. Migrating from PostgreSQL to DynamoDB, or from RabbitMQ to Kafka, only requires writing new adapters. The core's business logic is unaffected.
Simple Example

The Power Outlet Analogy

Your laptop (the domain core) needs electricity but does not care whether it comes from a wall outlet, a battery pack, or a solar panel. It defines a port -- the power connector specification. Each power source provides an adapter: the wall charger, the car adapter, the USB-C cable from a battery pack. When you travel to another country with different wall outlets, you do not redesign your laptop -- you use a different adapter. Similarly, in hexagonal architecture, your business logic defines what it needs (ports), and different adapters connect it to different infrastructure. You can test your laptop's functionality without any power source by using a lab power supply (an in-memory test adapter).

Real-World Examples

Netflix

Netflix's backend services follow hexagonal architecture principles. Their domain logic for content recommendation, subscriber management, and billing is isolated from infrastructure. This allowed them to migrate from Oracle to Cassandra and from their own data centers to AWS without rewriting business logic. The domain services defined repository ports that were re-implemented with Cassandra adapters while the recommendation algorithms remained unchanged.

Zalando

Zalando, Europe's largest online fashion platform, adopted hexagonal architecture across its 200+ microservices. Their open-source guidelines mandate that domain logic lives in a core module with no Spring Framework dependencies. REST controllers and database repositories are adapters in separate modules. This enabled them to upgrade Spring Boot versions, swap database technologies, and add GraphQL entry points across services without touching domain code.

GitHub

GitHub's architecture separates domain logic from infrastructure using a ports-and-adapters style. Their core domain models for repositories, pull requests, and issues are independent of MySQL-specific code. When they needed to add GitHub Actions (a new driving adapter for CI/CD triggers), the domain core's webhook and event processing logic did not need to change -- only new adapters were written to connect the CI/CD infrastructure.

Trade-Offs
AspectDescription
Testability vs IndirectionHexagonal architecture makes domain logic trivially testable with in-memory adapters and eliminates the need for integration tests to verify business rules. However, the additional interfaces and indirection layers increase the amount of boilerplate code and can make the codebase harder to navigate, especially for developers unfamiliar with the pattern.
Technology Flexibility vs Upfront Design CostDefining clean ports makes it easy to swap infrastructure (e.g., migrating databases) without touching business logic. However, designing good port interfaces requires significant upfront investment in understanding domain boundaries, and premature abstraction can create ports that do not align with real infrastructure capabilities.
Domain Purity vs PerformanceKeeping the domain core free of infrastructure concerns makes it clean and portable. However, some optimizations (database-specific query hints, batch operations, caching annotations) require infrastructure awareness that pure domain models cannot express. Over-abstracting can lead to the 'lowest common denominator' problem where ports cannot leverage technology-specific features.
Team Autonomy vs ConsistencyHexagonal architecture enforces a consistent structure that makes it easy for developers to move between projects -- the pattern is always the same. However, it requires team discipline to maintain the boundary between core and adapters. Without enforcement (linting rules, architecture tests), shortcuts creep in and erode the pattern over time.
Case Study

Database Migration Without Business Logic Changes

Scenario

A fintech startup running on PostgreSQL needed to migrate its transaction processing service to DynamoDB to handle 100x growth in transaction volume. The service contained 40,000 lines of business logic for fraud detection, transaction routing, and settlement calculations. In the original implementation, SQL queries and PostgreSQL-specific features were deeply embedded throughout the codebase, making migration estimates range from 6-12 months with high regression risk.

Solution

Before the migration, the team refactored to hexagonal architecture over 8 weeks. They extracted all business logic into a domain core with no database dependencies, defining repository ports for all data access. The existing PostgreSQL code was moved into adapter implementations. Once the hexagonal structure was in place, they wrote DynamoDB adapters implementing the same ports. The domain core -- all 40,000 lines of fraud detection, routing, and settlement logic -- remained completely unchanged. Both adapters ran in parallel during a 4-week shadow period where all writes went to both databases and reads were compared.

Outcome

The DynamoDB migration was completed in 14 weeks total (8 weeks refactoring + 2 weeks DynamoDB adapter development + 4 weeks shadow testing). Zero business logic bugs were introduced because the domain core was not modified. The refactoring itself uncovered 12 business logic bugs that had been hidden by tight database coupling. Post-migration, the service handles 50,000 transactions per second on DynamoDB versus the 500 TPS ceiling on PostgreSQL, and the hexagonal structure made subsequent infrastructure changes (adding Redis caching, switching message brokers) trivial.

Common Mistakes
  • Leaking infrastructure types into the domain core. Using JPA annotations on domain entities, Spring's @Transactional on domain services, or database-specific column types in domain models defeats the purpose of the architecture. Domain types must be plain objects with no infrastructure dependencies.
  • Creating ports that mirror infrastructure APIs instead of domain needs. A repository port should expose domain-meaningful operations like 'findActiveOrdersForCustomer()', not generic infrastructure operations like 'query(SQL string)'. Ports should be designed from the domain's perspective, not the infrastructure's.
  • Skipping the adapter layer for 'simple' infrastructure. Starting with direct database calls 'just for now' and promising to add the port layer later leads to coupling that is progressively harder to unwind. The port/adapter boundary should be established from the first line of code.
  • Over-abstracting ports for infrastructure that will never change. If you are certain the system will always use PostgreSQL, creating an elaborate repository abstraction adds complexity without benefit. Apply hexagonal architecture selectively to boundaries that genuinely need flexibility or testability.
Related Concepts

See Hexagonal Architecture (Ports and Adapters) in action

Explore system design templates that use hexagonal architecture (ports and adapters) and run traffic simulations to see how these concepts perform under real load.

Browse Templates

Swap database adapters without touching business logic

Metrics to watch
adapter_latency_mstest_coverage_pctp99_latency_msthroughput_rps
Run Simulation
Test Your Understanding

1In hexagonal architecture, which direction do dependencies flow?

2What is the difference between a driving (primary) adapter and a driven (secondary) adapter?

Deeper Reading