El anillo con la flecha que mordía: el servicio reservar() manipulaba ReservationModel (el Model DENTRO del dominio: la flecha hacia fuera) y settings.FEATURE_WEBHOOKS importado en services (la config del 27 filtrando al dominio). Tras la extracción: el dominio recibe flags por parámetro (la 24: inyectado).
Los puertos faltantes: el email era send_mail directo en la tarea (infra en el dominio del worker) → el puerto Mailer + SMTPMailer (adaptador) + FakeMailer (33). El rate limiter del 23 ya vivía detrás de cache (puerto implícito): formalizado como Limiter.
El grep: from django en domain/ → vacío tras la extracción. import models en services → quedó SOLO en el mapper/adapters (la decisión documentada: el active record de la 00b persiste para el CRUD, el dominio puro para el agregado).
El Percentage: Percentage(5) (la comisión del 08) con invariante 0-30 y of(money) -> Money: la comisión del org dejó de ser un Decimal(0.05) sin reglas en 3 lugares — ahora la regla vive en el constructor y el of() la aplica.
La conversión: vive en el MAPPER (adapters), jamás en el dominio (el dominio no sabe qué es un serializer): el DTO de la 25 acepta Money y su __str__/asdict lo serializa — el borde convierte, el centro no.
Ejercicio 3 — El agregado
La consecuencia del lazy-loading: get() del repositorio ahora exige el agregado COMPLETO (los seats con prefetch_related o el JOIN): el agregado parcial rompería las invariantes (¿total() sin seats? error). La regla documentada: el repositorio carga el agregado entero o nada — el agregado grande (20k seats) NO se carga entero: la disponibilidad masiva NO es el agregado (es una lectura del 25: AvailabilityReader), solo la reserva del comprador (2-6 seats) es agregado.
La migración de tests: 14 tests del agregado pasaron a unitario puro (<0.4 s la suite de dominio); SE QUEDAN en integración: el lock (34: dos transacciones), el outbox-tras-commit (34), la saga reanudable (34) — las costuras del motor no se migran al dominio puro.
Ejercicio 4 — La decisión
El ADR (extracto): "DDD parcial: VOs (Money, SeatRef, Percentage) + agregado Reservation en dominio puro con mappers; CRUD (Venue, config) en ORM activo. Costo medido: 4 días + 2 mappers. Trigger de revisión: el segundo servicio (53) o que las reglas de precios superen las 3 entidades interconectadas — el DDD completo sería 3 semanas por 1 aplicación CRUD: el mapa del 51 lo marca como sobre-ingeniería HOY".
El contrafáctico: DDD completo ≈ 3 semanas (todos los modelos + repos + bounded contexts): la ganancia real sería el dominio testeable sin BD para Venue/Seat (CRUD: no aporta: sin lógica que probar) y la frontera con el futuro servicio de pagos (la saga ya está desacoplada del 32). La factura la pagarían las 3 líneas de negocio del 51: facturación (el ledger: tocarlo = riesgo), conversión (el checkout: el refactor lo congela), fiabilidad. NO vale hoy; vale si el 53 trocea.
Los bounded contexts: tickets (reserva, asientos, disponibilidad), payments (intent, pasarela, refunds), events (catálogo, venues) — el mapa con sus lenguajes: "reserva" en tickets es el AGREGADO con expiración; en payments es solo un ID de contexto (el payload del outbox): la misma palabra, dos contextos — la regla del ubicuo POR contexto (no global).
Ejercicio 5 — El lenguaje ubicuo
Los renombres: process_stuff() → liquidar_comisiones() (la 31); XDataModel → Reservation; handle_webhook_generic → aplicar_webhook_pasarela. El test del dueño: con el glosario del 3, lee los nombres del módulo de tickets y entiende 12 de 15 — antes entendía 4.
La ACL: el if charge_status == "settled" estaba en services (el vocabulario ajeno contaminó) → movido al adaptador PasarelaAdapter que emite PaymentSucceeded/PaymentFailed (el dominio solo conoce SU vocabulario) — el borde traduce, el centro respira su idioma.
El glosario (extracto):
markdown
# Lenguaje del dominio (15 palabras)
- Reserva: la retención temporal de asientos de un comprador; expira a los 10 min (10).
- Confirmar: la transición PENDING→CONFIRMED tras el pago (32). irreversible.
- Expira: la transición automática PENDING→EXPIRED al vencer el TTL (29/31).
- Tarjeta regalo: saldo prepagado con ledger propio (08); paga parcial.
- Comisión: el Percentage del org cobrado por venta (08): la facturación.
- Disponibilidad: la lectura de asientos libres (25): informa, nunca decide (38).
...
Resumen del profesor
La 24/25 ya era hexagonal; el hueco era el Model en el servicio: el dominio puro (VOs + agregado) lo cierra, con mappers en el borde.
El VO ejecuta la invariante donde nace el dato; el agregado se carga completo (la disponibilidad masiva NO es agregado); el CRUD queda en ORM: DDD parcial, con trigger en el ADR.
El lenguaje ubicuo y la ACL valen más que la arquitectura: el negocio y el código hablan el mismo idioma, y el ajeno (la pasarela) se traduce en el borde.