Hexagonal architectures: a new perspective with ports and adapters
- October 2025 |
- 03 Mins read
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