Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — apertura del módulo de nivel empresarial Prerrequisito: Lección 51 — Entender el negocio
Objetivos
- Reconocer lo que el curso YA construyó: las capas de la 24/25 son hexagonal con otro nombre — y nombrar los conceptos DDD que faltan.
- Extraer el dominio de TicketFlow a objetos de dominio puros (entidades, value objects, agregados) y ver el costo real de hacerlo.
- Decidir cuándo DDD completo paga y cuándo es el sobre-diseño que el 51 identifica.
1. Hexagonal: lo que ya tienes (y el nombre)
La arquitectura hexagonal (puertos y adaptadores): el dominio en el centro, los detalles (HTTP, BD, Redis, pasarela) en el borde, las flechas de dependencia SOLO hacia adentro. La auditoría del curso: el dominio de TicketFlow ya está en services/ (24) con los Protocols (Clock, PaymentGateway, ReservationRepository — los PUERTOS), las implementaciones en el borde (ORMReservationRepository, SimulatedGateway — los ADAPTADORES), los DTOs hacia fuera (25), el outbox como puerto de eventos (25). Ya es hexagonal: lo que faltaba era el nombre y una cosa: el dominio aún manipula Models de Django (Reservation.objects dentro del servicio). La lección de hoy cierra ese último hueco — CON la decisión de si vale (§4).
┌──────────────────────────────────────┐
│ Django (HTTP) · Celery · CLI │ ← adaptadores de entrada
│ ┌────────────────────────────────┐ │
│ │ APLICACIÓN: casos de uso │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ DOMINIO: Reservation, │ │ │
│ │ │ Money, Checkout — puro │ │ │
│ │ └──────────────────────────┘ │ │
│ │ Puertos: Repos, Clock, GW │ │
│ └────────────────────────────────┘ │
│ Postgres · Redis · Pasarela │ ← adaptadores de salida
└──────────────────────────────────────┘2. Value objects: el dinero y las referencias que ya querías
El value object (VO): sin identidad, inmutable, definido por su valor, con las invariantes en el constructor — el Money de la 08 ya era uno (por eso el 37 lo cazó en el perfilado: era el objeto más caliente). Los que faltan:
# domain/values.py — puro, cero Django
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str = "EUR"
def __post_init__(self):
if self.amount < 0:
raise ValueError("Money no admite negativos") # la invariante viva
if len(self.currency) != 3:
raise ValueError("moneda ISO-4217")
def add(self, other: "Money") -> "Money":
if other.currency != self.currency:
raise ValueError("monedas distintas")
return Money(self.amount + other.amount, self.currency)
@dataclass(frozen=True)
class SeatRef:
value: str
def __post_init__(self):
if not re.fullmatch(r"[A-Z]\d{1,3}", self.value):
raise ValueError(f"asiento inválido: {self.value}") # A12 sí, "a-12?" noEl valor del VO: la invariante SE EJECUTA donde nace el dato (no en 12 sitios con if), el tipo documenta el dominio (def total(self) -> Money vs -> Decimal), y el test de la 33 los prueba sin Django. El costo: la conversión en el borde (el Model guarda Decimal, el dominio recibe Money — los mappers del §3).
3. El agregado y el mapeo: la extracción honesta
El agregado DDD: el clúster de objetos que se persisten como unidad con sus invariantes — Reservation (+ sus Seats, su Money) es el agregado que la 25 ya aisló con repositorio. La extracción completa al dominio puro:
# domain/reservation.py — SIN Django
class Reservation:
def __init__(self, ref: str, event_id: str, seats: list[SeatRef], money: Money, status: str):
...
def confirmar(self) -> None:
if self.status != "PENDING":
raise DomainError(f"transición inválida {self.status} → CONFIRMED") # la máquina de estados (00b)
self.status = "CONFIRMED"
def expira_en(self, clock: Clock, ttl: timedelta) -> bool:
return clock.now() > self.expires_atY el mapper en el adaptador (el único que sabe Django):
# adapters/mappers.py
def to_domain(row: ReservationModel) -> Reservation:
return Reservation(row.public_ref, row.event_id, [SeatRef(s) for s in row.seat_refs],
Money(row.total_amount), row.status)
def to_persistence(r: Reservation) -> dict:
return {"public_ref": r.ref, "seat_refs": [s.value for s in r.seats], "status": r.status, ...}El costo REAL medido: el servicio reservar() del 25 pasa de manipular el Model (el lock, el select_for_update, el save directo) a: obtener el estado (mapper), operar el dominio, escribir (mapper) — 2 mappers + el refactoreo de la máquina de estados. El beneficio: el dominio se testea en milisegundos SIN BD (la 33 lo pedía a gritos), las invariantes viven en UNA clase, y el transporte (Postgres→otro) es un mapper nuevo. El costo oculto: el lazy-loading desaparece (el agregado se carga COMPLETO: el diseño del repo del 25 debe cargar los seats con prefetch — el agregado grande es lento por diseño).
4. La decisión: ¿DDD completo para TicketFlow?
La matriz honesta del 51 (valor/esfuerzo/riesgo): DDD completo (dominio puro + mappers + repositorios para TODOS los modelos): esfuerzo ~3 semanas, beneficio: dominio testeable puro y fronteras explícitas. El dominio de TicketFlow: 6-8 entidades, 3 agregados con lógica real, 1 lenguaje de negocio estable — el cálculo del 51: partial: VOs (Money, SeatRef) + el agregado Reservation extraído + los puertos de la 24; el CRUD trivial (Venue, la config del 27) sigue con el ORM directo. La regla de decisión (el ADR del 48): "DDD parcial: los VOs y los agregados con invariantes viven en dominio puro; el CRUD y las lecturas viven en Django. Se revisa cuando el segundo servicio (53) o la complejidad de las reglas lo exijan". El DDD completo para UNA app Django CRUD es el sobre-diseño que el mapa del 51 marca.
5. El lenguaje ubicuo: la habilidad que no es código
El concepto DDD más valioso no es técnico: el lenguaje ubicuo — las palabras del negocio son las del código: el negocio dice "reserva", "expira", "se confirma", "tarjeta regalo" — y el código se llama reservar(), expirar(), confirmar(), GiftCard (no AppointmentModel, no GiftCardData). La auditoría del curso: el lenguaje del 00b (la máquina de estados: PENDING/CONFIRMED/EXPIRED) ES el lenguaje del negocio; el Anti-corruption layer (ACL) es el traductor del borde: el payload de la pasarela (charge_status: "settled") se traduce a PaymentSucceeded en UN lugar (el adaptador del 17) — el vocabulario ajeno no contamina el dominio. El beneficio medible: la conversación con el negocio (51) usa las MISMAS palabras que el código: el bug se reporta con el lenguaje del dominio y el traceback se lee en el mismo idioma.
Autoevaluación
- ¿Qué partes de la 24/25 ya eran hexagonal y cuál era el hueco que quedaba (el Model dentro del servicio)?
- ¿Qué garantiza un value object y qué dos invariantes ejecuta el
Moneydel curso? ¿Dónde vive la invariante? - ¿Qué es un agregado y qué costo oculto tiene el agregado grande (el lazy-loading)? ¿Qué exige del repositorio?
- ¿Qué decide "DDD parcial" para TicketFlow y cuál sería el trigger del DDD completo (el ADR)?
- ¿Qué es el lenguaje ubicuo y dónde se vio el Anti-corruption layer del curso (el payload de la pasarela)?
Continúa con los ejercicios. Las solutions.md solo tras intentarlo.