Last week we faced the failures that come with more than one machine. Tonight, the bigger question: how many machines should you have in the first place? The whole spectrum, what each style buys, what it charges, and how to choose.
Microservices are not a maturity level you graduate to. They are a trade: you buy independent deploy and scale, and you pay in network calls, partial failure, and scattered data. Most teams should start coupled and split only on a real seam.
One big house versus a village of cabins. Cabins can be renovated one at a time, but now you walk outside in the rain to get from the kitchen to the bedroom. Build the big house with tidy rooms first; move a room into its own cabin only when there is a real reason.
The question is never "monolith or microservices." It is "how far along this line does my system need to be?" You can sit anywhere, and you can move along it over time. Every step right buys independence and charges complexity.
flowchart LR A["Monolith
one deployable"] --> B["Modular monolith
boundaries inside"] B --> C["Microservices
independent services"] C --> D["Event-driven
decoupled in time"] classDef a fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef b fill:#D6F5E3,stroke:#1F2937,color:#0E1726; classDef c fill:#DCEBFE,stroke:#1F2937,color:#0E1726; classDef d fill:#EDE9FE,stroke:#1F2937,color:#0E1726; class A a; class B b; class C c; class D d;
Think of a volume knob, not a light switch. The skill is not picking a side, it is choosing how far to turn the knob for this system, this team, this year, and being able to say why.
Every style on the spectrum is just a different answer to these two. Cohesion: do the things that change together live together? Coupling: how much does one part need to know about another? You want high cohesion inside a boundary and low coupling across it.
Cohesion is keeping all the kitchen things in the kitchen. Coupling is how often you must run to another room to cook. Good architecture, at any size, is high cohesion and low coupling. The styles are just where you draw the walls.
All your code ships as a single unit, runs in one process, and usually talks to one database. This is not a dirty word. It is the simplest thing that works, and for most products it is the right starting point.
flowchart TB
subgraph ONE["one deployable · one process"]
UI["web / API layer"] --> SVC["business logic"]
SVC --> DATA["data access"]
end
DATA --> DB[("one shared database")]
classDef box fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
classDef db fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
class UI,SVC,DATA box;
class DB db;
One building, with floors. Everything is under one roof, so walking between rooms is instant and free. The downside only shows up later, when the building gets too big for one team to share.
None of the costs hit on day one. They creep in as the codebase and the team grow. That is the whole story of this session: the monolith is right until specific, nameable pains appear.
Teams do not leave the monolith because microservices are fashionable. They leave when one of these four genuinely hurts. If none of them hurt, you do not have a reason to split yet.
flowchart LR M["the monolith grows"] --> P1["deploy coupling
one small change ships everything"] M --> P2["scaling coupling
scale the whole app for one hot path"] M --> P3["blast radius
one bug can take it all down"] M --> P4["team friction
many teams, one shared codebase"] classDef m fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef bad fill:#FEE4E2,stroke:#1F2937,color:#0E1726; class M m; class P1,P2,P3,P4 bad;
These are the four sentences that justify a split later. Memorise them. "We split billing off because it scales differently and a bug in it must not take down checkout" is a real reason. "We wanted microservices" is not.
Keep the single deployable, but split the inside into real modules with enforced boundaries: each owns its data and exposes a clear internal API. You get most of the clarity of microservices with almost none of the distributed cost.
flowchart TB
subgraph PROC["one deployable · one process"]
A["Orders
module"] -->|"internal API"| B["Billing
module"]
B -->|"internal API"| C["Catalog
module"]
end
A --> DBA[("orders tables")]
B --> DBB[("billing tables")]
C --> DBC[("catalog tables")]
classDef mod fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
classDef db fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
class A,B,C mod;
class DBA,DBB,DBC db;
Still one building, but now the rooms have real walls and doors instead of an open-plan mess. If you ever do need to move a room into its own cabin later, the wall is already there to cut along.
"We have modules" means nothing if any code can reach into any table. The boundary has to be enforced by structure, not by good intentions, or it rots back into a big ball of mud.
Now each module becomes its own service: its own process, its own database, deployed and scaled on its own schedule, talking to the others over the network. The walls became roads.
flowchart TB
GW["API gateway"] --> S1["Orders service"]
GW --> S2["Billing service"]
GW --> S3["Catalog service"]
S1 -->|"network call"| S2
S1 --> D1[("orders DB")]
S2 --> D2[("billing DB")]
S3 --> D3[("catalog DB")]
classDef gw fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
classDef svc fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
classDef db fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
class GW gw;
class S1,S2,S3 svc;
class D1,D2,D3 db;
The rooms moved into separate cabins, each with its own lock and its own fridge. Independence, yes, but every trip between them is now a walk across the yard, and the yard has weather.
Notice every benefit is about independence: deploy, scale, fail, choose, own, on your own schedule. If your teams do not actually need that independence yet, you are buying something you will not use.
The split turns fast, reliable in-process calls into slow, fallible network calls. Every problem from the last two sessions, timeouts, retries, idempotency, distributed data, now lives inside your own architecture.
flowchart LR SP["you split into services"] --> C1["network in the middle
slow, and it can fail"] SP --> C2["partial failure
one down, the rest must cope"] SP --> C3["distributed data
no easy transactions or joins"] SP --> C4["ops overhead
deploy, observe, secure each one"] SP --> C5["harder testing
end to end across services"] classDef s fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef bad fill:#FEE4E2,stroke:#1F2937,color:#0E1726; class SP s; class C1,C2,C3,C4,C5 bad;
This is why we did S5 and S6 first. Microservices do not remove distributed-systems problems, they sign you up for all of them. If that list scares you, that is the correct reaction; it is the price of admission.
Systems end up shaped like the communication structure of the teams that build them. So service boundaries and team boundaries should match on purpose, otherwise the architecture and the org quietly fight each other.
flowchart TB
subgraph ORG["how your teams are organised"]
T1["team A"]
T2["team B"]
T3["team C"]
end
T1 --> M1["component A"]
T2 --> M2["component B"]
T3 --> M3["component C"]
classDef team fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
classDef comp fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
class T1,T2,T3 team;
class M1,M2,M3 comp;
Three teams will build three things, whether you planned three or not. The "inverse Conway move" is to design the teams you want first, and let the architecture follow. Boundaries that cut across teams never hold.
Before you extract a service, you must be able to name which of these genuinely differs across the boundary. If you cannot name one, keep it a module. Each of these is a legitimate reason; "it felt cleaner" is not.
flowchart TB
Q{"why split this off?"}
Q --> A1["scales
independently"]
Q --> A2["changes at a
different rate"]
Q --> A3["must fail
in isolation"]
Q --> A4["owned by a
different team"]
Q --> A5["different tech or
compliance need"]
A1 --> OK["a real seam ·
the split is earned"]
A2 --> OK
A3 --> OK
A4 --> OK
A5 --> OK
classDef q fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
classDef a fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
classDef ok fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
class Q q;
class A1,A2,A3,A4,A5 a;
class OK ok;
The interview-and-promotion habit: when someone proposes a service, ask "which axis?" If the honest answer is none of these five, the right design is a module in the monolith, not a new service.
flowchart TB
S{"should this be
its own service?"}
S -->|"no axis differs"| KEEP["keep it a module
in the monolith"]
S -->|"one real axis"| MAYBE{"can it cleanly
own its data?"}
MAYBE -->|"no, it shares tables"| KEEP
MAYBE -->|"yes, clean boundary"| SPLIT["extract a service"]
classDef q fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
classDef keep fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
classDef split fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
class S,MAYBE q;
class KEEP keep;
class SPLIT split;
The data question is the killer. If two would-be services can't stop sharing the same tables, they are not really separate, and splitting them just gives you a distributed monolith. Own the data or stay a module.
Once you commit to services, size matters. Make them too coarse and you keep the monolith's coupling. Make them too fine and a single user action becomes a dozen network hops. Aim for a service a team can own and reason about whole.
A good service is "a thing one team owns and can explain on a whiteboard." If a single click triggers a relay race across ten services, you have sliced too thin and bought yourself the worst of both worlds.
Instead of one service calling the next and waiting, a service emits an event to a broker and moves on. Anyone who cares subscribes. The producer does not know or wait for the consumers. This is decoupling in time, not just in space.
flowchart LR P["producer
order placed"] --> B["event broker
Kafka · SQS · Rabbit"] B --> C1["billing"] B --> C2["shipping"] B --> C3["email"] classDef p fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef b fill:#DCEBFE,stroke:#1F2937,color:#0E1726; classDef c fill:#D6F5E3,stroke:#1F2937,color:#0E1726; class P p; class B b; class C1,C2,C3 c;
Instead of phoning each department one by one and waiting on hold, you pin a note to the board: "order placed." Billing, shipping and email each read it when they are ready. Add a new department later and the producer never changes.
"Event-driven" hides three different patterns, and mixing them up causes real pain. Know which one you mean.
Notification is a doorbell: "someone's here," go check. State transfer is a parcel: everything you need is in the box. Event sourcing is keeping every receipt forever and recomputing your balance from them. Most teams want the first two; reach for the third deliberately.
When a workflow spans several services, who drives it? A central conductor (orchestration), or each service reacting to the last one's events (choreography)? Same trade you met in S6 with sagas.
flowchart TB
subgraph O["orchestration · one conductor"]
OR["orchestrator"] --> O1["step 1"]
OR --> O2["step 2"]
OR --> O3["step 3"]
end
subgraph C["choreography · react to events"]
E1["service 1"] -->|"event"| E2["service 2"]
E2 -->|"event"| E3["service 3"]
end
classDef orc fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
classDef cho fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
class OR,O1,O2,O3 orc;
class E1,E2,E3 cho;
Orchestration is an orchestra with a conductor: one place owns the order, easy to follow and debug. Choreography is dancers reacting to each other: beautifully decoupled, but nobody can show you the whole dance. Start with orchestration.
Take it further: no long-running service at all. A function spins up in response to an event, does one job, and disappears. You pay per call and never manage a server. Great for spiky, event-shaped work; awkward for steady, stateful, latency-sensitive paths.
flowchart LR EV["an event
http · queue · cron"] --> FN["function
spins up on demand"] FN --> OUT["does one job,
then disappears"] FN --> DB[("managed store")] classDef ev fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef fn fill:#D6F5E3,stroke:#1F2937,color:#0E1726; classDef db fill:#DCEBFE,stroke:#1F2937,color:#0E1726; class EV ev; class FN,OUT fn; class DB db;
Like a motion-sensor light: nobody there, nothing runs, no cost. Someone walks in, it switches on for exactly as long as needed. Brilliant for bursty jobs, a poor fit when you need the light on steadily or instantly (cold starts).
"Layered" and "hexagonal" describe how you organise code inside a unit. "Monolith" and "microservices" describe how you deploy it. They are different axes: a monolith can be beautifully hexagonal inside, and a clean internal design is what makes a future split possible.
flowchart LR IN["inbound adapters
http · cli · queue"] --> CORE["domain core
pure logic, no I/O"] CORE --> OUT["outbound adapters
db · email · payments"] classDef io fill:#FEF3C7,stroke:#1F2937,color:#0E1726; classDef core fill:#D6F5E3,stroke:#1F2937,color:#0E1726; class IN,OUT io; class CORE core;
Hexagonal (ports and adapters) keeps the business logic in the middle, pure, with the outside world plugged in at the edges. Do this inside your monolith and the day you extract a service, the seam is already drawn.
The worst of both worlds. You paid for the network, the ops, and the distributed data, but the services are still so coupled they must be deployed together and share a database. You took on every cost of splitting and kept every cost of coupling.
flowchart LR
A["service A"] -->|"sync call"| B["service B"]
B -->|"sync call"| C["service C"]
C -->|"sync call"| A
A -.->|"shared tables"| DB[("one database")]
B -.-> DB
C -.-> DB
classDef bad fill:#FEE4E2,stroke:#1F2937,color:#0E1726;
classDef db fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
class A,B,C bad;
class DB db;
You moved every room into its own cabin, then ran a hallway between them all and kept one shared fridge. Now you have the walk in the rain and you still fight over the milk. This is the most common way microservices go wrong.
If any of these is true, you have the costs of distribution without the independence. The fix is usually not more services, it is fixing the boundary, often by pulling pieces back together into a module.
You almost never rewrite a monolith into services at once; that is how rewrites die. Instead, put a router in front, then peel off one capability at a time into its own service, until the old monolith is gone or small.
flowchart LR
CL["clients"] --> F["router / façade"]
F -->|"most routes"| MONO["the monolith"]
F -->|"one peeled-off route"| SVC["new service"]
MONO --> DB[("shared DB, for now")]
SVC --> NDB[("its own DB")]
classDef cl fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
classDef mono fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
classDef svc fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
class CL,F cl;
class MONO,DB mono;
class SVC,NDB svc;
Named after a fig that grows around a tree and slowly replaces it. The router lets you move one route at a time, with the old system still running, so you can stop or reverse at any point. Safe, boring, and it actually finishes.
| Style | Deploy | Scaling | Data | Failure | Best when |
|---|---|---|---|---|---|
| Monolith | one unit | all together | one DB, easy txns | shared blast radius | early, small team, find the domain |
| Modular monolith | one unit | all together | per-module tables | shared, but contained | the default for most teams |
| Microservices | independent | per service | a DB per service | isolated, if done right | real scaling / team / failure axis |
| Event-driven | independent | per consumer | events + local state | decoupled in time | fan-out, async work, loose coupling |
Read top to bottom as "more independence, more complexity." Most products live happily on the top two rows for years. The bottom two are tools you reach for when a named pressure demands them.
Every trap is the same root error: splitting without a real axis. Name the axis first. If you can't, the answer is a module, not a service.
Take a system you know. Answer four things out loud:
No diagrams needed, just the four answers. If question two has no honest answer for some boundary, you have just found a service that should have stayed a module.
The spectrum runs monolith → modular monolith → microservices → event-driven. Default to the modular monolith, split only where a real axis (scaling, change rate, failure, team, compliance) diverges, give each service its own data, and watch for the distributed monolith.