Módulo 5 · Arquitectura y código mantenible

Lección 25 — Patrones de servicio

Repositorio, servicio, DTO, eventos de dominio y el patrón outbox.

Publicada
En esta lección
  1. Ejercicio 1 — Repositorio del agregado Reservation
  2. Ejercicio 2 — Servicio de aplicación con outbox
  3. Ejercicio 3 — DTO de salida
  4. Ejercicio 4 — Eventos de dominio, mínimo viable
  5. Ejercicio 5 — Deduplicación del consumidor
  6. Entrega

Refactors sobre TicketFlow. Sin solutions.md hasta entregar.

Ejercicio 1 — Repositorio del agregado Reservation

  1. Define el Protocol ReservationRepository con get, save y existe_reserva_activa(event_id, user_id). Implementa ORMReservationRepository con la consulta con select_for_update de la 10.
  2. Escribe InMemoryReservationRepository para tests: dict en memoria, mismo contrato. Mueve el test de la reserva duplicada (10) a este fake.
  3. Decisión escrita: para Venue y Seat (CRUD simple), ¿mantienes manager directo o repositorio? Justifica en 3 líneas.

Ejercicio 2 — Servicio de aplicación con outbox

  1. Crea la tabla OutboxEvent (aggregate_type, aggregate_id, event_type, payload JSONB, created_at, publicado_at nullable) con índice parcial sobre publicado_at IS NULL.
  2. Modifica reservar() para escribir ReservationConfirmed en el outbox DENTRO de la transacción. Verifica: si la transacción falla (asiento ocupado), no hay fila en outbox.
  3. Escribe el poller: management command que lee hasta 50 eventos pendientes ordenados por id, los "publica" (por ahora, print/log) y marca publicado_at. Idempotente al relanzar.

Ejercicio 3 — DTO de salida

  1. Implementa ReservationSummary (dataclass frozen) y la función resumen(). Cambia la vista de detalle de reserva para serializar desde el DTO, no desde el Model.
  2. Rompe el acoplamiento del todo: ¿qué pasa si mañana renombras un campo del Model? ¿Cuántos sitios tocan si serializas desde Model vs desde DTO?
  3. Bonus: añade expires_in_seconds calculado en el DTO (con Clock inyectado), no en el serializer — ¿por qué ese cálculo es dominio?

Ejercicio 4 — Eventos de dominio, mínimo viable

  1. Enumera 5 eventos de dominio de TicketFlow (ReservationConfirmed, ReservationExpired, PaymentSucceeded, RefundIssued, EventPublished) y para cada uno: quién lo emite, quién lo consume, payload mínimo.
  2. Añade PaymentFailed al outbox desde el servicio de pagos. Escribe el test: pago rechazado → transacción commit + fila outbox con event_type="PaymentFailed" y payload con reason.

Ejercicio 5 — Deduplicación del consumidor

  1. En el consumidor de eventos (el poller que "publica"), guarda en Redis event:{event_id} con TTL 24h al procesar; si ya existe, salta. Escribe el test que publica el mismo evento dos veces y comprueba que el efecto (log/email fake) ocurre UNA vez.
  2. ¿Por qué la deduplicación va en el consumidor y no en el poller? Escribe la respuesta en 2 líneas.

Entrega

Pega la interfaz del repositorio, la migración del outbox, el poller y el DTO. Después: Lección 26 — Manejo de errores centralizado.