The ring with the biting arrow: the reservar() service manipulated ReservationModel (the Model INSIDE the domain: the outward arrow) and settings.FEATURE_WEBHOOKS imported in services (27's config leaking into the domain). After the extraction: the domain receives flags as parameters (24: injected).
The missing ports: the email was send_mail directly in the task (infra in the worker's domain) → the Mailer port + SMTPMailer (adapter) + FakeMailer (33). 23's rate limiter already lived behind cache (implicit port): formalized as Limiter.
The grep: from django in domain/ → empty after the extraction. import models in services → stayed ONLY in the mapper/adapters (the documented decision: 00b's active record persists for CRUD, pure domain for the aggregate).
The Percentage: Percentage(5) (08's commission) with the 0-30 invariant and of(money) -> Money: the org's commission stopped being a rule-less Decimal(0.05) in 3 places — now the rule lives in the constructor and of() applies it.
The conversion: it lives in the MAPPER (adapters), never in the domain (the domain does not know what a serializer is): 25's DTO accepts Money and its __str__/asdict serializes it — the edge converts, the center does not.
Exercise 3 — The aggregate
The lazy-loading consequence: the repository's get() now demands the COMPLETE aggregate (the seats with prefetch_related or the JOIN): a partial aggregate would break the invariants (total() without seats? error). The documented rule: the repository loads the whole aggregate or nothing — the big aggregate (20k seats) does NOT load whole: mass availability is NOT the aggregate (it is a 25 read: AvailabilityReader); only the buyer's reservation (2-6 seats) is an aggregate.
The test migration: 14 aggregate tests moved to pure unit (the domain suite <0.4 s); these STAY in integration: the lock (34: two transactions), the outbox-after-commit (34), the resumable saga (34) — the engine's seams do not migrate to pure domain.
Exercise 4 — The decision
The ADR (excerpt): "Partial DDD: VOs (Money, SeatRef, Percentage) + the Reservation aggregate in pure domain with mappers; CRUD (Venue, config) in active ORM. Measured cost: 4 days + 2 mappers. Review trigger: the second service (53) or pricing rules exceeding 3 interconnected entities — full DDD would be 3 weeks for 1 CRUD application: 51's map flags it as over-engineering TODAY".
The counterfactual: full DDD ≈ 3 weeks (every model + repos + bounded contexts): the real gain would be the DB-free testable domain for Venue/Seat (CRUD: no gain: no logic to test) and the border with the future payments service (the saga is already decoupled by 32). The bill would be paid by 51's 3 business lines: invoicing (the ledger: touching it = risk), conversion (checkout: the refactor freezes it), reliability. NOT worth it today; worth it if 53 splits.
The bounded contexts: tickets (reservation, seats, availability), payments (intent, gateway, refunds), events (catalog, venues) — the map with their languages: "reserva" in tickets is the AGGREGATE with expiration; in payments it is just a context ID (the outbox's payload): the same word, two contexts — the ubiquitous rule PER context (not global).
Exercise 5 — The ubiquitous language
The renames: process_stuff() → liquidar_comisiones() (31); XDataModel → Reservation; handle_webhook_generic → aplicar_webhook_pasarela. The owner test: with exercise 3's glossary, they read the tickets module's names and understand 12 of 15 — before, 4.
The ACL: the if charge_status == "settled" was in services (the foreign vocabulary contaminated) → moved to the PasarelaAdapter adapter that emits PaymentSucceeded/PaymentFailed (the domain only knows ITS vocabulary) — the edge translates, the center breathes its own language.
The glossary (excerpt):
markdown
# Domain language (15 words)
- Reserva (reservation): the buyer's temporary seat hold; expires in 10 min (10).
- Confirmar (confirm): the PENDING→CONFIRMED transition after payment (32). irreversible.
- Expira (expires): the automatic PENDING→EXPIRED transition when the TTL lapses (29/31).
- Tarjeta regalo (gift card): prepaid balance with its own ledger (08); pays partial.
- Comisión (commission): the org's Percentage charged per sale (08): the invoicing.
- Disponibilidad (availability): the free-seats read (25): informs, never decides (38).
...
Professor's summary
24/25 was already hexagonal; the gap was the Model in the service: pure domain (VOs + aggregate) closes it, with mappers at the edge.
The VO enforces the invariant where the data is born; the aggregate loads whole (mass availability is NOT an aggregate); CRUD stays in the ORM: partial DDD, with the trigger in the ADR.
The ubiquitous language and the ACL are worth more than the architecture: business and code speak the same language, and the foreign one (the gateway) is translated at the edge.