Module 6 · Async processing and messaging

Lesson 32 — Eventual consistency and sagas

The payment, the reservation and the email: three systems, one coherent story.

Published
In this lesson
  1. Exercise 1 — The strong/eventual map
  2. Exercise 2 — The PurchaseSaga table
  3. Exercise 3 — The UNKNOWN
  4. Exercise 4 — The compensations
  5. Exercise 5 — Peripheral choreography
  6. Submit

TicketFlow's purchase saga. No solutions.md before submitting.

Exercise 1 — The strong/eventual map

  1. Classify ALL the checkout's operations: availability, reservation+seats, charge, confirmation email, outgoing webhooks, sales dashboard, GDPR report (23). Strong or eventual, and the convergence guarantee of the eventual ones.
  2. Look in your code for the opposite sin: any email/charge in the critical request that should move, or any "eventual query" the user waits for? Document the finding.

Exercise 2 — The PurchaseSaga table

  1. Create the PurchaseSaga(saga_id UUID, reserva FK, intent_id, estado, intentos, created_at, updated_at) model with the state machine (00b) as TextChoices and transitions validated in the model.
  2. Write the orchestrator iniciar_compra(user, event, seats): local TX (reservation) + PENDING saga row + charge command with on_commit (29). Test: the payment fails BEFORE enqueueing if the reservation TX rolls back.
  3. Resume test: kill the "process" (simulate an exception after the charge) and relaunch reanudar_sagas() — the PENDING row with intent_id is picked up, the charge isn't duplicated (the fake gateway's idempotency, 14).

Exercise 3 — The UNKNOWN

  1. Implement the timeout handling: except GatewayTimeout → Outcome.UNKNOWN, with reconciliar(intent_id) doing GET /charges/{intent} against the fake gateway. Three cases in tests: charged (→ CONFIRMED), declined (→ compensate), still UNKNOWN (→ retry with backoff).
  2. Define the deadlines: maximum reconciliation retries and compensation deadline. What happens when the deadline expires with the saga still UNKNOWN? Write it as a 4-step runbook (the final state is human, not automatic).
  3. Break it on purpose: compensate blind on a timeout WITHOUT reconciling and demonstrate with the fake that the customer pays twice (charge + refund of a charge that existed... with the seat freed). Paste the red test as a warning.

Exercise 4 — The compensations

  1. Implement compensar(saga): refund (ledger with the inverse entry, 08) + free seats + ReservationCancelled to the outbox + email. Idempotent: two calls → one refund.
  2. The reverse-order test: a saga with 3 executed steps (charge, reservation, email) — in which order does your code compensate and why? Write it as a test assertion (the fake email is NOT "un-sent": document what happens with non-compensable effects).
  3. The daily job (31): perseguir_compensaciones_pendientes() — compensation_pending older than 24 h → alert. Test with FakeClock.

Exercise 5 — Peripheral choreography

  1. The stream consumers (30) reacting to PaymentSucceeded: emails, analytics. Write the analytics consumer that builds "sales per day" WITHOUT touching the purchase flow. What happens if analytics is down for 2 h? (answer: it converges via PEL/claim — verify it).
  2. Write the full story's test (35's E2E ahead of time): happy purchase → CONFIRMED + 1 email + 1 webhook event; declined purchase → compensated + failure email; timeout-but-charged purchase → reconciled to CONFIRMED.

Submit

Paste the map, the saga model, the UNKNOWN's three cases and the saga's E2E test. Next: Lesson 33 — Well-made unit tests (module 7).