Ports and Adapters
Idea central
Ports and Adapters protege la lógica de una aplicación de los detalles de sus interacciones externas. Un port expresa un contrato significativo en la frontera; un adapter traduce una tecnología concreta a ese contrato o lo implementa. Cockburn presenta esta aproximación también como Hexagonal Architecture en su formulación original.
hexagonal-architecture conserva la perspectiva histórica del nombre. Este documento es el canonical de la misma aproximación, no una arquitectura competidora.
Inside, outside y frontera
El interior contiene decisiones que queremos expresar con el lenguaje de la aplicación. El exterior incluye UI, HTTP, bases de datos, archivos, brokers y servicios de terceros. La separación es conceptual: un adaptador puede ejecutarse en el mismo proceso que el núcleo.
Una frontera útil permite describir qué pide o entrega la aplicación sin introducir tipos específicos del framework externo. AWS explica el papel de ports y adapters al aislar lógica de aplicación y dependencias tecnológicas en su guía de arquitectura hexagonal.
No toda aplicación necesita aislar cada llamada de biblioteca. El objetivo es proteger decisiones cuyo acoplamiento dificulta cambio o pruebas. Dibujar un adaptador para cada función trivial puede multiplicar contratos sin aislar una incertidumbre relevante.
Quién inicia la interacción
Un adaptador driving / primary convierte una interacción que inicia un actor externo en una operación del núcleo: una pantalla, un endpoint o un consumidor de mensajes pueden solicitar una aprobación.
Un adaptador driven / secondary satisface una necesidad iniciada por el núcleo: guardar una decisión, publicar un resultado o consultar un proveedor. Un broker puede participar en ambos papeles, como entrada y como salida. Las categorías describen la conversación, no una clasificación fija de tecnologías.
En este ejemplo propio, el núcleo ofrece SolicitarAprobación y necesita RegistrarDecisión y NotificarResultado. Un controlador HTTP y un consumidor del broker invocan el primer contrato. Un adaptador SQL y otro de mensajería satisfacen los contratos de salida. Los actores remotos no necesitan conocer la organización interna de clases.
Port no significa interface keyword
El contrato debe precisar intención, datos, resultados y comportamiento esperado. interfaces desarrolla la superficie expuesta y sus obligaciones, más allá de una keyword. Puede materializarse como interface, protocol, función, contrato de mensaje o API, según lenguaje y frontera. Llamar IApprovalRepository a una clase no prueba que exista un buen port.
RegistrarDecisión puede especificar unicidad de una decisión y cómo comunicar un conflicto. Una interfaz con métodos genéricos Get/Save que expone objetos del ORM puede seguir acoplando el núcleo a persistencia. El nombre del tipo importa menos que las obligaciones que permite expresar.
Adapter aquí designa un rol arquitectónico de frontera. adapter-pattern distingue ese rol del GoF Adapter, cuya intención es adaptar una interfaz incompatible: un HTTP, database o message broker adapter no necesita implementar necesariamente esa colaboración de diseño.
El contrato también necesita una decisión sobre errores. La aplicación puede reconocer «conflicto de versión» o «dependencia temporalmente indisponible» sin interpretar códigos propietarios de un driver. El adaptador traduce los fallos que entiende; no debería ocultar resultados inciertos fingiendo éxito.
Dependencias y flujo de control
Dependencias de código en este ejemplo:
HTTP adapter → Application entry contract
SQL adapter → Application persistence contract
Flujo de una ejecución:
HTTP adapter → Use case → SQL adapter → DatabaseInvocar un adaptador hacia afuera no obliga al núcleo a importarlo. El caso de uso conoce el contrato interior; un punto externo de composición suministra la implementación. Separar ensamblaje y uso es el problema desarrollado por Fowler en Dependency Injection. Un contenedor es una herramienta posible, no una condición arquitectónica.
El núcleo que instancia directamente el adaptador SQL conserva una dependencia concreta aunque el método llamado pertenezca a una interfaz. También puede filtrar infraestructura mediante decoradores obligatorios, tipos de configuración o tipos de datos. La revisión necesita mirar imports y tipos, no solamente llamadas.
Pruebas y sustitución de tecnología
En nuestro ejemplo, un adaptador en memoria permite probar si una solicitud rechazada evita publicar un resultado. Otra prueba puede provocar un conflicto al guardar y comprobar qué respuesta produce el caso de uso. Son pruebas de comportamiento del núcleo con colaboraciones controladas. testability permite evaluar preparación, control y evidencia, sin confundir esa separación con mockear todo.
Después deben probarse los adaptadores reales: mapping, constraints, transacciones y envío al broker. La operación que funciona con memoria puede fallar con concurrencia o timeouts. Las garantías de delivery-guarantees y idempotency pertenecen a esas interacciones; la separación estructural no las crea.
Cambiar SQL por otra tecnología requiere verificar las mismas obligaciones, incluyendo atomicidad y conflictos. Un port reduce el área de código expuesta al cambio, pero no hace equivalentes dos sistemas de almacenamiento. Una abstracción demasiado débil puede aplazar la incompatibilidad hasta producción. modifiability analiza costo y propagación de esa sustitución; evolvability investiga si la frontera permite seguir acomodando cambios con el tiempo.
interoperability permite evaluar si el mapping conserva significado y comportamiento entre participantes; portability examina adaptación a otra plataforma con obligaciones preservadas. Un adaptador real puede contribuir a ambos escenarios. Ni implementar un port ni sustituirlo en un test demuestra equivalencia semántica o traslado completo.
Fuerzas y trade-offs
La separación ayuda cuando hay múltiples entradas, integraciones cambiantes o lógica que conviene ejercitar independientemente. Su costo incluye contratos, traducciones y pruebas de compatibilidad. Un port genérico para «cualquier almacenamiento posible» puede terminar sin expresar garantías útiles; uno excesivamente tecnológico reproduce el acoplamiento.
Una frontera de aplicación tampoco garantiza una frontera de seguridad. Autenticación del canal y autorización del caso de uso requieren responsabilidades explícitas. Un segundo adaptador de entrada no debería permitir saltarse una regla porque el primero la implementaba accidentalmente.
Relaciones y composición
onion-architecture enfatiza capas alrededor del dominio y clean-architecture organiza niveles de políticas con una Dependency Rule. Comparten preocupaciones, pero sus modelos aportan vocabulario adicional; internal-structure permite compararlos sin declarar identidad completa.
Puede aplicarse dentro de un monolith o de un servicio de microservices. También pueden organizarse los casos de uso como vertical-slice-architecture. Ninguna combinación evita el trabajo de definir límites: si todas las slices importan drivers externos, no se conserva el núcleo protegido por el mero uso de ambos nombres.