Arquitectura Hexagonal

La arquitectura hexagonal es una forma práctica de aplicar Clean Architecture. La aplicación define puertos; la infraestructura aporta adaptadores.

El objetivo es que la lógica importante no dependa de cómo entra una petición ni de dónde se guardan los datos.

Capas

Las ventajas es tener código desacoplado y separar lógica de negocio de los detalles de implementación.

Presentación

Esta capa no aparece en el diagrama, pero iría por fuera de la Infraestructura. Es la capa donde viven las vistas, componentes y la UI en general.

Infraestructura

Capa de entrada/salida. Llamadas a BD, fetching de datos…

Aplicación

Donde viven los casos de uso. Aquí irían los métodos que hacen cosas

Dominio

El dominio es donde están las entidades, repositorios e interfaces. User, Event, Product…

Regla de dependencias

Basándonos en estas capas, las dependencias van de fuera a dentro. De Infra podemos importar de App y Dominio, pero de Dominio no podemos importar de fuera.

El dominio define lo que necesita. La infraestructura decide cómo hacerlo.

Puertos y adaptadores

Un puerto es una interfaz que expresa una capacidad:

  • Buscar códigos postales.
  • Guardar un usuario.
  • Enviar un email.
  • Leer un archivo.

Un adaptador es la implementación concreta:

  • Repositorio con Prisma.
  • Cliente HTTP.
  • SDK de Stripe.
  • Servicio SMTP.

La aplicación habla con puertos. La infraestructura aporta adaptadores.

Uso en NextJS

Es una movida pero lo he conseguido sacar y me gusta.

La estructura sería esta:

Dominio

Aquí establecemos interfaces y schemas.

import { PostalCode } from "./schemas/postalCode"
 
  
 
export interface PostalCodeRepository {
	search(postalCode: string): Promise<PostalCode[]>
	validate(postalCode: string): Promise<PostalCode>
}
import { z } from "zod"
 
export const PostalCodeSchema = z.object({
	code: z.string(),
})
 
export type PostalCode = z.infer<typeof PostalCodeSchema>

Application

Aquí establecemos los ‘casos de uso’. Es decir, los métodos que aplican la lógica de negocio, como searchPostalCode o isValidPostalCode por ejemplo.

En este ejemplo concreto usamos una técnica llamada currying que consiste en despiezar una función que acepta múltiples argumentos en una serie de callbacks en las que cada una acepta un único argumento.

import { PostalCodeRepository } from "../domain/PostalCodeRepository"
 
export function searchPostalCode(postalCodeRepository: PostalCodeRepository) {
	return async function (postalCode: string) {
		return await postalCodeRepository.search(postalCode)
	}
}

Infrastructure

Aquí es donde van los ‘detalles de implementación’. Es decir, donde añadimos la implementación técnica en la que conectamos a una BD, hacemos un fetch y gestionamos entradas y salidas a través de puertos y adaptadores.

Testing

La ventaja práctica es que cada pieza se puede probar en el nivel adecuado:

  • Dominio: tests unitarios puros, sin mocks ni framework.
  • Application: tests de casos de uso con puertos fake o in-memory.
  • Infrastructure: tests de integración contra base de datos, APIs o filesystem.
  • Presentation: tests de interacción o E2E para flujos críticos.

Ver Testing en Clean Architecture.