Switch to:
Hexagonal architectures: a new perspective with ports and adapters

Hexagonal architectures: a new perspective with ports and adapters

The evolution of software engineering has led to the birth of increasingly sophisticated architectural paradigms, among which the hexagonal architecture stands out, introduced by Alistair Cockburn in 2005.

This innovative approach, also known as the ‘Ports and Adapters’ pattern, represents a response to the limitations of traditional layered architectures and offers an elegant solution for building robust and easily maintainable software systems.

Traditional three-tier architectures (presentation, business logic, and persistence) have dominated the software development landscape for decades, but this approach has significant structural limitations that compromise its flexibility and testability. The hierarchical dependency from top to bottom creates strong coupling between components, making the software rigid and difficult to modify.

When business logic infiltrates presentation or persistence layers, the problem of code duplication and testing difficulty emerges, compromising the overall quality of the system.

Hexagonal architecture overturns this perspective, proposing a concentric model that places domain logic at the center, protected from external influences. This inside-out vision eliminates the need to think in terms of ‘above’ and ‘below’, focusing instead on the clear separation between the application’s core and the outside world. The heart of hexagonal architecture consists of three fundamental elements that work in synergy to ensure effective decoupling.

The domain or ‘domain’ represents the central core of the application, containing exclusively, for example, entities, service interfaces, and validators. This layer contains no implementation logic, but only pure definitions that establish the business rules.

A practical example of domain definition could be:


// domain/user/index.ts
export interface UserService {
  createUser(userData: CreateUserRequest): Promise<User>;
  findUserById(id: string): Promise<User | null>;
  validateUser(user: User): ValidationResult;
}

export interface CreateUserRequest {
  email: string;
  password: string;
  name: string;
}

export interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

Around the domain develops the application layer or ‘application’, which represents the true brain of the system.

Here resides the business logic, the orchestration of use cases, and the logic that determines how different components must interact to achieve the set objectives. This layer depends exclusively on the interfaces defined in the domain, maintaining its independence from concrete implementations.

In the application configurator, services are instantiated and made available:


// application/configurator.ts
import { UserService } from '../domain/user';
import { DrizzleUserService } from '../adapters/drizzle/user';

export class ServiceConfigurator {
  private userService: UserService;

  constructor() {
    this.userService = new DrizzleUserService();
  }

  getUserService(): UserService {
    return this.userService;
  }

}

The outer crown is formed by adapters or ‘adapter’, the bridges that connect the application to the outside world.

These components take care of translating application requests into concrete calls to databases, authentication services, external APIs, and other systems. Adapters implement the interfaces defined in the domain, ensuring that changing an external technology does not compromise system stability. An example of a concrete adapter for user management with database could be:


// adapters/drizzle/user/index.ts
import { UserService, CreateUserRequest, User } from '../../../domain/user';
import { db } from '../connection';

export class DrizzleUserService implements UserService {
  async createUser(userData: CreateUserRequest): Promise<User> {
    const newUser = await db.insert(usersTable).values({
      email: userData.email,
      password: hashPassword(userData.password),
      name: userData.name,
      createdAt: new Date()
  }).returning();

  return newUser[0];
}

  async findUserById(id: string): Promise<User | null> {
    const users = await db.select().from(usersTable).where(eq(usersTable.id, id));
    return users[0] || null;
  }

  validateUser(user: User): ValidationResult {
    // Implementation of validation
    return { isValid: true, errors: [] };
  }

}

The operating mechanism of hexagonal architecture is based on the concept of ports and adapters. Ports act as technologically neutral entry points, defining the interfaces that allow communication between the application’s core and the outside world. Adapters implement these ports using specific technologies, acting as translators between the domain language and that of external technologies.

An example of use in the API could be:


// application/api/users/index.tsx
import { ServiceConfigurator } from '../../configurator';

const configurator = new ServiceConfigurator();
const userService = configurator.getUserService();

export async function POST(request: Request) {
  const userData = await request.json();

  try {
    const newUser = await userService.createUser(userData);
    return Response.json(newUser, { status: 201 });
  } catch (error) {
    return Response.json({ error: 'Failed to create user' }, { status: 500 });
  }
}

The adoption of hexagonal architecture brings concrete and measurable benefits. Software testability improves dramatically thanks to the intrinsic decoupling between components. It is possible to test business logic in complete isolation, using mocks of adapters to simulate external dependencies. Code maintainability increases significantly thanks to the clear separation of responsibilities, while system scalability benefits from the possibility of replacing or updating individual components without impacting the entire architecture.

The implementation of hexagonal architecture requires an initial investment in terms of design and development, but represents a powerful tool for building software systems that prioritize flexibility, testability, and maintainability. Its adoption requires a mindset shift that moves attention from technology to domain, creating applications that faithfully reflect business needs and naturally adapt to market changes.

For more information: https://alistair.cockburn.us/hexagonal-architecture

Ranieri Vannozzi

The author

Ranieri Vannozzi

Freelance software developer and consultant

I have been writing code since 2008, working on enterprise development, cloud-native architectures and technical leadership. On HeroHubs I write about what I run into every day on real projects, from code to architecture and artificial intelligence.

Article written with AI assistance and reviewed by the author.

Related Posts

Canary vs Blue Green: how to eliminate downtime

Architecture

March 2026

Canary vs Blue Green: how to eliminate downtime

A guide to software release strategies: discover how Blue Green and Canary deployment can protect your business and improve user experience.

Agent as a Service (AaaS): the future beyond SaaS

Architecture

August 2025

Agent as a Service (AaaS): the future beyond SaaS

Explore the Agent as a Service (AaaS) model: how autonomous AI agents are revolutionizing software, surpassing SaaS, and transforming business processes and automation.

SST: the future of serverless on AWS

Architecture

July 2025

SST: the future of serverless on AWS

Discover SST (Serverless Stack): the framework revolutionizing serverless development on AWS. Benefits, user experience, scalability, and best practices for cloud-native projects.

Firebase real-time database: the right choice for real-time applications?

Architecture

June 2025

Firebase real-time database: the right choice for real-time applications?

Explore the pros, cons, and alternatives of Firebase real-time database: a complete guide to choosing the best solution for real-time apps and platforms.

LinkedIn and Apache Kafka: 7 trillion messages a day

Architecture

June 2025

LinkedIn and Apache Kafka: 7 trillion messages a day

Discover how LinkedIn uses Apache Kafka to process 7 trillion messages daily: architecture, scalability, open-source innovations, and best practices for real-time big data.

Software architecture: which one to use?

Architecture

May 2025

Software architecture: which one to use?

A guide to major software architectures: advantages, disadvantages, and use cases for client-server, layered, microservices, event-driven, serverless, and more for modern projects.

Microservices or monolith: still a current debate?

Architecture

March 2025

Microservices or monolith: still a current debate?

Microservices vs monolith: advantages, disadvantages, and use cases to choose the best software architecture for your project in 2025.

AWS, Cloudflare, Supabase and others: which one to choose?

Architecture

February 2025

AWS, Cloudflare, Supabase and others: which one to choose?

A guide to choosing the ideal cloud provider: AWS, Cloudflare, Supabase, Google Cloud, Azure, DigitalOcean, Vercel, Netlify, and Heroku compared for every project need.

The fallacies of distributed systems

Architecture

January 2025

The fallacies of distributed systems

Explore the main fallacies of distributed systems: challenges, best practices, and design patterns for building resilient, secure, and scalable architectures in the cloud era.