Capítulo 1: Conceptos Fundamentales y Orígenes
Capítulo 1: Conceptos Fundamentales y Orígenes
1. Historia y Origen
El Creador: Alistair Cockburn
La Arquitectura Hexagonal fue creada por Alistair Cockburn en 2005, aunque la idea había estado gestándose desde principios de los años 90. Cockburn es un científico informático reconocido por sus contribuciones al desarrollo ágil de software y autor del libro “Crystal Clear: A Human-Powered Methodology for Small Teams”.
El Contexto Histórico
A principios de los 2000, la industria del software enfrentaba un problema recurrente: las aplicaciones estaban fuertemente acopladas a frameworks, bases de datos y tecnologías específicas. Esto generaba:
- Dificultad para testear sin levantar toda la infraestructura
- Imposibilidad de cambiar tecnologías sin reescribir el sistema
- Lógica de negocio mezclada con detalles técnicos
- Proyectos legacy imposibles de mantener
El Artículo Original
En 2005, Cockburn publicó su artículo “Hexagonal Architecture” donde propuso un patrón arquitectónico alternativo. El nombre original también fue “Ports and Adapters” (Puertos y Adaptadores), que describe mejor el patrón subyacente.
“Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.”
— Alistair Cockburn
Traducción: “Permitir que una aplicación sea conducida de igual manera por usuarios, programas, tests automatizados o scripts batch, y que pueda ser desarrollada y testeada de forma aislada de sus dispositivos y bases de datos finales.”
2. El Problema que Resuelve
La Arquitectura Tradicional en Capas
Tradicionalmente, las aplicaciones se organizaban en tres capas horizontales:
┌─────────────────────────┐
│ Presentación (UI) │
├─────────────────────────┤
│ Lógica de Negocio │
├─────────────────────────┤
│ Persistencia (DB) │
└─────────────────────────┘
Problemas de este enfoque:
- Acoplamiento vertical: Cambios en la UI afectan a la lógica de negocio
- Dependencia de la BD: El dominio depende del ORM o framework de persistencia
- Testing difícil: Necesitas la BD y UI para testear lógica de negocio
- Frameworks invasivos: El código de negocio está lleno de anotaciones y dependencias
Ejemplo del Problema
// ❌ Lógica de negocio acoplada a framework
import { Entity, Column } from 'typeorm'; // Dependencia de TypeORM
@Entity() // Anotación del framework
class User {
@Column()
email: string;
@Column()
password: string;
// ¿Dónde está la lógica de negocio?
// ¿Cómo testeo sin la base de datos?
}
La Propuesta de Cockburn
La arquitectura hexagonal invierte las dependencias y separa claramente:
┌─────────────────┐
┌───│ Adaptadores │───┐
│ │ (Externos) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
└──▶│ Puertos │◀──┘
│ (Interfaces) │
└────────┬────────┘
│
┌────────▼────────┐
│ Dominio │
│ (Lógica Pura) │
└─────────────────┘
Ventajas:
- ✅ Lógica de negocio independiente de frameworks
- ✅ Testeable sin infraestructura
- ✅ Intercambiable: Cambia BD, UI o API sin tocar el dominio
- ✅ Mantenible: Cambios aislados en cada capa
3. Componentes Principales
3.1 El Dominio (Hexágono Central)
El núcleo de la aplicación. Contiene:
- Entidades: Objetos con identidad única
- Value Objects: Objetos inmutables sin identidad
- Lógica de Negocio: Reglas y comportamientos puros
Características clave:
- No conoce frameworks, bases de datos ni APIs
- Es puro TypeScript/JavaScript
- Define sus propias interfaces (puertos)
// ✅ Dominio puro, sin dependencias externas
class User {
private constructor(
private readonly id: string,
private readonly email: Email,
private password: Password
) {}
static create(id: string, email: string, password: string): User {
return new User(
id,
Email.create(email),
Password.create(password)
);
}
changePassword(currentPassword: string, newPassword: string): void {
if (!this.password.matches(currentPassword)) {
throw new Error('Password incorrecto');
}
this.password = Password.create(newPassword);
}
}
3.2 Los Puertos (Interfaces)
Los puertos son interfaces que definen cómo se comunica el dominio con el exterior. Hay dos tipos:
Puertos Primarios (Primary/Driving)
Definen cómo el mundo exterior usa el dominio. También llamados “driving ports” porque conducen la aplicación.
// Puerto primario: Define QUÉ puede hacer el dominio
interface UserService {
createUser(email: string, password: string): Promise<User>;
getUserById(id: string): Promise<User>;
changePassword(userId: string, oldPass: string, newPass: string): Promise<void>;
}
Puertos Secundarios (Secondary/Driven)
Definen cómo el dominio usa el mundo exterior. También llamados “driven ports” porque el dominio los conduce.
// Puerto secundario: Define QUÉ necesita el dominio
interface UserRepository {
save(user: User): Promise<void>;
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
}
3.3 Los Adaptadores
Los adaptadores son implementaciones concretas de los puertos. Conectan el dominio con tecnologías reales.
Adaptadores Primarios (Driving)
Implementan la infraestructura que invoca al dominio:
// Adaptador primario: Express API
class UserController {
constructor(private userService: UserService) {}
async createUser(req: Request, res: Response): Promise<void> {
const { email, password } = req.body;
const user = await this.userService.createUser(email, password);
res.status(201).json({ id: user.id });
}
}
Adaptadores Secundarios (Driven)
Implementan la infraestructura que el dominio necesita:
// Adaptador secundario: PostgreSQL Repository
class PostgresUserRepository implements UserRepository {
constructor(private db: Pool) {}
async save(user: User): Promise<void> {
await this.db.query(
'INSERT INTO users (id, email, password) VALUES ($1, $2, $3)',
[user.id, user.email, user.password]
);
}
async findById(id: string): Promise<User | null> {
const result = await this.db.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return result.rows[0] ? User.fromDB(result.rows[0]) : null;
}
}
4. Modelo Mental: La Analogía del Enchufe
Para entender la arquitectura hexagonal, piensa en un enchufe eléctrico:
- El Dominio es un aparato electrónico (ej: una laptop)
- Los Puertos son las interfaces de conexión (ej: puerto USB-C)
- Los Adaptadores son los cables específicos (ej: cable USB-C a HDMI)
Tu laptop (dominio) no necesita saber si está conectada a:
- Un monitor externo (UI web)
- Un proyector (API REST)
- Un disco duro (base de datos PostgreSQL)
- Un sistema de testing (mocks)
Solo define el puerto (interface) y cualquier adaptador que cumpla esa interface puede conectarse.
5. Flujo de una Petición
Veamos cómo fluye una petición HTTP a través de la arquitectura:
1. HTTP Request
↓
2. [Adaptador Primario] Controller
↓
3. [Puerto Primario] UserService Interface
↓
4. [Caso de Uso] CreateUserUseCase
↓
5. [Dominio] User.create()
↓
6. [Puerto Secundario] UserRepository Interface
↓
7. [Adaptador Secundario] PostgresUserRepository
↓
8. Database
Lo importante: El flujo siempre va hacia el dominio y sale de él a través de interfaces (puertos).
6. Ejemplo Completo Minimalista
// ========== DOMINIO ==========
class Email {
private constructor(private value: string) {}
static create(email: string): Email {
if (!email.includes('@')) throw new Error('Email inválido');
return new Email(email);
}
toString(): string { return this.value; }
}
class User {
constructor(
readonly id: string,
readonly email: Email
) {}
}
// ========== PUERTOS ==========
// Puerto primario
interface UserService {
createUser(email: string): Promise<User>;
}
// Puerto secundario
interface UserRepository {
save(user: User): Promise<void>;
exists(email: string): Promise<boolean>;
}
// ========== APLICACIÓN ==========
class CreateUserUseCase implements UserService {
constructor(private userRepo: UserRepository) {}
async createUser(email: string): Promise<User> {
const emailVO = Email.create(email);
if (await this.userRepo.exists(email)) {
throw new Error('Email ya registrado');
}
const user = new User(crypto.randomUUID(), emailVO);
await this.userRepo.save(user);
return user;
}
}
// ========== ADAPTADORES ==========
// Adaptador primario: CLI
class CliAdapter {
constructor(private userService: UserService) {}
async run(args: string[]): Promise<void> {
const email = args[0];
const user = await this.userService.createUser(email);
console.log(`Usuario creado: ${user.id}`);
}
}
// Adaptador secundario: In-Memory
class InMemoryUserRepository implements UserRepository {
private users: User[] = [];
async save(user: User): Promise<void> {
this.users.push(user);
}
async exists(email: string): Promise<boolean> {
return this.users.some(u => u.email.toString() === email);
}
}
// ========== COMPOSICIÓN ==========
const repository = new InMemoryUserRepository();
const useCase = new CreateUserUseCase(repository);
const cli = new CliAdapter(useCase);
// Ejecutar
cli.run(['[email protected]']);
7. Beneficios Clave
Testabilidad
// Test sin base de datos real
test('debería crear un usuario', async () => {
const mockRepo = new InMemoryUserRepository();
const useCase = new CreateUserUseCase(mockRepo);
const user = await useCase.createUser('[email protected]');
expect(user.email.toString()).toBe('[email protected]');
});
Intercambiabilidad
// Cambiar de in-memory a PostgreSQL sin tocar el dominio
const repository = new PostgresUserRepository(pool);
const useCase = new CreateUserUseCase(repository); // ¡Mismo código!
const api = new ExpressAdapter(useCase);
Independencia de Frameworks
El dominio no importa:
- ❌ Express, Fastify, NestJS
- ❌ TypeORM, Prisma, Mongoose
- ❌ React, Vue, Angular
Solo TypeScript puro.
8. Conclusión
La Arquitectura Hexagonal de Alistair Cockburn resuelve problemas fundamentales del desarrollo de software:
- Separa la lógica de negocio de la infraestructura
- Define interfaces claras (puertos) entre capas
- Permite cambiar tecnologías sin afectar el dominio
- Facilita el testing y mantenimiento
En el próximo capítulo veremos los principios de diseño que sustentan esta arquitectura.
Glosario del Capítulo
| Término (Inglés) | Término (Español) | Definición |
|---|---|---|
| Hexagonal Architecture | Arquitectura Hexagonal | Patrón arquitectónico creado por Alistair Cockburn en 2005 |
| Ports and Adapters | Puertos y Adaptadores | Nombre alternativo que describe el patrón de diseño |
| Domain | Dominio | Núcleo de la aplicación con lógica de negocio pura |
| Port | Puerto | Interface que define comunicación con el exterior |
| Adapter | Adaptador | Implementación concreta que conecta con tecnologías |
| Primary Port | Puerto Primario | Interface que permite invocar el dominio (driving) |
| Secondary Port | Puerto Secundario | Interface que el dominio usa (driven) |
| Use Case | Caso de Uso | Orquestación de lógica de negocio |
| Value Object | Objeto de Valor | Objeto inmutable sin identidad |