← dev-notes
DDD · PARTE 3 DE 4

Persistencia
& Patrones Avanzados

Repositories, Domain Events, Event Sourcing y CQRS — cómo el dominio sobrevive fuera de la memoria

DDD · P3
🗄 Repositories 📢 Domain Events ⚡ Event Sourcing ↔ CQRS
01 Repositories — la abstracción de persistencia del dominio
🗄
PATRÓN
Repository
Abstracción de BDColección en memoriaAgnóstico
Abstrae el mecanismo de persistencia detrás de una interfaz que semánticamente parece una colección de objetos de dominio en memoria. El dominio no sabe si detrás hay Postgres, MongoDB o un array.
Regla fundamental: Solo existen Repositories para Aggregate Roots, nunca para entidades internas. No existe OrderItemRepository — si necesitas un OrderItem, lo obtienes a través del OrderRepository.
La interfaz del Repository vive en el dominio. La implementación (JPA, MongoDB) vive en infraestructura. Esto es la Inversión de Dependencias aplicada a la persistencia.
domain/port/out/OrderRepository.java — interfaz en el dominioJAVA
// Vive en domain/ — sin imports de JPA, JDBC ni Spring
public interface OrderRepository {
    Order          save(Order order);
    Optional<Order> findById(OrderId id);
    List<Order>    findByCustomer(CustomerId id);
    void           delete(OrderId id);
}

// NO existe: OrderItemRepository ← anti-pattern
✅ CORRECTO
orderRepo.findById(id)
.addItem(item)
orderRepo.save(order)
❌ INCORRECTO
orderItemRepo
.findById(itemId)
.setQuantity(3) ← ¡NO!
02 Domain Events — comunicación asíncrona entre contextos
Flujo completo de un Domain Event — desde el Aggregate hasta el consumidor
01
🌳
Aggregate
order.confirm()
registra
02
📢
Domain Event
OrderConfirmed
Application
Service publica
03
📡
Event Bus
Kafka / SQS
consume
04
📧
Notificaciones
BC consumidor
consume
04
🚚
Logística
BC consumidor
consume
04
📊
Analytics
BC consumidor
📢
CONCEPTO
Domain Event
PasadoInmutableDesacoplamiento
Algo que ocurrió en el dominio y que otros pueden querer saber. Siempre en tiempo pasado. Inmutable. El Aggregate que lo generó no conoce a sus suscriptores.
Convención de nombres: OrderConfirmed, PaymentProcessed, UserRegistered. Verbo en participio pasado. Siempre en el lenguaje del dominio, nunca técnico.
Los eventos viajan entre Bounded Contexts. Cada BC define su propio modelo de cómo interpreta el evento — gracias al Anti-Corruption Layer.
Domain Event + registro en el AggregateJAVA
// Evento — inmutable, en pasado
public record OrderConfirmed(
    OrderId orderId,
    CustomerId customerId,
    Money total,
    Instant occurredAt
) implements DomainEvent {}

// En el Aggregate — registra, no publica
public void confirm() {
    this.status = CONFIRMED;
    registerEvent(new OrderConfirmed(
        id, customerId, total, Instant.now()
    ));
}
// El App Service lo publica después de save()
⚠️
PATRÓN
Transactional Outbox
GarantíaAt-least-onceConsistencia
El problema: si persistes el Aggregate y luego publicas el evento, pueden fallar por separado y perder el evento. Solución: guardar el evento en la misma transacción BD, en una tabla "outbox". Un proceso aparte lo publica al broker.
Flujo: save(aggregate) + insert(event en outbox) → misma TX → publicador lee outbox → publica a Kafka → marca como publicado.
Patrón que demuestra conocimiento senior. Si el entrevistador pregunta "¿qué pasa si falla entre save() y publish()?", esta es la respuesta.
03 Event Sourcing — el estado como secuencia de eventos
📜
PATRÓN AVANZADO
Event Sourcing
Append-onlyEvent StoreReplayableAudit log
En lugar de guardar el estado actual del Aggregate en la BD, guardas la secuencia de eventos que lo llevaron a ese estado. El estado actual se reconstruye reproduciendo los eventos desde el inicio (o desde un snapshot).
BD convencional: tabla orders con columna status = "SHIPPED"

Event Sourcing: Event Store con OrderCreated → ItemAdded → OrderConfirmed → OrderShipped. El estado es la suma de esos eventos.
VENTAJAS
✅ Audit log completo gratis
✅ Puedes reconstruir estado en cualquier punto del tiempo
✅ Los Domain Events son ciudadanos de primera clase
✅ Fácil integración con CQRS
DESVENTAJAS
❌ Complejidad operacional alta
❌ Las queries directas se complican (necesitas proyecciones)
❌ Evolución del schema de eventos es delicada
❌ No usar para dominios simples
⚠️
Cuándo NO usar Event Sourcing
Event Sourcing agrega complejidad significativa. Solo justifica en dominios donde el historial de cambios es un requerimiento de negocio (finanzas, auditoría médica, sistemas legales) o donde necesitas replayability. Para la mayoría de los sistemas, Domain Events sin Event Sourcing es suficiente.
04 CQRS — separar el modelo de escritura del de lectura
Command Query Responsibility Segregation — flujo completo
WRITE SIDE — Commands
Modelo de escritura
Recibe Commands: intenciones de cambiar el estado (CreateOrder, ConfirmPayment)
Valida el comando, ejecuta la lógica en los Aggregates
Persiste el Aggregate via Repository y publica Domain Events
No retorna datos (o solo el ID del recurso creado)
Write DB
Normalizada, optimizada para escritura consistente (ACID). PostgreSQL, MySQL.
Domain
Events
sincronizan
READ SIDE — Queries
Modelo de lectura
Recibe Queries: peticiones de datos (GetOrderSummary, ListOrdersByCustomer)
Lee desde un modelo desnormalizado optimizado para la pantalla (Read Model)
No pasa por el Aggregate ni el Repository de escritura
Retorna DTOs / ViewModels directamente a la capa de presentación
Read DB
Desnormalizada, vistas materializadas o proyecciones. Redis, Elasticsearch, MongoDB.
¿CÓMO SE SINCRONIZAN? — Event Handlers / Projectors
Cuando el Write Side persiste un Aggregate y publica un OrderConfirmed event, un Projector escucha ese evento y actualiza el Read Model. El Read Model siempre es eventualmente consistente con el Write Model. Este delay suele ser de milisegundos.
📤
WRITE SIDE
Commands
IntenciónImperativoPuede fallar
Un Command es una intención de cambiar el estado del sistema. Puede ser rechazado si viola invariantes. Su nombre es imperativo.
Ejemplos: CreateOrderCommand, ConfirmPaymentCommand, CancelOrderCommand
Diferencia con evento: El Command puede ser rechazado. El Event ya ocurrió y es un hecho inmutable.
📥
READ SIDE
Queries & Read Models
Sin side effectsDesnormalizadoViewModel
Una Query nunca muta el estado. El Read Model es una proyección optimizada para lo que la UI necesita mostrar — no para la consistencia transaccional.
Ejemplo: La pantalla de "detalle de pedido" necesita datos del Order, Customer y Product juntos. El Read Model tiene esa vista ya ensamblada, sin joins en runtime.
⚖️
CUÁNDO USAR
¿CQRS siempre?
ComplejidadEscalabilidadTrade-off
CQRS agrega complejidad de sincronización y consistencia eventual. No es gratis.
Usa CQRS si: Tienes alta diferencia entre carga de lectura y escritura. Los modelos de lectura son muy distintos al de escritura. Escalas read y write de forma independiente.

No uses si: El dominio es simple. El modelo de lectura es casi igual al de escritura. No tienes equipo para mantenerlo.
Puedes aplicar CQRS sin Event Sourcing. Son patrones independientes que se complementan bien pero no se requieren mutuamente.