Si alguna vez has intentado probar una regla de negocio enterrada dentro de un controlador, o has visto cómo una actualización del framework rompe la mitad de tu aplicación, ya conoces el problema que resuelve la arquitectura hexagonal.
Tu lógica de negocio — el código que hace valioso a tu producto — no debería depender de tu framework web, tu base de datos ni tu cola de mensajes. Y sin embargo, en la mayoría de los codebases, lo hace. La arquitectura hexagonal (también conocida como Ports and Adapters o "Puertos y Adaptadores") es un patrón que invierte esa dependencia, y es la base de cómo construimos aplicaciones en HexagonalGuru.
El Problema: Acoplamiento al Framework
En una aplicación MVC típica, el flujo va de la petición HTTP al controlador, del controlador al servicio y del servicio a la base de datos. El controlador conoce HTTP. El servicio suele conocer el ORM. Las reglas de negocio están repartidas entre las tres capas.
Las consecuencias son predecibles:
- Probar el código es doloroso. No puedes verificar una regla de negocio sin arrancar el framework, la base de datos y media inyección de dependencias.
- Actualizar el framework es arriesgado. Tu lógica de dominio está enredada con anotaciones, clases base y convenciones propias del framework.
- Cambiar de infraestructura es caro. Pasar de MySQL a PostgreSQL, o de REST a una cola de mensajes, implica reescribir código de negocio, no solo la capa de conexión.
La Idea Central
La arquitectura hexagonal, acuñada por Alistair Cockburn en 2005, propone una regla simple:
El núcleo de la aplicación define las interfaces. El mundo exterior se adapta a ellas — nunca al revés.
En lugar de que tu lógica de negocio llame a la base de datos, tu lógica de negocio declara "necesito algo que pueda guardar un pedido" (un puerto). La capa de datos proporciona una implementación (un adaptador). La dependencia apunta hacia dentro:
Los Tres Bloques Constructivos
1. Dominio (el centro)
Lógica de negocio pura: entidades, value objects y servicios de dominio. Sin imports del framework. Sin anotaciones de base de datos. Solo las reglas de tu negocio, expresadas en código.
2. Puertos (los contratos)
Interfaces definidas por el núcleo de la aplicación:
- Puertos de entrada (driving): lo que el mundo exterior puede pedirle a la aplicación. En la práctica, son tus clases de casos de uso o interfaces, p. ej.
RegistrarUsuario,RealizarPedido. - Puertos de salida (driven): lo que la aplicación necesita del mundo exterior, p. ej.
RepositorioUsuarios,PasarelaDePago,Mailer.
3. Adaptadores (los bordes)
Implementaciones que conectan los puertos con la tecnología real:
- Adaptadores de entrada llaman a los puertos de entrada: un controlador HTTP, un comando CLI, un consumidor de mensajes.
- Adaptadores de salida implementan los puertos de salida: un repositorio de Doctrine, un cliente de Stripe, un mailer SMTP.
Los adaptadores son reemplazables por diseño. ¿Cambias MySQL por MongoDB? Escribes un adaptador nuevo — el núcleo no cambia.
Un Ejemplo Mínimo
Supongamos que necesitamos registrar usuarios. El puerto de entrada es un caso de uso:
interface RegistrarUsuario
{
public function __invoke(string $email, string $password): void;
}
El puerto de salida es lo que el caso de uso necesita:
interface RepositorioUsuarios
{
public function guardar(Usuario $usuario): void;
public function buscarPorEmail(string $email): ?Usuario;
}
El caso de uso vive en el núcleo y no conoce nada de frameworks:
final class RegistrarUsuarioUseCase implements RegistrarUsuario
{
public function __construct(
private RepositorioUsuarios $usuarios,
private Mailer $mailer,
) {}
public function __invoke(string $email, string $password): void
{
if ($this->usuarios->buscarPorEmail($email)) {
throw new UsuarioYaExiste($email);
}
$usuario = Usuario::registrar($email, $password);
$this->usuarios->guardar($usuario);
$this->mailer->enviarBienvenida($usuario->email());
}
}
El adaptador de Doctrine implementa el puerto:
final class DoctrineRepositorioUsuarios implements RepositorioUsuarios
{
public function __construct(private EntityManager $em) {}
public function guardar(Usuario $usuario): void
{
$this->em->persist($usuario);
$this->em->flush();
}
// ...
}
Y el adaptador HTTP es una capa fina:
final class RegistrarUsuarioController
{
public function __construct(private RegistrarUsuario $registrarUsuario) {}
public function __invoke(Request $request): Response
{
($this->registrarUsuario)($request->email, $request->password);
return new Response(status: 201);
}
}
// Fíjate: la regla de negocio ("un usuario no puede
// registrarse dos veces") se prueba de forma aislada, con un
// repositorio en memoria — sin capa HTTP, sin base de datos,
// sin arrancar el framework.
Qué Ganas
- Tests rápidos y aislados. Prueba reglas de negocio en milisegundos con adaptadores en memoria. Los tests de integración solo cubren los adaptadores.
- Independencia del framework. Laravel, Symfony o el próximo framework de moda se convierten en un detalle. Las actualizaciones tocan los bordes, no el núcleo.
- Cambios de infraestructura reales. Base de datos, cola, proveedor de email — todo se reduce a cambiar adaptadores.
- Límites claros. Los desarrolladores nuevos encuentran las reglas de negocio al instante; no tienen que reconstruirlas a partir de controladores.
- Alineada con DDD y Clean Architecture. La arquitectura hexagonal encaja de forma natural con Domain-Driven Design y es un subconjunto pragmático de las ideas de Clean Architecture.
Cuándo No Usarla
Somos honestos con esto: la arquitectura hexagonal no es gratis. Añade indirección, más interfaces y más archivos. Evítala (o úsala parcialmente) cuando:
- Aplicaciones dominadas por CRUD con poca lógica de negocio — una app MVC estándar es suficiente.
- MVPs y prototipos donde la velocidad de iteración importa más que la estructura a largo plazo.
- Equipos muy pequeños en proyectos de corta vida.
Una regla práctica: cuanta más lógica de negocio tengas y más tiempo viva el proyecto, más rentable será la arquitectura hexagonal.
Errores Comunes
- Filtrar código del framework al núcleo. Si tu entidad extiende
Modelo está llena de anotaciones del ORM, el framework vuelve a mandar. - Un puerto por adaptador sin necesidad. No crees
RepositorioUsuariosMySQLyRepositorioUsuariossi jamás tendrás dos implementaciones — usa la interfaz donde aporta (testabilidad, límites externos). - Casos de uso anémicos. Un caso de uso que solo reenvía llamadas al repositorio no aporta nada. Los casos de uso deben orquestar la lógica de dominio.
- Saltarse los tests. Si no escribes tests aislados para el núcleo, estás pagando el coste del patrón sin cobrar el beneficio.
En Resumen
La arquitectura hexagonal no se trata de dibujar hexágonos — se trata de quién depende de quién. Haz que tu tecnología dependa de tus reglas de negocio, y tu software será testeable, reemplazable y estará construido para durar.
Si tu código sufre los síntomas que hemos descrito — lógica imposible de probar, actualizaciones arriesgadas, cambios de infraestructura caros — una auditoría de software suele ser el mejor primer paso. Mapeamos tu deuda técnica y te entregamos un informe priorizado y accionable. Solicita una consultoría gratuita →
¿Quieres profundizar? Lee el siguiente artículo de la serie: Domain-Driven Design explicado con un ejemplo real.