Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — patrones sobre las capas de la 24 Prerrequisito: Lección 24 — SOLID e inyección de dependencias
Objetivos
- Implementar el patrón Repositorio para aislar el acceso a datos del dominio, y decidir con criterio cuándo NO compensa en Django.
- Modelar eventos de dominio y publicarlos con el patrón Outbox para que nunca se pierdan al notificar (el eslabón débil de la 17).
- Distinguir servicio de dominio, servicio de aplicación y DTO, y colocar cada pieza en su capa.
1. Repositorio: la frontera con la base de datos
Un repositorio encapsula el acceso a datos detrás de una interfaz del dominio: el servicio pregunta repos.reservas.por_evento_disponibles(event_id), no construye Ticket.objects.filter(...) en tres vistas distintas. Beneficios: la consulta complicada vive en UN lugar; el dominio no importa el ORM (la inversión de la 24); los fakes de test son listas en memoria.
class ReservationRepository(Protocol):
def get(self, ref: str) -> Reservation: ...
def save(self, r: Reservation) -> None: ...
class ORMCampaignRepository:
def get(self, ref: str) -> Reservation:
return Reservation.objects.select_for_update().get(public_ref=ref)
def save(self, r: Reservation) -> None:
r.save()La regla honesta: no por cada modelo. Django ya ES un repositorio por tabla (el manager). Un repositorio completo en Django puro suele ser una capa que duplica el ORM con menos features. Lo que sí aísla: los agregados con consultas complejas (reservas por evento con asientos), el acceso multi-tabla del checkout, y todo lo que un test sin BD quiera fingir. Repositorio para Reservation (agregado con invariantes), manager directo para CRUD trivial de Venue.
2. Servicio de dominio vs. servicio de aplicación
Dos cosas se llaman "servicio" y no son lo mismo:
- Servicio de dominio: lógica de negocio que no cabe en una entidad —
reservar(),asignar_asientos(),liquidar_comisiones(). Habla el idioma del negocio, orquesta entidades y repositorios, no conoce HTTP ni serializadores. Es el corazón de la capa services de la 24. - Servicio de aplicación: casos de uso finos sobre el dominio — "recibir una petición de reserva con este usuario" (autenticación implícita, transacción, publicación de eventos, llamada a la pasarela). En Django suele fusionarse con el dominio; sepáralos cuando el mismo flujo deba dispararse desde API, management command y worker con aditamentos distintos (14, 29).
La vista de la 24 llama al servicio; el servicio llama a repositorios y modelos. La flecha de dependencias nunca vuelve hacia arriba.
3. DTO: los datos que cruzan la frontera
Un DTO (Data Transfer Object) es un objeto de datos plano que viaja entre capas sin comportamiento. En Django ya tienes dos y a veces no lo sabías: el serializer de entrada valida y transporta (parsed_data), y el serializer de salida modela la respuesta. ¿Para qué un DTO propio? Cuando el servicio NO debe devolver un Model: la vista recibe ReservationSummary(event, seats, total, expires_at) — un dataclass inmutable — y decide cómo serializarlo. Ventajas: el servicio puede llamarse desde CLI/worker sin lazy-loading sorpresa; el contrato de salida es explícito; no filtras campos internos por descuido.
@dataclass(frozen=True)
class ReservationSummary:
public_ref: str
event: str
seats: tuple[str, ...]
total: Money
expires_at: datetime
def resumen(r: Reservation) -> ReservationSummary:
return ReservationSummary(r.public_ref, r.event.title, r.seat_refs, r.total(), r.expires_at)Regla: DTO hacia fuera (respuesta, mensaje de cola, evento), Model dentro. Si un serializer DRF ya cubre el caso, no añadas DTO por moda — añádelo cuando el consumidor no es la vista HTTP (worker, cola, reporte) o cuando el shape de salida difiere del modelo.
4. Eventos de dominio y el patrón Outbox
Un evento de dominio es un hecho consumado en el pasado: ReservationConfirmed, PaymentFailed. Se publica DESPUÉS de la transacción; el problema de la 17 era: si confirmas la reserva (commit) y el broker está caído, el webhook al comprador se pierde. El patrón outbox: en la MISMA transacción escribes el evento en una tabla outbox_event; un poller/worker lee la tabla y publica al broker. El evento sale a la red solo si la transacción fue real.
def reservar(...) -> Reservation:
with transaction.atomic():
r = Reservation.objects.select_for_update().get(...)
r.confirm()
OutboxEvent.objects.create(
aggregate_type="reservation",
aggregate_id=r.public_ref,
event_type="ReservationConfirmed",
payload={"ref": r.public_ref, "seats": r.seat_refs, "total": r.total_amount},
)
# commit: el poller publicará (at-least-once, el consumidor deduplica por aggregate_id+type)Propiedades: el poller reintenta (el evento no se pierde), puede duplicar (at-least-once) — el consumidor deduplica con la clave única event_id (el UUID del outbox) — y conserva el orden por created_at/id por agregado. Es la respuesta de Django al problema del doble commit (17): no hay transacción distribuida, hay una tabla y un worker.
5. Cómo queda el checkout de TicketFlow
View (HTTP) → serializer + status codes
ApplicationService → reservar(): transacción + outbox + clock + gateway inyectados
DomainService/Model → reglas: máquina de estados, invariantes, timeouts
Repository → agregados con consultas complejas, fakes de test
DTO → ReservationSummary hacia fuera; Model dentro
Outbox + poller → eventos fiables hacia webhooks/emails (17)Señal de que lo aplicaste bien: el servicio de reserva se invoca igual desde la vista, desde manage.py reservar_cli y desde el worker de la 29 — y los tres cuelgan de la misma transacción y el mismo outbox.
Autoevaluación
- ¿Por qué un repositorio sobre un manager de Django puro puede ser sobreingeniería, y qué señales justifican uno?
- ¿Diferencia entre servicio de dominio y servicio de aplicación? ¿Qué gana el proyecto al separarlos (o al no hacerlo)?
- ¿Cuándo un DTO y no un Model en la salida del servicio? Da dos criterios concretos.
- Explica el patrón outbox: ¿qué problema del doble commit resuelve y qué garantía entrega (at-least-once, orden)?
- Si el poller del outbox se cae 10 minutos, ¿qué pasa con los eventos y cómo deduplica el consumidor?
Continúa con los ejercicios. Las solutions.md solo tras intentarlo.