Hexagonal Architecture
Hexagonal architecture (also called ports and adapters) is a design pattern that isolates the core business logic of an application from its external dependencies by defining explicit boundaries through ports (interfaces) and adapters (implementations).
What It Really Means
In a typical application, business logic gets tangled with infrastructure concerns. Your order processing function calls the Stripe API directly, queries the database using raw SQL, and sends Kafka messages. If you want to switch from PostgreSQL to DynamoDB, you have to rewrite your business logic. If you want to test order processing without a running database and Kafka broker, you cannot.
Hexagonal architecture solves this by placing business logic at the center and defining ports — interfaces that describe what the application needs from the outside world and what it offers to it. Adapters implement these interfaces for specific technologies. Your business logic says "I need to save an order" (port). One adapter implements this with PostgreSQL, another with DynamoDB, another with an in-memory store for testing.
The architecture was proposed by Alistair Cockburn in 2005. The hexagonal shape is arbitrary — it simply provides enough sides to illustrate that there are many ports, not just the traditional "top" (UI) and "bottom" (database) layers. The key insight is that the dependency arrow always points inward: adapters depend on the core, never the reverse.
This is closely related to Clean Architecture (Robert C. Martin) and Onion Architecture (Jeffrey Palermo). All three share the same fundamental principle: protect the domain from infrastructure details.
How It Works in Practice
The Three Zones
Core (Domain): Pure business logic. No imports from frameworks, databases, or external APIs. Contains entities, value objects, domain services, and business rules. This is the most stable and valuable part of your system.
Ports: Interfaces that define how the core communicates with the outside world. There are two types:
- Driving ports (primary): How the outside world uses the core. Example:
OrderService.placeOrder(). These are implemented by the core.
- Driven ports (secondary): What the core needs from the outside world. Example:
OrderRepository.save(), PaymentGateway.charge(). These are interfaces the core defines but adapters implement.
Adapters: Concrete implementations that connect ports to real technologies.
- Driving adapters: REST controllers, CLI handlers, gRPC servers — they call the core through driving ports.
- Driven adapters: PostgreSQL repositories, Stripe payment gateways, Kafka event publishers — they implement driven ports.
Real-World Example: Payment Processing
A payment processing system might have:
- Core: Business rules for validating payments, calculating fees, handling refunds
- Driving ports: REST API, admin dashboard, webhook receiver
- Driven ports:
PaymentGateway, TransactionRepository, FraudDetector, EventPublisher
- Driven adapters: Stripe adapter, PostgreSQL adapter, ML fraud service adapter, Kafka adapter
Swapping Stripe for Adyen means writing a new PaymentGateway adapter. The core business logic — fee calculation, refund rules, fraud thresholds — does not change.
Implementation