Module 12 · Enterprise level (capstone)

Lesson 53 — Modular monolith vs microservices

A reasoned decision, not a fashion: when to split and when not to.

Published
In this lesson
  1. Exercise 1 — The honest matrix
  2. Exercise 2 — The modularization
  3. Exercise 3 — The contracts
  4. Exercise 4 — The threshold and the strangle
  5. Exercise 5 — Defending the decision
  6. Professor's summary

Exercise 1 — The honest matrix

  1. TicketFlow's numbers: 5 microservices = 5 pipelines (41's CI ×5), 5 deploys/week (cadence drops from 2.1/day to ~0.4/service), 12 of the roadmap's 18 features would become distributed (checkout touches tickets+payments+events), and 32's saga would go from "one case" to "the standard" of every cross-module flow. The network+timeouts+breakers (54) cost at every edge: ~2 weeks of setup + permanent maintenance.
  2. The best REAL argument in favor: payment's deployment and failure independence — "if checkout explodes (36's peak), payments must NOT fall with it" (the blast radius). It outweighs its cost when: the payments volume justifies its separate operation AND the PCI (56) demands the physical boundary AND the team exceeds 8-10 devs.
  3. The counter-argument (the decision): "With 1 dev, 1 app and 50k sales/month, the modular boundary (2) buys the boundaries and the open exit for 10% of the cost; microservices would pay 5 pipelines, 12 distributed flows and a saga per feature. Documented trigger: PCI or volume ×10. Review: the trigger, not fashion".

Exercise 2 — The modularization

  1. The map: tickets (reservation/expiration/availability), payments (gateway/ledger/commissions), events (catalog/venues), identity (auth/MFA), shared (Money, Clock, outbox, 26's errors). 24's utils dissolves: domain stuff to its module, stable shared stuff to shared, the orphans deleted (the hunting).
  2. What the interface reveals: payments used tickets' Ticket DIRECTLY (the hidden import of the commission calculation) and events read Reservation.objects (the occupancy dashboard) — 2 real violators caught WHILE WRITING the interface: the imports the boundary forces up are the coupling's honest inventory.
  3. The CI with 12 forbidden combinations (5 modules, n×(n−1)): caught exercise 2's 2 violators + 1 tests fixture importing another module's models (migrated to the interface). The boundary test is architecture's linter: without it, the constitution is a pretty doc.

Exercise 3 — The contracts

  1. The synchronous contract:
python
def test_contrato_tickets_reservar():
    r = tickets.reservar(user, event_uuid, ["A1"], clock=FakeClock(...))
    assert isinstance(r, ReservationSummary)          # 25's DTO: the contract
    with pytest.raises(SeatUnavailable):              # the exceptions ARE the contract
        tickets.reservar(user, event_uuid, ["A1"], clock=FakeClock(...))

If the signature changes, this test (running from payments/) breaks: the contract between modules is tested like 35's pact between services.

  1. The async contract: PaymentSucceeded with minimum payload {intent_id, ref, amount} (25: self-sufficient), consumed by tickets to CONFIRM. If it were synchronous: the single transaction would break (the charge inside the reservation's TX = 39's pool suffering + 32's UNKNOWN without reconciliation) and the receiver would stay coupled to the sender in real time — async buys temporal decoupling at the price of eventuality (32: it converges).
  1. The rule (8 lines): "Synchronous if the operation belongs to the SAME transaction (seat+state). Async if the receiver is peripheral or tolerates delay (emails, analytics, confirmation after payment). Case study: reservar() synchronous (same TX), PaymentSucceeded async (convergence), PaymentFailed async (compensation)".

Exercise 4 — The threshold and the strangle

  1. The trigger: the PCI (56) — "payments gets extracted when compliance demands the physical boundary of card data (the domain that PAYS for its operation with a regulatory requirement) or when the payments volume ×10 versus the rest (55: divergent scaling). Measurable: the PCI's date and the volume ratio on 46's dash".
  1. The compared strangle (49's estimate):
WITH the prepared boundary (interfaces+events+tests): 3 weeks
  P1 (3 d)  the new service duplicates the flow (the flag decides)
  P2 (3 d)  46's canary (5%→50% of traffic to the new service)
  P3 (2 d)  100% + the monolith dethrones the code (11)
  P4 (3 d)  cleanup + the HTTP contract (14)
WITHOUT the boundary (rewriting the coupling): 2-3 months
  (the 12 distributed flows, the saga in each one, the contracts from scratch)

The 3:1-4:1 ratio IS the value of today's modularity: the boundary is not bought tomorrow, it is paid in small installments from today.

  1. The 5 changes extraction forces (mitigated by the boundary): (1) the contract between modules goes from import to HTTP/gRPC (35's tests are the mold); (2) 32's saga crosses the network (network UNKNOWNs join the gateway's — 54); (3) the outbox stops being a shared table: external stream (30); (4) identity: 18's internal token between services; (5) observability: 46's trace crosses services (the trace_id already travels, 45).

Exercise 5 — Defending the decision

  1. The pitch (10 lines): "Today the monolith uses 3 replicas, 2 of which exist for Friday's checkout: the divergent scaling justifying microservices is 1 service out of 5. The 12 flows that would be touched become distributed: each with 32's saga, 54 and 14's contract. The proposal: modular monolith with tested boundaries (today), the documented PCI/volume trigger (tomorrow), and the strangle with the boundary built (day X): the same exit at 1/4 of the cost. If the ×10 volume arrives, the plan is already paid for."
  1. The 3-sentence answer: "Boundaries yes, physics not yet: the modules are separated by interface and events (52/53). Extracting now pays 5 pipelines and 12 distributed flows for the benefit of 1. The ADR has the trigger: when PCI or volume asks for it, the extraction is already paid for."
  1. The personal rule (excerpt): "Modularity before distribution; the boundary is paid in installments from day 1 (interface+events+test); the strangle's trigger is written BEFORE needing it; fashion is not a metric."

Professor's summary

  • Microservices do not fix coupling: they make it payable — first the conceptual boundary (with tests), then the physical one if the business demands it.
  • The modular monolith: public interfaces, events via outbox, cross-module FKs forbidden, small shared — the boundary test in CI is the constitution.
  • The strangle with a prepared boundary costs 1/3-1/4 of doing it without: today's modularity is tomorrow's extraction pre-paid — with the trigger written in the ADR.