← Volver al listado de tecnologías

Capítulo 1: Conceptos Fundamentales y Orígenes

Por: Tu Nombre
arquitecturahexagonalports-adapterstypescriptfundamentos

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:

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:

  1. Acoplamiento vertical: Cambios en la UI afectan a la lógica de negocio
  2. Dependencia de la BD: El dominio depende del ORM o framework de persistencia
  3. Testing difícil: Necesitas la BD y UI para testear lógica de negocio
  4. 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:

3. Componentes Principales

3.1 El Dominio (Hexágono Central)

El núcleo de la aplicación. Contiene:

Características clave:

// ✅ 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:

Tu laptop (dominio) no necesita saber si está conectada a:

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:

Solo TypeScript puro.

8. Conclusión

La Arquitectura Hexagonal de Alistair Cockburn resuelve problemas fundamentales del desarrollo de software:

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 ArchitectureArquitectura HexagonalPatrón arquitectónico creado por Alistair Cockburn en 2005
Ports and AdaptersPuertos y AdaptadoresNombre alternativo que describe el patrón de diseño
DomainDominioNúcleo de la aplicación con lógica de negocio pura
PortPuertoInterface que define comunicación con el exterior
AdapterAdaptadorImplementación concreta que conecta con tecnologías
Primary PortPuerto PrimarioInterface que permite invocar el dominio (driving)
Secondary PortPuerto SecundarioInterface que el dominio usa (driven)
Use CaseCaso de UsoOrquestación de lógica de negocio
Value ObjectObjeto de ValorObjeto inmutable sin identidad

Referencias