Stack: Django/DRF · Project: TicketFlow Status: Published — a reasoned decision, not a fashion Prerequisite: Lesson 52 — Clean/Hexagonal and DDD applied
Objectives
- Compare modular monolith and microservices with the REAL costs of each (network, data, operations, team), without the Netflix marketing.
- Modularize TicketFlow as a modular monolith: modules with explicit boundaries and the contract between them.
- Identify the thresholds justifying the extraction of ONE service (the strangle) and the exit path if ever needed.
1. The honest comparison: what each buys and charges
| Dimension | Modular monolith | Microservices |
|---|---|---|
| Transactions (10) | One DB, local ACID | Eventual sagas/composition (32) in EVERY flow |
| Network between pieces | Function calls (0 ms, 0 failures) | Latency + timeouts + breakers (54) at every edge |
| Data | One schema, free joins | DB per service, duplication + sync (25) |
| Deployment | One (41: blue-green of everything) | N pipelines, contract versioning (14) |
| Operations | 1 app, 1 dash | N apps, N dashes, service mesh, 46×N |
| Team | 1-10 happy devs | The orchestra needs a conductor and musicians per service |
| Deploy independence | None (one merge = one deploy) | Total (the real big benefit) |
| Boundaries | Discipline (the linter/the rules) | Physical (the network enforces them) |
The point the marketing hides: microservices do NOT fix coupling — they make it PAYABLE: what in the monolith was a bad import (24: the view querying another module's ORM) in microservices is a network call that FAILS with latency and retries. If your conceptual boundaries are bad, the physical ones will make them bad AND slow. The correct order: modular boundaries first (today), physical ones if the business demands them (tomorrow).
2. TicketFlow's modular monolith: the modules and their boundaries
The modularization over what the course already did (52's bounded contexts):
ticketflow/
tickets/ # the seat's domain: reservation, availability, expiration (10/25/31)
payments/ # the money: gateway, intent, refunds, commissions (32/08)
events/ # the catalog: events, venues, prices (00b)
identity/ # auth, MFA, roles (18-21)
shared/ # the shared domain: Money, Clock, outbox, 26's handlerThe boundary rules (the modular monolith's constitution, enforced with tests): (1) one module talks to another through ITS public interface (24's service) — never through its internal models/managers; (2) events between modules go through the outbox/stream (25/30) — never through a direct import of the foreign module in the business flow; (3) the DB: ONE schema (for now) with cross-module FKs forbidden (the reference goes by public UUID, 13: the FK creates the physical coupling that tomorrow blocks extraction); (4) shared/ is small and stable (Money, Clock): 24's junk drawer is modularity's anti-pattern.
# tickets/api.py — the module's public interface (what others import)
def reservar(user, event_uuid, seat_refs, *, clock=None) -> ReservationSummary: ...
def disponibilidad(event_uuid) -> list[SeatFree]: ...
# and the import the constitution forbids (41's CI test catches it):
# from tickets.models import ReservationModel ← in payments/: FORBIDDENThe boundary test (the one making modularity REAL):
def test_modulos_no_importan_entres_si():
with open("payments/services.py") as f:
assert "from tickets.models" not in f.read() # the interface or nothing3. The contracts between modules: synchronous and asynchronous
Communication between modules: synchronous (the public function: payments' checkout calls tickets.reservar(): the transaction is ONE — the monolith's biggest gift) and asynchronous (the outbox's events: PaymentSucceeded reaches tickets with no sender-to-receiver coupling — 32's choreography). The choice rule: synchronous when the operation belongs to the SAME transaction (the seat and the purchase's state); asynchronous when the receiver is peripheral (emails, analytics) or temporal coupling is acceptable. And each interface's contract: tested (35: the contract test between modules = the pact that the day of the external service will be HTTP).
The team-of-1 decision (51): the modular monolith buys 90% of the benefit (boundaries, separated domains, extractable tomorrow) for 10% of the cost. TicketFlow's microservices TODAY: 5 deployments, 5 pipelines, 32's saga in EVERY business flow, and 1 single dev orchestrating: the operations bill outweighs the benefit by an order of magnitude.
4. The extraction threshold: when ONE service leaves
The strangle pattern (the progressive exit): extract the module when its context demands PHYSICAL separation: (1) the life cycle diverges (payments deploys 10×/day for compliance while tickets does 2×/month: the monolith couples the rhythms); (2) the scaling diverges (payments needs 3 replicas and tickets 1: the monolith scales the whole box, 55); (3) the technology diverges (the pricing engine in Go/C: the language boundary); (4) security diverges (payments with PCI access: the compliance boundary, 56); (5) the team diverges (2 teams fighting over the same repo: Conway decides). TicketFlow TODAY: nothing diverges — the documented trigger (the ADR): "payments gets extracted when 56's PCI demands it or the payments volume ×10 versus the rest".
The strangle (extraction without big-bang): (1) the module already has a public interface (§2: the boundary built from today); (2) the new service DUPLICATES the functionality with 27's flag deciding who serves; (3) traffic migrated by percentage (46's canary); (4) the monolith dethrones the code (11: expand-contract applied to architecture); (5) cleanup. Extracting ONE module with the boundary ready: 2-4 weeks (49's estimate); without it: 2-4 months (rewriting the hidden coupling).
5. What the modular monolith gives you TODAY
TicketFlow modular's concrete list: the seat domain (tickets) testable without touching the money (payments); the new teammate's onboarding (48): 5 modules with ADRs vs an anonymous repo; the ONE deploy (41) with the 5 DORA metrics healthy; 42's cloud cost (1 PaaS, not 5); and the open exit: every module with its interface is the strangle candidate when the trigger arrives — the modular monolith is NOT "microservices postponed out of laziness": it is the INFORMED decision to pay for the network only when the network pays its cost (51: every piece, its business line).
Self-assessment
- What does each model buy and charge across §1's 8 dimensions? What does the microservice do with the coupling the monolith ignores?
- List TicketFlow's 4 modular-monolith boundary rules and which CI test makes them real.
- When synchronous and when asynchronous between modules, and which monolith gift gets lost going async?
- The 5 strangle thresholds: which is most likely in TicketFlow and which document fires it?
- Which steps does the strangle have, and how does the cost change extracting WITH a prepared boundary vs WITHOUT one?
Continue with the exercises. The solutions only after trying it yourself.