Microservices solve an organizational problem, not a technical one. Choosing them for the wrong reason is the most expensive mistake in this repo.
Prerequisites: Availability, The 8 Fallacies Time to read: ~26 minutes
Your application is one codebase, one deployment, one database. It works. Then:
🚨 Notice what’s on that list: almost all of it is about people and process, not about throughput. That’s the correct reason to split, and it’s why the framing matters so much.
One deployable unit. All modules in one process, one codebase, usually one database.
🚨 A monolith is not “unstructured.” A well-built monolith has clear module boundaries, dependency rules, and separation of concerns. It just deploys as one artifact. The opposite of a monolith is distribution, not organization — and conflating the two is the most common confusion in this topic.
What it gives you, and these are substantial:
| Advantage | Why it matters |
|---|---|
| ACID transactions across everything | No sagas, no eventual consistency, no compensation logic |
| A function call, not a network call | ~1 ns, no timeout, no retry, no circuit breaker, no partial failure |
| Refactoring across boundaries | Your IDE renames it everywhere; the compiler finds every caller |
| One thing to deploy, monitor, and debug | A single stack trace tells the whole story |
| Simple local development | Run the app. Done. |
| No distributed systems problems | No Part 4 at all |
| Far cheaper | One deployment, no service mesh, no per-service infrastructure |
📐 Reality check: Stack Overflow served billions of page views a year from a handful of servers running a monolithic application. Shopify, GitHub, and Basecamp run large monoliths at significant scale. The monolith is not a scaling ceiling — it’s an organizational one.
Where it genuinely breaks down:
Independently deployable services, each owning its data, communicating over a network.
What you gain:
| Advantage | The real benefit |
|---|---|
| Independent deployment | 🚨 The main one. Teams ship without coordinating |
| Independent scaling | Scale only what’s hot |
| Technology freedom | The right tool per service |
| Fault isolation | If designed for it — see below |
| Team autonomy | Clear ownership; smaller cognitive scope per person |
| Independent failure and recovery | One service restarting doesn’t restart everything |
What you pay — and this list is longer than people expect:
🚨 1. Availability multiplies downward.
8 services in the request path, each 99.9% available:
0.999⁸ = 99.2% → ~70 hours of downtime per year
Worse than any individual service. → Availability
🚨 2. Every function call becomes a network call. ~1 ns becomes ~1 ms, and now it can fail, time out, be retried, arrive twice, or arrive out of order. Every arrow needs a timeout, a retry policy, a circuit breaker, and an idempotency story. → Part 4
🚨 3. No cross-service transactions. Business operations spanning services need sagas with compensating actions — real code, real edge cases, no isolation.
4. Distributed debugging. One request touches eight services. Without distributed tracing — which is now mandatory, not optional — you cannot answer “why was this slow?”
5. Data consistency becomes eventual. Services own their data, so the same fact exists in several places and they diverge.
6. Operational multiplication. 30 services × (CI pipeline + deploy + monitoring + alerts + on-call + runbooks + secrets + dashboards).
7. Versioning and compatibility. Every inter-service contract must survive independent deployment of both sides. → Versioning
8. Local development. Running 30 services on a laptop is a genuine problem that costs teams real productivity, and the solutions (mocks, shared dev environments, service virtualization) all have downsides.
9. Cost. More instances, more networking (cross-AZ traffic is billed), a service mesh, more observability data.
| Monolith | Microservices | |
|---|---|---|
| Deploy independence | ❌ | ✅ The reason to choose it |
| Transactions | ✅ ACID | ❌ Sagas |
| Latency between components | ~1 ns | ~1 ms + failure modes |
| Debugging | One stack trace | Distributed tracing required |
| Availability | One thing at 99.9% | Multiplies down |
| Scaling granularity | Whole app | Per service |
| Technology choice | One | Per service |
| Team scaling | Poor past ~50 engineers | ✅ Good |
| Operational cost | Low | High |
| Infrastructure cost | Low | 2–5× |
| Time to first feature | Fast | Slow |
“Organizations design systems that mirror their own communication structure.” — Melvin Conway, 1967
🚨 This is the actual determinant. If you have four teams and split into forty services, each team owns ten services and changes still require coordination — you got all the costs and none of the benefit. If you have forty teams sharing one monolith, coordination overhead dominates everything.
The rule that follows: “Number of services should roughly track number of teams, not number of nouns in your domain.”
The inverse Conway manoeuvre — deliberately organizing teams around the architecture you want — is how companies actually change their architecture successfully. Architecture follows org structure, so change the org structure first.
🎙️ “How many engineering teams are there? That’s the main input — microservices solve a coordination problem, so with three teams I’d stay with a modular monolith and revisit when team count makes deployment coordination the bottleneck.”
That question — “how many teams?” — is one of the strongest things you can ask in a design interview, because it shows you understand what the decision is actually about.
🚨 The best default for most systems, and proposing it confidently is a senior signal.
One deployable unit, with strictly enforced internal module boundaries:
app/
├── billing/ ← may only be accessed via billing/api.py
│ ├── api.py ← the public interface
│ ├── domain/
│ └── store/ ← its own tables; nothing else touches them
├── orders/
│ ├── api.py
│ └── ...
└── shipping/
Enforcement matters — without it, boundaries erode. Options: module systems (Java modules, .NET assemblies), package-private visibility, architecture tests (ArchUnit, import-linter), dependency rules in CI, or separate schemas per module with no cross-schema queries.
What you get:
🚨 This last point is the strongest argument. Domain boundaries are hard to get right up front, and in a monolith a wrong boundary is a refactor. In microservices it’s a migration project. Get the boundaries right cheaply first.
🎙️ “I’d start with a modular monolith with enforced boundaries and separate schemas per module. It gives us the design discipline of services without the distributed systems cost, and when a module genuinely needs independent deployment or scaling, extracting it is straightforward.”
Split when you have a concrete, named reason:
| Reason | Example |
|---|---|
| Team coordination is the bottleneck | Twelve teams; every release requires a coordination meeting |
| Radically different scaling | Video transcoding needs GPUs and 100 instances; the API needs 3 |
| Different availability requirements | Payments needs four nines; the recommendation engine doesn’t |
| Genuine technology need | ML inference in Python; the rest is Java |
| Compliance isolation | Card data must live in a PCI-scoped environment |
| A genuinely independent lifecycle | A partner integration that changes on someone else’s schedule |
Do not split because:
🚨 The most important warning: never start a greenfield project with microservices. You don’t yet know the domain boundaries, and getting them wrong at the service level is enormously expensive to correct. Martin Fowler’s “monolith first” argument is the standard reference, and it’s right.
There’s no line count. Useful heuristics:
🚨 The strongest test: “Does a typical feature require changing only this service?” If most features touch four services, the boundaries are wrong — you have a distributed monolith, the worst outcome available.
Nano-services are a real failure mode: services so small that any change spans several, so you have all the distributed cost and none of the independence.
Big-bang rewrites fail. The strangler fig pattern is the standard approach:
flowchart LR
C[Client] --> P[Proxy / façade]
P -->|most routes| M[Monolith]
P -->|extracted routes| S1[New service]
P -.->|later| S2[Next service]
🚨 Extract the data too. A “service” sharing the monolith’s database is not a service — it’s a distributed monolith with extra steps. This is the most common half-migration failure. → Strangler Fig
| Decision | Gain | Cost |
|---|---|---|
| Monolith | Simplicity, transactions, speed of development, low cost | Deployment coupling; whole-app scaling; blast radius |
| Modular monolith | Boundaries + simplicity + a migration path | Requires discipline and enforcement |
| Microservices | Independent deploy and scale, team autonomy | Distributed systems complexity, cost, availability multiplication |
| Splitting early | Boundaries enforced from day one | Boundaries are probably wrong, and expensive to fix |
| Splitting late | Boundaries validated by real usage | Coordination pain in the meantime |
1. Measure the coupling in a real system. Take the last 50 pull requests in any codebase you have access to. For each, count how many modules or services it touched. If most touch several, your boundaries are wrong — and that’s true whether they’re modules or services. This is a genuinely useful diagnostic and takes twenty minutes.
2. Feel the availability multiplication. Build a 4-service chain where each has a 1% random failure rate. Measure the end-to-end success rate — it’ll be ~96%, not 99%. Then add fallbacks to two of them and measure again.
3. Enforce a boundary. Take a monolith and add an architecture test that fails the build if
orders imports from billing/internal. Watch how many existing violations you find. That number is
the boundary erosion you didn’t know about.
4. Extract one module. Pick the most independent module in a codebase you know and actually try to extract it — including its data. Note where the shared tables, shared transactions, and hidden dependencies are. That exercise is what tells you whether a split is feasible.