Software

Arquitectura hexagonal en la práctica: separando dominio e infraestructura

La arquitectura hexagonal (Ports & Adapters) permite aislar la lógica de negocio de los detalles de infraestructura. El resultado: código más testeable, más fácil de mantener y más resistente al cambio de frameworks o bases de datos.

N-Byte
9 min lectura

La arquitectura hexagonal, propuesta por Alistair Cockburn como "Ports and Adapters", parte de una premisa simple: el código de negocio no debería saber nada sobre cómo llegan los datos ni cómo se persisten. HTTP, SQL, eventos de Kafka, archivos CSV —todos son detalles de infraestructura que el dominio no debería conocer. Cuando lo conoce, cambiar el ORM, migrar de REST a GraphQL o escribir tests unitarios se vuelve doloroso.

La estructura central

El hexágono tiene tres capas:

Dominio: Entidades, value objects, servicios de dominio y puertos (interfaces). Esta capa no importa nada externo —ni frameworks, ni librerías de base de datos, ni HTTP. Solo define qué operaciones son posibles y qué reglas de negocio existen.

Aplicación: Casos de uso. Orquestan el dominio para cumplir una acción específica del negocio. Tampoco conocen HTTP ni SQL —hablan con el dominio a través de las entidades, y con la infraestructura a través de los puertos.

Infraestructura: Adaptadores que implementan los puertos. Aquí viven los repositorios de base de datos, los controladores HTTP, los publicadores de eventos, los clientes de APIs externas.

Un ejemplo concreto en TypeScript

// ─── Dominio ─────────────────────────────────────────────────────────────────
 
// Puerto: interfaz que define qué necesita el dominio de la persistencia
export interface UserRepository {
  findById(id: string): Promise<User | null>
  save(user: User): Promise<void>
}
 
// Entidad de dominio
export class User {
  constructor(
    readonly id: string,
    private email: string,
    private isActive: boolean
  ) {}
 
  activate(): void {
    if (this.isActive) throw new Error("El usuario ya está activo")
    this.isActive = true
  }
}
 
// ─── Aplicación ──────────────────────────────────────────────────────────────
 
export class ActivateUserUseCase {
  constructor(
    private readonly users: UserRepository,  // Puerto, no implementación
    private readonly events: EventPublisher
  ) {}
 
  async execute(userId: string): Promise<void> {
    const user = await this.users.findById(userId)
    if (!user) throw new UserNotFoundError(userId)
 
    user.activate()
 
    await this.users.save(user)
    await this.events.publish(new UserActivatedEvent(userId))
  }
}
 
// ─── Infraestructura ─────────────────────────────────────────────────────────
 
// Adaptador: implementa el puerto usando Prisma
export class PrismaUserRepository implements UserRepository {
  constructor(private readonly prisma: PrismaClient) {}
 
  async findById(id: string): Promise<User | null> {
    const record = await this.prisma.user.findUnique({ where: { id } })
    if (!record) return null
    return new User(record.id, record.email, record.isActive)
  }
 
  async save(user: User): Promise<void> {
    // mapear el dominio al esquema de Prisma
  }
}

Por qué esto cambia el testing

Con esta estructura, testear el caso de uso es trivial: no necesitas una base de datos real, no necesitas un servidor HTTP. Solo necesitas un mock del puerto:

describe("ActivateUserUseCase", () => {
  it("activa un usuario inactivo y publica el evento", async () => {
    const fakeUsers: UserRepository = {
      findById: async () => new User("1", "user@test.com", false),
      save: vi.fn()
    }
    const fakeEvents: EventPublisher = { publish: vi.fn() }
 
    const useCase = new ActivateUserUseCase(fakeUsers, fakeEvents)
    await useCase.execute("1")
 
    expect(fakeEvents.publish).toHaveBeenCalledWith(
      expect.objectContaining({ userId: "1" })
    )
  })
})

Cuándo aplicarla (y cuándo no)

La arquitectura hexagonal agrega valor cuando:

  • La lógica de negocio es compleja y cambia con frecuencia
  • El equipo necesita testear la lógica de negocio de forma rápida y confiable
  • Es probable que cambien los detalles de infraestructura (ORM, base de datos, framework)

No tiene sentido para:

  • CRUDs simples sin lógica de negocio real
  • Prototipos o MVPs donde la velocidad importa más que la mantenibilidad
  • Equipos pequeños donde la sobrecarga de capas supera el beneficio

La arquitectura hexagonal no es una bala de plata. Es una herramienta para un problema específico: el acoplamiento entre dominio e infraestructura. Aplícala donde ese problema existe.

Recibe artículos de Software