Ejercicio 1 — Repositorio
class ReservationRepository(Protocol):
def get(self, ref: str) -> Reservation: ...
def save(self, r: Reservation) -> None: ...
def existe_reserva_activa(self, event_id: int, user_id: int) -> bool: ...
class InMemoryReservationRepository:
def __init__(self): self._items: dict[str, Reservation] = {}
def get(self, ref): return self._items[ref] # KeyError si no existe: mismo contrato que .get() del ORM
def save(self, r): self._items[r.public_ref] = r
def existe_reserva_activa(self, event_id, user_id):
return any(r.event_id == event_id and r.user_id == user_id and r.status == "ACTIVE"
for r in self._items.values())- Para Venue/Seat: manager directo. Repositorio ahí es reescribir el ORM sin ganar nada; el agregado Reservation justifica el suyo porque concentra la consulta con lock, las invariantes y el fake de test.
Ejercicio 2 — Outbox
Migración (extracto):
class OutboxEvent(models.Model):
aggregate_type = models.CharField(max_length=50)
aggregate_id = models.CharField(max_length=64)
event_type = models.CharField(max_length=100)
payload = models.JSONField()
created_at = models.DateTimeField(auto_now_add=True)
publicado_at = models.DateTimeField(null=True, blank=True)
class Meta:
indexes = [models.Index(
fields=["created_at"],
condition=Q(publicado_at__isnull=True), # índice parcial: solo pendientes
name="outbox_pending_idx",
)]Test de atomicidad: fuerza el fallo (asiento ya reservado) y verifica OutboxEvent.objects.count() == 0 después del DomainError — el rollback se llevó el evento: eso es exactamente el punto del outbox.
Poller idempotente:
class Command(BaseCommand):
def handle(self, *args, **opts):
while True:
with transaction.atomic():
batch = list(OutboxEvent.objects.filter(publicado_at__isnull=True).order_by("id")[:50])
if not batch:
return
for ev in batch:
self.publicar(ev) # broker/log; at-least-once
ev.publicado_at = timezone.now()
ev.save(update_fields=["publicado_at"])Ejercicio 3 — DTO
- La vista:
@api_view(["GET"])
def reservation_detail(request, ref):
r = repos.reservas.get(ref) # o servicio.leer(ref)
return Response(ReservationSummarySerializer(resumen(r)).data)- Serializar desde Model: renombrar
seat_refs→seatstoca el serializer y potencialmente cada consumidor delmodel_to_dict. Con DTO: tocaresumen()(un lugar) y el serializer que lo pinta. Un punto de traducción. expires_in_secondses regla de negocio (el TTL de la reserva, la 10) — si vive en el serializer, cada cliente lo recalcula y el CLI no lo tiene. En el DTO con Clock inyectado, el test lo fija sin freezegun.
Ejercicio 4 — Eventos
Tabla mínima de eventos (payload justo, el consumidor no hace queries extra):
| Evento | Emisor | Consumidor | Payload mínimo |
|---|---|---|---|
| ReservationConfirmed | servicio reservar | email, analytics | ref, user_id, event_id, seats, total, expires_at |
| ReservationExpired | job expiración (29) | email, liberar asientos | ref, user_id |
| PaymentSucceeded | servicio pagos | confirmar reserva, recibo | intent_id, ref, amount |
| RefundIssued | servicio refunds | email, contabilidad | intent_id, ref, amount, reason |
| EventPublished | admin/organizador | notificaciones push | event_id, venue |
| PaymentFailed | servicio pagos | email "inténtalo de nuevo", métricas | intent_id, ref, reason |
El error clásico del payload mínimo: el consumidor "solo mira el event_id y luego consulta" — si la fila cambió (reserva expirada y borrada), el email sale con datos incorrectos. Payload autosuficiente (idempotencia + inmutabilidad del hecho).
Ejercicio 5 — Deduplicación
def consumir(ev: OutboxEvent):
key = f"event:{ev.id}"
if not cache.add(key, "1", 60 * 60 * 24): # SET NX: atómico
return # ya procesado: dedup
self.publicar(ev)Test: el fake de "publicar" es una lista; procesa dos veces el mismo evento y len(published) == 1. La deduplicación vive en el CONSUMIDOR porque el poller solo garantiza at-least-once desde el outbox a la cola; el efecto final (email enviado, webhook firmado de la 17) es quien necesita exactly-once efectivo, y solo el dueño del efecto conoce su estado. El poller puede deduplicar como optimización, no como garantía.
Resumen del profesor
- Repositorio para agregados con lógica; manager directo para CRUD trivial. La capa que duplica el ORM sin dominio es ruido.
- Outbox = eventos fiables con una tabla y un poller; at-least-once + dedup en el consumidor. La alternativa (transacciones distribuidas) no existe en tu stack.
- DTO hacia fuera cuando el consumidor no es la vista HTTP o el shape difiere del modelo; Model dentro.