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. Objectives
  2. 1. The honest comparison: what each buys and charges
  3. 2. TicketFlow's modular monolith: the modules and their boundaries
  4. 3. The contracts between modules: synchronous and asynchronous
  5. 4. The extraction threshold: when ONE service leaves
  6. 5. What the modular monolith gives you TODAY
  7. Self-assessment

Stack: Django/DRF · Project: TicketFlow Status: Published — a reasoned decision, not a fashion Prerequisite: Lesson 52 — Clean/Hexagonal and DDD applied


Objectives

  1. Compare modular monolith and microservices with the REAL costs of each (network, data, operations, team), without the Netflix marketing.
  2. Modularize TicketFlow as a modular monolith: modules with explicit boundaries and the contract between them.
  3. 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

DimensionModular monolithMicroservices
Transactions (10)One DB, local ACIDEventual sagas/composition (32) in EVERY flow
Network between piecesFunction calls (0 ms, 0 failures)Latency + timeouts + breakers (54) at every edge
DataOne schema, free joinsDB per service, duplication + sync (25)
DeploymentOne (41: blue-green of everything)N pipelines, contract versioning (14)
Operations1 app, 1 dashN apps, N dashes, service mesh, 46×N
Team1-10 happy devsThe orchestra needs a conductor and musicians per service
Deploy independenceNone (one merge = one deploy)Total (the real big benefit)
BoundariesDiscipline (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 handler

The 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.

python
# 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/: FORBIDDEN

The boundary test (the one making modularity REAL):

python
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 nothing

3. 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

  1. What does each model buy and charge across §1's 8 dimensions? What does the microservice do with the coupling the monolith ignores?
  2. List TicketFlow's 4 modular-monolith boundary rules and which CI test makes them real.
  3. When synchronous and when asynchronous between modules, and which monolith gift gets lost going async?
  4. The 5 strangle thresholds: which is most likely in TicketFlow and which document fires it?
  5. 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.