Cerramos el módulo anterior con una constatación: el catálogo GoF se escribió para objetos que conviven dentro de un mismo proceso, pero el software actual se organiza a una escala mayor. Antes de repartir PideYa por la red (eso llegará en las dos próximas lecciones), necesitamos subir un nivel de zoom dentro del propio monolito: cómo se organizan los objetos en capas, cómo se conecta el dominio con el mundo exterior y cómo se estructura el acceso a datos. Verás que los patrones arquitectónicos no sustituyen al GoF: lo contienen. Cada patrón de esta lección es, en el fondo, una intención GoF (o un principio SOLID) aplicada a la estructura global de la aplicación.

Contenido

  1. Arquitectura en capas: dónde vive cada patrón GoF de PideYa
  2. MVC y sus variantes (MVP, MVVM)
  3. Arquitectura hexagonal: puertos y adaptadores
  4. Inyección de dependencias y contenedores IoC
  5. Repository y Unit of Work
  6. CQRS y Event Sourcing: introducción conceptual

Arquitectura en capas

El patrón arquitectónico más veterano (ya aparece en POSA, que conocimos en la lección 01-02) consiste en organizar el código en capas horizontales, donde cada capa solo conoce a la inferior:

Capa Responsabilidad Patrones GoF de PideYa que viven en ella
Presentación UI, controladores web, API REST Command (panel con undo), Observer (refrescar la vista del pedido)
Aplicación Casos de uso, orquestación Facade (FachadaCheckout), Mediator (CentralReparto), Template Method (informes)
Dominio Reglas de negocio, entidades State (EstadoPedido), Strategy (envío), Builder (Pedido.Builder), Composite (carta), Chain (validación)
Infraestructura BD, red, servicios externos Adapter (AdaptadorPayPal), Proxy (caché), Abstract Factory (FabricaEspana/FabricaMexico)

Fíjate en que la tabla no es una clasificación nueva: es el mapa de todo lo que construimos en los módulos 2-4, colocado en su sitio. La regla de oro es la dirección de las dependencias: el dominio no debe importar nada de infraestructura. Si Pedido conociera a PasarelaRedsys, un cambio en la pasarela obligaría a recompilar el corazón del negocio.

flowchart TD
    P[Presentación] --> A[Aplicación]
    A --> D[Dominio]
    A --> I[Infraestructura]
    I -.implementa interfaces de.-> D

La flecha punteada es la clave y es pura inversión de dependencias (DIP), que vimos en 01-03: la infraestructura implementa interfaces definidas por el dominio, no al revés.

MVC y sus variantes

Dentro de la capa de presentación, el patrón dominante es MVC (Modelo-Vista-Controlador):

  • Modelo: el estado y las reglas (nuestro dominio: Pedido, EstadoPedido...).
  • Vista: lo que ve el usuario (plantillas HTML, pantalla de la app del repartidor).
  • Controlador: recibe la acción del usuario, invoca al modelo y elige la vista.

MVC es, esencialmente, Observer + Strategy + Composite trabajando juntos: la vista observa al modelo, el controlador es una estrategia de manejo de entrada y las vistas se componen de subvistas. No es casualidad: el GoF cita MVC como ejemplo en su introducción.

Sus variantes ajustan quién conoce a quién — las mencionamos sin desarrollarlas:

Variante Diferencia clave Dónde se ve
MVP El Presenter habla con la vista a través de una interfaz; la vista es pasiva UIs de escritorio, Android clásico
MVVM El ViewModel expone estado observable y la vista se enlaza por data binding Frontends reactivos, Jetpack Compose, frameworks JS

Arquitectura hexagonal: puertos y adaptadores

La arquitectura hexagonal (Alistair Cockburn) radicaliza la idea de las capas: el dominio en el centro, y todo lo demás (web, BD, pasarelas, colas) conectado mediante puertos y adaptadores.

  • Un puerto es una interfaz definida por el dominio: es DIP hecho arquitectura.
  • Un adaptador es una implementación concreta de ese puerto: es, literalmente, el patrón Adapter de 03-02 elevado a pieza arquitectónica.

En PideYa ya teníamos el ejemplo perfecto sin saberlo:

// PUERTO (vive en el dominio): el negocio define QUÉ necesita
public interface PasarelaPago {
    ResultadoCobro cobrar(Pedido pedido, DatosTarjeta tarjeta);
}

// ADAPTADORES (viven en infraestructura): CÓMO se consigue
public class AdaptadorRedsys implements PasarelaPago { /* API de Redsys */ }
public class AdaptadorPayPal implements PasarelaPago { /* el de 03-02 */ }
public class PasarelaEnMemoria implements PasarelaPago { /* para tests */ }

El dominio de PideYa cobra pedidos sin saber si detrás hay Redsys, PayPal o un doble de pruebas. Se distinguen dos tipos de puertos:

  • Puertos primarios (driving): por donde entran las peticiones (la interfaz que implementa FachadaCheckout — nuestra Facade era ya un puerto primario avant la lettre).
  • Puertos secundarios (driven): lo que el dominio necesita del exterior (PasarelaPago, RepositorioPedidos, Notificador).

Inyección de dependencias y contenedores IoC

Si el dominio solo conoce interfaces, alguien tiene que decidir qué implementación concreta se usa y pasarla. Eso es inyección de dependencias (DI): en lugar de que FachadaCheckout haga new AdaptadorRedsys(), recibe un PasarelaPago por constructor.

public class FachadaCheckout {
    private final PasarelaPago pasarela;
    private final Notificador notificador;

    // Las dependencias se INYECTAN: la fachada no sabe cuáles son
    public FachadaCheckout(PasarelaPago pasarela, Notificador notificador) {
        this.pasarela = pasarela;
        this.notificador = notificador;
    }
}

Un contenedor IoC (Spring, Guice, CDI) industrializa esto: escanea las clases, decide qué implementación satisface cada interfaz y construye el grafo de objetos completo. Visto desde el catálogo:

  • El contenedor es una Abstract Factory generalizada: la FabricaEspana de 02-04 creaba familias coherentes a mano; Spring lo hace por configuración.
  • Los beans singleton de Spring resuelven la intención del Singleton de 02-02 sin sus inconvenientes: una sola instancia, pero testeable e inyectable — la crítica que hicimos allí encuentra aquí su respuesta.

Repository y Unit of Work

Bajamos ahora a la frontera con la base de datos, con dos patrones del catálogo PoEAA de Fowler (presentado en 01-02).

Repository ofrece al dominio una colección aparente de objetos, ocultando el SQL:

// Puerto secundario: el dominio habla de pedidos, no de tablas
public interface RepositorioPedidos {
    Optional<Pedido> buscarPorId(String id);
    List<Pedido> pendientesDeReparto(Zona zona);
    void guardar(Pedido pedido);
}

La implementación (RepositorioPedidosJpa) vive en infraestructura. El repositorio combina intenciones conocidas: es un Adapter sobre la persistencia, a menudo devuelve resultados vía Iterator, y sus consultas complejas pueden construirse con un Builder (así funciona la Criteria API de JPA, como vimos en 05-03).

Unit of Work registra todos los objetos tocados durante una transacción de negocio y los persiste de una vez al confirmar. Si el checkout modifica el pedido, el stock y los puntos de fidelización, la Unit of Work garantiza que se escriban juntos o ninguno. En Hibernate ya lo usas sin saberlo: la Session/EntityManager es una Unit of Work que hace dirty checking de las entidades cargadas.

CQRS y Event Sourcing

Terminamos con dos patrones que preparan el terreno para los módulos siguientes. Solo la idea; su despliegue real es asunto de sistemas distribuidos.

CQRS (Command Query Responsibility Segregation) separa el modelo de escritura (comandos: RealizarPedido, CancelarPedido) del modelo de lectura (consultas: "pantalla de seguimiento", "listado de la cocina"). En PideYa lo motivó un dato real: por cada escritura de un pedido hay cientos de lecturas de su estado. Con CQRS, las lecturas atacan vistas desnormalizadas y baratas, y las escrituras pasan por todo el dominio (validaciones Chain, EstadoPedido...). ¿Te suena la mitad "command"? Es la intención del patrón Command de 04-03: peticiones cosificadas como objetos.

Event Sourcing va un paso más allá: en lugar de guardar el estado actual del pedido, guarda la secuencia de eventos que lo produjo:

PedidoCreado → LineaAnadida(pizza) → LineaAnadida(refresco) → PedidoConfirmado → PedidoCobrado

El estado actual se reconstruye reproduciendo los eventos. Reconoce las intenciones GoF:

  • Como Memento (04-07), captura el pasado para poder volver a él — pero guardando deltas (eventos) en vez de fotos completas.
  • Como Observer (04-08), cada evento persistido puede notificar a otros interesados: las vistas de lectura de CQRS se actualizan escuchando esos eventos.

Esa pareja CQRS + Event Sourcing es la puerta natural a las arquitecturas orientadas a eventos que veremos en 06-03.

Errores Comunes y Consejos

  • Capas de mentira: tener paquetes controller/service/repository pero con el dominio importando clases de infraestructura. Las capas se miden por las dependencias, no por los nombres de paquete. Consejo: usa ArchUnit para verificar en un test que dominio no importa infraestructura.
  • Hexagonal ceremonial: crear puerto + adaptador para todo, incluida lógica que jamás tendrá segunda implementación. Es la patternitis de 05-05 a escala arquitectónica. Crea el puerto cuando haya una frontera real (algo externo o algo que necesitas sustituir en tests).
  • Repository que filtra la tecnología: métodos como ejecutarHql(String) en la interfaz del repositorio destruyen su propósito. Si el dominio ve HQL, no hay abstracción.
  • Adoptar CQRS/Event Sourcing por moda: son patrones caros (dos modelos, consistencia diferida, versionado de eventos). Aplica el "espera hasta que duela" de 05-01: si un solo modelo CRUD te sirve, quédatelo.
  • Confundir DI con tener un framework: la inyección de dependencias es pasar colaboradores por constructor; puedes (y debes poder) hacerla a mano en un main. El contenedor solo automatiza el ensamblaje.

Ejercicios

  1. Clasifica en capas. Coloca cada clase de PideYa en su capa (presentación, aplicación, dominio o infraestructura) y justifica: EstadoPedido, AdaptadorPayPal, FachadaCheckout, ControladorPedidosRest, RepositorioPedidosJpa, Pedido.Builder.
  2. Diseña un puerto. PideYa necesita calcular tiempos estimados de entrega consultando un servicio externo de tráfico. Define el puerto (interfaz de dominio), un adaptador real y un adaptador falso para tests. Indica si es un puerto primario o secundario.
  3. Eventos de un pedido. Escribe la secuencia de eventos (Event Sourcing) que dejaría un pedido que se crea con dos platos, se confirma, se cobra, y al que el cliente cambia la dirección antes del reparto. ¿Cómo reconstruirías la dirección actual del pedido?

Soluciones

  1. EstadoPedido → dominio (reglas del ciclo de vida). AdaptadorPayPal → infraestructura (implementa el puerto PasarelaPago contra una API externa). FachadaCheckout → aplicación (orquesta un caso de uso completo). ControladorPedidosRest → presentación (traduce HTTP a llamadas de aplicación). RepositorioPedidosJpa → infraestructura (implementa el puerto RepositorioPedidos con JPA). Pedido.Builder → dominio (construcción del agregado con sus invariantes).
  2. Puerto secundario (el dominio necesita algo del exterior): public interface EstimadorEntrega { Duration estimar(Direccion origen, Direccion destino); }. Adaptador real: EstimadorGoogleMaps implements EstimadorEntrega, que llama a la API y traduce su respuesta. Adaptador de test: EstimadorFijo implements EstimadorEntrega que devuelve siempre Duration.ofMinutes(30). El dominio calcula promesas de entrega sin tocar red.
  3. PedidoCreado(id, cliente)LineaAnadida(plato1)LineaAnadida(plato2)PedidoConfirmado(direccionInicial)PedidoCobrado(importe)DireccionCambiada(nuevaDireccion). La dirección actual se obtiene reproduciendo los eventos en orden: la última escritura relevante (DireccionCambiada) gana. Nótese el bonus: el historial conserva que hubo un cambio de dirección, información que un modelo de estado actual habría perdido.

Conclusión

Hemos subido el zoom y descubierto que la arquitectura de PideYa está tejida con los mismos hilos del catálogo: las capas ordenan dónde vive cada patrón GoF, la hexagonal convierte DIP y Adapter en el plano del edificio, los contenedores IoC industrializan las fábricas, Repository y Unit of Work disciplinan la persistencia, y CQRS/Event Sourcing reinterpretan Command, Memento y Observer sobre el flujo de datos. Todo esto, todavía, dentro de un solo proceso desplegable. Pero PideYa sigue creciendo: el equipo de reparto quiere desplegar sin esperar al de pagos, y el monolito empieza a ser el cuello de botella. Toca partirlo — con sus propios patrones y sus propios peligros — en Patrones de Diseño en Microservicios.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados