El modelo en capas tradicional fue un paso enorme para ordenar sistemas empresariales, pero introduce restricciones difíciles de sostener cuando el negocio exige rapidez y flexibilidad. Las dependencias verticales, el acoplamiento entre la lógica y las herramientas de infraestructura, y la dificultad de testear en aislamiento impactan negativamente en la capacidad de evolución del software.
En este tema analizamos las principales limitaciones del enfoque N-Tier, mostrando cómo la arquitectura hexagonal surge como una respuesta práctica a estos dolores. Revisaremos la dependencia de frameworks y bases de datos, las barreras para realizar pruebas unitarias sin infraestructura pesada y el efecto dominó que se produce ante cambios en puntos críticos. Cerraremos con un ejemplo realista que ilustra los síntomas más comunes.
La arquitectura en capas suele organizar el código de forma que la capa de negocio utilice directamente APIs de frameworks. Cuando la lógica llama a servicios del Spring Framework o invoca repositorios de Hibernate, se establece una dependencia rígida con la infraestructura elegida. Cambiar de ORM o adoptar una estrategia diferente de persistencia implica modificar clases centrales, reescribir anotaciones y revalidar reglas funcionales.
Algo similar ocurre con la base de datos. Si el modelo de dominio se diseña pensando en estructuras propias de PostgreSQL, cualquier intento de migrar hacia otra tecnología con requisitos diferentes obliga a revisar la totalidad de la lógica. Esta dependencia tecnológica limita la capacidad de experimentar con nuevas opciones, condiciona la negociación con proveedores y complica los planes de contingencia.
Al tener la lógica entremezclada con APIs de terceros, preparar pruebas unitarias exige levantar contextos completos. Probar un servicio que usa directamente un repositorio JPA requiere inicializar el contenedor de Spring, levantar una base embebida y configurar transacciones, incluso si la regla de negocio es simple.
El resultado es que los equipos dependen de pruebas integrales lentas o manuales, y pierden la retroalimentación rápida que ofrecen los test unitarios. Incluso frameworks pensados para facilitar pruebas terminan siendo obstáculos cuando todo el código depende de configuraciones externas.
Para ilustrarlo, basta observar la configuración típica de una prueba:
@SpringBootTest
@AutoConfigureTestDatabase
class ServicioFacturacionTest {
@Autowired
private ServicioFacturacion servicio;
@Test
void registraFacturaValida() {
FacturaDTO dto = new FacturaDTO("ACME", BigDecimal.TEN);
servicio.registrar(dto);
assertTrue(servicio.existeFactura(dto.numero()));
}
}
El test se ve obligado a cargar el contexto completo, aun cuando la lógica de facturación podría evaluarse con objetos de dominio puros. Esta rigidez hace que los equipos posterguen las pruebas, lo cual reduce la calidad y aumenta el tiempo de entrega.
En un modelo estricto de capas, las dependencias suelen fluir desde la interfaz hacia los datos, pero rara vez se controlan las dependencias internas de la capa de negocio. Por eso, un cambio en la base de datos (como renombrar una columna o migrar de SQL a un motor NoSQL) se propaga de manera transversal. Las entidades de dominio dejan de compilar, los servicios exponen nuevas excepciones y se ven obligados a contemplar reglas orientadas a la infraestructura.
La capa de presentación también arrastra modificaciones generalizadas. Cambiar una interfaz web por una API pública implica adaptar controladores, DTOs, validaciones y, en muchos casos, reescribir el tratamiento de errores dentro de la lógica de negocio. Sin fronteras claras, se pierde la capacidad de ofrecer múltiples canales (web, móvil, integraciones externas) sin reestructurar el núcleo de la aplicación.
El siguiente ejemplo en Java muestra un servicio de pedidos típico en una arquitectura N-Tier. La clase mezcla reglas funcionales con detalles de infraestructura: anotaciones de Spring, repositorios JPA, transacciones y objetos de transporte específicos de la capa de presentación.
@Service
@Transactional
public class PedidoService {
private final PedidoRepository repository;
private final MailSender mailSender;
public PedidoService(PedidoRepository repository, MailSender mailSender) {
this.repository = repository;
this.mailSender = mailSender;
}
public PedidoDTO crear(PedidoDTO dto) {
PedidoEntity entity = mapear(dto);
validar(entity);
repository.save(entity); // Depende de JpaRepository
mailSender.send(dto.email(), "Pedido creado", "Número " + entity.getNumero());
return mapear(entity);
}
private PedidoEntity mapear(PedidoDTO dto) {
PedidoEntity entity = new PedidoEntity();
entity.setNumero(dto.numero());
entity.setMonto(dto.monto());
entity.setFecha(LocalDate.now());
return entity;
}
private void validar(PedidoEntity entity) {
if (repository.existsByNumero(entity.getNumero())) {
throw new IllegalStateException("El pedido ya existe");
}
}
}
La clase depende directamente de JpaRepository, del contenedor de Spring, del mecanismo de envío de correos y de DTOs de la capa superior. Cualquier cambio en la base de datos, el proveedor de correo o la interfaz de usuario exige modificar el servicio. Además, testearlo implica levantar el contexto completo para que las anotaciones y la inyección funcionen.
Este patrón se repite en muchos sistemas N-Tier: la lógica de negocio queda cautiva del framework, y los casos de uso terminan siendo difíciles de mantener. La arquitectura hexagonal propone separar estos elementos, definiendo puertos para cada necesidad (persistencia, notificaciones, interfaces de usuario) y adaptadores específicos por tecnología.
Comprender las limitaciones del modelo en capas nos prepara para abrazar alternativas que prioricen la independencia del dominio. En el próximo tema exploraremos los conceptos fundamentales de la arquitectura hexagonal, incluyendo la definición de puertos y adaptadores que permiten romper con el acoplamiento descrito aquí.