Nivel 3 · 25 min
Arquitectura Hexagonal
La Arquitectura Hexagonal (Ports & Adapters) de Alistair Cockburn aísla el núcleo de la aplicación de cualquier infraestructura mediante puertos (interfaces) y adaptadores (implementaciones). El objetivo: el dominio es completamente testeable sin infraestructura.
Puertos y Adaptadores
Los Puertos son interfaces definidas por el núcleo de la aplicación. Los Driving Ports (puertos de entrada) aceptan input: HTTP, CLI, tests. Los Driven Ports (puertos de salida) definen servicios externos que el núcleo necesita: bases de datos, email, sistemas externos. Los Adaptadores son las implementaciones concretas.
Aislamiento del Dominio
El hexágono central no tiene dependencias en infraestructura. Ningún import de Spring, Hibernate, Jackson, o cualquier librería de terceros. Solo define interfaces que la infraestructura debe implementar. El hexágono puede ejecutarse en cualquier contexto: test unitario, CLI, o con adaptadores reales.
Beneficio de Testeabilidad
Podés reemplazar cualquier adaptador real con un test double: stub de base de datos en memoria, mock de email, fake de servicio externo. Los tests del dominio corren en milisegundos sin levantar Spring, sin base de datos real, sin red.
Code example
// Driven Port: definido por el dominio
interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
}
// Driving Port
interface OrderService {
OrderResult placeOrder(PlaceOrderCommand cmd);
}
// Adapter: implementa el driven port
class PostgresOrderRepository implements OrderRepository {
private final JpaOrderRepository jpa;
@Override
public Optional<Order> findById(OrderId id) {
return jpa.findById(id.value()).map(this::toDomain);
}
}
// Test: usa un fake adapter — corre en ms
class PlaceOrderTest {
@Test
void placeOrder_withSufficientInventory_createsOrder() {
var orders = new InMemoryOrderRepository();
var inventory = new StubInventoryService(available: true);
var service = new OrderServiceImpl(orders, inventory);
var result = service.placeOrder(new PlaceOrderCommand(...));
assertThat(result.status()).isEqualTo(PENDING);
}
}