7. Limitaciones y problemas comunes

La arquitectura en capas ofrece una estructura sólida, pero no está exenta de riesgos. Mal aplicada puede generar cuellos de botella, acoplamientos complejos y dificultades al escalar en entornos distribuidos. En esta sección revisamos los errores más frecuentes y las señales que indican que conviene evolucionar hacia estilos más flexibles.

7.1 Acoplamiento entre capas mal diseñadas

Uno de los problemas recurrentes es la dependencia cruzada entre capas que deberían ser independientes. Cuando la capa de presentación accede directamente a la base de datos o la capa de persistencia conoce detalles de la interfaz, el modelo pierde coherencia.

  • Dependencias ascendentes: repositorios que invocan controladores o servicios de UI, rompiendo la dirección del flujo.
  • Filtros en la capa incorrecta: interfaces que implementan lógica de negocio porque el dominio no se actualizó.
  • Entidades anémicas: modelos que solo transportan datos sin reglas, obligando a duplicar validaciones en todas las capas.

Un indicador claro es la presencia de imports cíclicos o paquetes que dependen mutuamente. La siguiente situación muestra el problema y su refactor:

// Problema: la capa de persistencia importa clases de la API
package com.example.coworking.infrastructure.persistence;

import com.example.coworking.api.dto.ReservaRequest; // Dependencia indeseada

public class ReservaRepositorioJdbc {
    public void guardar(ReservaRequest request) { /* ... */ }
}

// Corrección: utilizar el modelo de dominio
package com.example.coworking.infrastructure.persistence;

import com.example.coworking.domain.model.Reserva;

public class ReservaRepositorioJdbc {
    public void guardar(Reserva reserva) { /* ... */ }
}

7.2 Pérdida de rendimiento por exceso de pasos intermedios

Agregar capas sin un propósito claro puede introducir latencias innecesarias. Cada paso adicional implica conversión de datos, validaciones y potenciales viajes a la red.

  • Transformaciones redundantes de DTO a entidades en distintas capas.
  • Cadenas de servicios que solo delegan llamadas sin aportar lógica.
  • Accesos a datos que se duplican porque no existe una estrategia de caching coherente.

Es recomendable medir tiempos con herramientas de profiling y simplificar los pasos que no agregan valor. En algunos casos una capa puede volverse un simple adaptador fino:

public class ConsultaDisponibilidadService {

    private final CalendarioSala calendario;

    public ConsultaDisponibilidadService(CalendarioSala calendario) {
        this.calendario = calendario;
    }

    public DisponibilidadDTO verificar(String salaId, LocalDateTime desde, LocalDateTime hasta) {
        // Delegación directa: no agrega reglas, solo transforma datos.
        // Si la conversión es trivial, conviene absorberla en la capa superior.
        return DisponibilidadDTO.from(calendario.comprobar(salaId, desde, hasta));
    }
}

7.3 Dificultades en arquitecturas distribuidas

En entornos distribuidos, mantener sincronizadas las capas se vuelve más complejo. Los casos de uso pueden necesitar datos de múltiples microservicios y la latencia en red agrava el problema.

  • Consistencia eventual: las capas dependen de datos que llegan con retraso, afectando reglas del dominio.
  • Transacciones distribuidas: coordinar commits entre servicios genera bloqueos y requiere patrones como sagas.
  • Observabilidad fragmentada: cada capa se despliega en nodos distintos y es difícil seguir el rastro de una petición.

Una solución común es introducir un service mesh y centralizar la trazabilidad, pero también conviene revisar si la división de capas sigue aportando valor en ese contexto.

7.4 Cuándo migrar a modelos más flexibles

La arquitectura en capas es un excelente punto de partida, pero hay escenarios donde conviene adoptar estilos como la arquitectura hexagonal o los microservicios:

  • Dominio con reglas heterogéneas: múltiples subdominios que evolucionan a ritmos distintos pueden beneficiarse de arquitecturas como hexagonal o clean.
  • Necesidad de plugins: cuando se requiere incorporar adaptadores sin modificar el núcleo, la arquitectura hexagonal simplifica la extensión.
  • Escalado independiente: si cada capa tiene patrones de consumo muy diferentes, dividir en microservicios especializados evita sobrecargar el resto.

La migración no debe ser abrupta. Muchos equipos adoptan estrategias evolutivas:

  1. Identificar un módulo con cambios frecuentes y encapsularlo como servicio interno.
  2. Introducir puertos-interfaces en el dominio para permitir adaptadores alternativos.
  3. Extraer gradualmente capas hacia servicios dedicados, manteniendo contratos estables.
public interface ReservaFacade {
    ReservaDTO crear(ReservaCommand command);
}

// Adaptador interno inicial (monolito con capas)
public class ReservaFacadeMonolite implements ReservaFacade {
    private final ReservarSala reservarSala;
    // ...
}

// Adaptador remoto posterior (microservicio)
public class ReservaFacadeClient implements ReservaFacade {
    private final HttpClient httpClient;
    // ...
}

Arquitecturas como la arquitectura hexagonal permiten mantener el dominio estable mientras los adaptadores evolucionan.

7.5 Próximos pasos

Ser conscientes de las limitaciones ayuda a aplicar controles y planear estrategias de evolución. En el siguiente tema revisaremos buenas prácticas y patrones que refuerzan la arquitectura en capas, mitigando varios de los riesgos expuestos aquí.