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.
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
List<Order> findByCustomer(CustomerId id);
void delete(OrderId id);
}
✅ CORRECTO
orderRepo.findById(id)
.addItem(item)
orderRepo.save(order)
❌ INCORRECTO
orderItemRepo
.findById(itemId)
.setQuantity(3) ← ¡NO!
Flujo completo de un Domain Event — desde el Aggregate hasta el consumidor
01
🌳
Aggregate
order.confirm()
02
📢
Domain Event
OrderConfirmed
Application
→
Service publica
03
📡
Event Bus
Kafka / SQS
04
📧
Notificaciones
BC consumidor
04
🚚
Logística
BC consumidor
04
📊
Analytics
BC consumidor
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.
public record OrderConfirmed(
OrderId orderId,
CustomerId customerId,
Money total,
Instant occurredAt
) implements DomainEvent {}
public void confirm() {
this.status = CONFIRMED;
registerEvent(new OrderConfirmed(
id, customerId, total, Instant.now()
));
}
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.
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.
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.
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.
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.
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.