Module 5 · Architecture and maintainable code

Lesson 25 — Service patterns

Repository, service, DTO, domain events and the outbox pattern.

Published
In this lesson
  1. Exercise 1 — The Reservation aggregate's repository
  2. Exercise 2 — Application service with outbox
  3. Exercise 3 — The output DTO
  4. Exercise 4 — Domain events, minimum viable
  5. Exercise 5 — Consumer deduplication
  6. Submit

Refactors on TicketFlow. No solutions.md before submitting.

Exercise 1 — The Reservation aggregate's repository

  1. Define the Protocol ReservationRepository with get, save and existe_reserva_activa(event_id, user_id). Implement ORMReservationRepository with lesson 10's select_for_update query.
  2. Write InMemoryReservationRepository for tests: an in-memory dict, same contract. Move the duplicate-reservation test (10) to this fake.
  3. Written decision: for Venue and Seat (simple CRUD), do you keep the direct manager or a repository? Justify in 3 lines.

Exercise 2 — Application service with outbox

  1. Create the OutboxEvent table (aggregate_type, aggregate_id, event_type, payload JSONB, created_at, nullable publicado_at) with a partial index over publicado_at IS NULL.
  2. Modify reservar() to write ReservationConfirmed into the outbox INSIDE the transaction. Verify: if the transaction fails (seat taken), there is no outbox row.
  3. Write the poller: a management command that reads up to 50 pending events ordered by id, "publishes" them (for now, print/log) and marks publicado_at. Idempotent when relaunched.

Exercise 3 — The output DTO

  1. Implement ReservationSummary (frozen dataclass) and the resumen() function. Change the reservation detail view to serialize from the DTO, not from the Model.
  2. Break the coupling all the way: what happens if tomorrow you rename a Model field? How many places does it touch if you serialize from Model vs from DTO?
  3. Bonus: add expires_in_seconds computed in the DTO (with an injected Clock), not in the serializer — why is that computation domain?

Exercise 4 — Domain events, minimum viable

  1. List 5 TicketFlow domain events (ReservationConfirmed, ReservationExpired, PaymentSucceeded, RefundIssued, EventPublished) and for each: who emits it, who consumes it, minimum payload.
  2. Add PaymentFailed to the outbox from the payments service. Write the test: rejected payment → transaction commit + outbox row with event_type="PaymentFailed" and a payload with reason.

Exercise 5 — Consumer deduplication

  1. In the events consumer (the "publishing" poller), store event:{event_id} in Redis with a 24h TTL when processing; if it already exists, skip. Write the test that publishes the same event twice and checks the effect (log/email fake) happens ONCE.
  2. Why does deduplication belong in the consumer and not in the poller? Write the answer in 2 lines.

Submit

Paste the repository interface, the outbox migration, the poller and the DTO. Next: Lesson 26 — Centralized error handling.