1.Monolith, modular monolith, microservices
A **traditional monolith** is one deployable where any code can import any other code. A **modular monolith** is still one deployable, but the modules have declared boundaries and talk through published interfaces. **Microservices** split those modules into separately deployed services that talk over the network. The roadmap is blunt about the trade: "microservices are not automatically more scalable, more reliable, or easier to maintain".
Key features
- The question is where the boundary is enforced: nowhere, in code, or in the network
- A modular monolith keeps one transaction, one deploy, one traceback, cheap refactoring
- Every extracted call gains timeouts, retries, duplicates and partial failure
- There is no distributed `atomic()` — a cross-service workflow needs an outbox or a saga
- "Microservices are not automatically more scalable, more reliable, or easier to maintain"
- Draw the boundary in the monolith first; a boundary you cannot draw there will not survive a network
What changes when the boundary moves
What actually justifies extracting a service
- Three columns, each showing the same workflow — orders, then billing, then notify.
- In the first column, "traditional monolith", the three parts sit inside one box with plain arrows between them, and a note says any module can import any other module directly.
- In the second column, "modular monolith", the same three parts sit inside one box but each is drawn as a separate bordered module, with arrows labelled "via the public interface", and a note says one transaction still covers all three.
- In the third column, "microservices", the three parts are three separate boxes with no enclosing box, joined by dashed red arrows labelled HTTP, and a note says each arrow can time out, retry or duplicate, and no single transaction spans them.
- A footer states the trade directly: the first two share one transaction, one deploy and one traceback, while the third gains independent deployment and scaling and pays for it with partial failure.
Command
from billing.api import raise_invoice # the module's public interfaceThe three styles are three places to enforce a boundary: nowhere, in code, or in the network. A modular monolith keeps one transaction, one deploy, one traceback and cheap refactoring while giving you the boundary — a public interface module, primitives across it, and a test that forbids everything else. Microservices trade those for independent deployment, scaling and failure, and charge for it in timeouts, retries, duplicates, sagas and correlated tracing. Extract when the reason survives "what would we gain that a module boundary cannot give us?" — different scaling axis, different release cadence, different team, different runtime — and draw the boundary in the monolith first either way.