Hexagonal Architecture (Ports & Adapters)
Invert your dependencies: core domain logic at the center, adapters at the edges. How ports and adapters enable testable, framework-independent code.
The Problem Hexagonal Architecture Solves
Hexagonal Architecture (also called Ports and Adapters) was introduced by Alistair Cockburn in 2005. It addresses a fundamental problem with layered architectures: the domain logic depends on the database. In a typical three-tier app, your business logic imports `UserRepository`, which imports `pg` (PostgreSQL), which means your domain logic cannot run without a real database. Testing is hard; switching databases is hard; the domain is not truly isolated.
The hexagonal approach inverts this: the domain/application core sits at the center and defines ports (interfaces). Infrastructure components (databases, message queues, HTTP clients, UI frameworks) implement those ports as adapters. The core never imports infrastructure — infrastructure imports the core.
Ports vs Adapters
| Concept | Definition | Example |
|---|---|---|
| Inbound Port (Driving) | Interface the core exposes for external actors to call into the application | OrderService interface with placeOrder(), cancelOrder() methods |
| Outbound Port (Driven) | Interface the core defines for infrastructure it needs to call | UserRepository interface with findById(), save() methods |
| Inbound Adapter | Implementation that translates external input and calls an inbound port | REST controller, CLI command handler, message consumer |
| Outbound Adapter | Implementation of an outbound port using real infrastructure | PostgresUserRepository, SendGridEmailSender, StripePaymentGateway |
Code Example: The Dependency Inversion
// ── Outbound Port (defined by the Core) ──
// The core owns this interface. It does NOT import PostgreSQL.
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}
// ── Domain Entity ──
class User {
constructor(public id: string, public email: string, private active: boolean) {}
deactivate(): void {
if (!this.active) throw new DomainError("User already inactive");
this.active = false;
}
}
// ── Application Service (Use Case) ──
// Depends only on the port interface, not the adapter
class UserService {
constructor(private readonly users: UserRepository) {}
async deactivateUser(userId: string): Promise<void> {
const user = await this.users.findById(userId);
if (!user) throw new NotFoundError(userId);
user.deactivate();
await this.users.save(user);
}
}
// ── Outbound Adapter (infrastructure, depends on Core) ──
class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
const row = await db.query("SELECT * FROM users WHERE id = $1", [id]);
return row ? new User(row.id, row.email, row.active) : null;
}
async save(user: User): Promise<void> {
await db.query("UPDATE users SET ...", [...]);
}
}
// ── Inbound Adapter (HTTP controller) ──
app.delete("/users/:id", async (req, res) => {
await userService.deactivateUser(req.params.id);
res.status(204).send();
});The Testing Superpower
The primary benefit of hexagonal architecture is testability. Because the application core depends only on port interfaces, you can inject in-memory implementations for tests. No database, no HTTP server, no email service — just fast, deterministic unit tests for all your business logic.
// Fast unit test — no database needed
class InMemoryUserRepository implements UserRepository {
private users: Map<string, User> = new Map();
async findById(id: string) { return this.users.get(id) ?? null; }
async save(user: User) { this.users.set(user.id, user); }
}
describe("UserService.deactivateUser", () => {
it("deactivates an active user", async () => {
const repo = new InMemoryUserRepository();
repo.save(new User("u1", "alice@example.com", true));
const svc = new UserService(repo);
await svc.deactivateUser("u1");
const user = await repo.findById("u1");
expect(user?.isActive).toBe(false);
});
});Why 'Hexagonal'?
The hexagon shape is arbitrary — Cockburn chose it to visually convey that the application has multiple sides (ports), and any number of adapters can plug into any port. It is NOT that there are exactly six ports. The hexagon is a conceptual shape that avoids the up/down bias of a layered diagram.
Hexagonal vs Clean vs Onion Architecture
Hexagonal, Clean Architecture (Uncle Bob), and Onion Architecture (Jeffrey Palermo) are all variations of the same idea: isolate the domain from infrastructure using dependency inversion. They differ in naming and in how they slice the internal layers. In interviews, these terms are often used interchangeably to describe 'domain-centric architecture with inverted dependencies.'
Interview Tip
When asked about testability in your design, mention hexagonal architecture: 'I'd define repository and service interfaces as ports in the domain layer, with the concrete PostgreSQL or Kafka implementations as adapters in the infrastructure layer. This means all business logic tests run in-process with in-memory adapters — fast and reliable.' This signals architectural maturity.