Hexagonal Architecture (Ports & Adapters)

Explore the Ports & Adapters pattern that isolates your application core from external systems using explicit interfaces.

Advanced · 16 min read

What is Hexagonal Architecture?

Hexagonal Architecture, proposed by Alistair Cockburn, structures an application around its core domain. External systems (databases, APIs, UIs) connect to the core through Ports (interfaces) and Adapters (implementations). The hexagonal shape is just a visual metaphor meaning "many sides" for different types of external actors.

Ports & Adapters Diagram

Hexagonal Architecture — the application core defines ports, adapters plug in from outside

Driving vs Driven

Driving (Primary) Driven (Secondary)
Initiates interaction with the app Called by the app when it needs something
Examples: HTTP controller, CLI, tests Examples: database, email service, message queue
Calls USE CASE methods Implements PORT interfaces
Left side of the hexagon Right side of the hexagon

Port Interface

// Port — defined INSIDE the application core
// The core does not know how notifications are sent
export interface NotificationPort {
  send(to: string, subject: string, body: string): Promise<void>;
}

Adapter Implementations

// Driven adapter — implements the port using SMTP
import { NotificationPort } from '../ports/NotificationPort';
import nodemailer from 'nodemailer';

export class EmailNotificationAdapter implements NotificationPort {
  private transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    port: 587,
    auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
  });

  async send(to: string, subject: string, body: string): Promise<void> {
    await this.transporter.sendMail({
      from: 'noreply@app.com', to, subject, html: body,
    });
  }
}
// Alternative adapter — same port, different technology
import { NotificationPort } from '../ports/NotificationPort';

export class SlackNotificationAdapter implements NotificationPort {
  constructor(private webhookUrl: string) {}

  async send(to: string, subject: string, body: string): Promise<void> {
    await fetch(this.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: `*${subject}*\n${body}\n(to: ${to})` }),
    });
  }
}
// Composition root — wire adapters at startup
import { CreateUser } from './use-cases/CreateUser';
import { PostgresUserRepository } from './adapters/PostgresUserRepository';
import { EmailNotificationAdapter } from './adapters/EmailNotificationAdapter';

const userRepo = new PostgresUserRepository();
const notifier = new EmailNotificationAdapter();
const createUser = new CreateUser(userRepo, notifier);

// In tests, swap with in-memory / mock adapters
// const userRepo = new InMemoryUserRepository();
// const notifier = new FakeNotificationAdapter();

Benefits

  • Technology independence: Swap Postgres for MongoDB by writing a new adapter
  • Testability: Test use cases with in-memory adapters
  • Flexibility: Add new entry points (REST, gRPC, CLI) without changing core logic
  • Explicit boundaries: Ports document what the application needs from the outside world

TIP: Key takeaway: Hexagonal Architecture makes external dependencies pluggable via Ports (interfaces defined by the core) and Adapters (implementations that live outside the core). This is Clean Architecture's practical cousin.


Part of the Software Architecture series on Tekivex. Browse all tutorials or explore our open-source products.