Microservices became the default answer to “how should we build this?” sometime around 2016, and the industry has been quietly paying the bill ever since. We’ve reviewed enough architectures at IWWOMI to say this plainly: if you have fewer than 20 engineers, microservices are almost certainly the wrong choice. The rest of this post explains why, and when the math actually flips.
The Majestic Monolith Was Always the Right Default
DHH used the term “majestic monolith” in 2016 and got mocked for it. He was right. Basecamp, Shopify, Stack Overflow, GitHub — all of these ran (or still run) enormous monoliths well past the point where conference talks said they should have split up. The reason is unglamorous: a single deployable artifact is dramatically cheaper to reason about than a graph of services.
In a monolith, a function call is a function call. In microservices, that same call becomes a network hop with retries, timeouts, circuit breakers, serialization, version skew, and a distributed trace you’ll need to read at 2 a.m. The cost of that complexity is fixed; the benefit only shows up at a certain scale.
If your team can fit in a single Slack channel and still get work done, you do not have an architecture problem. You have a discipline problem. Microservices will make it worse, not better.
Start with a monolith. Keep it modular. We’ll get to what “modular” actually means in code below.
Conway’s Law Is Not a Suggestion
Melvin Conway observed in 1967 that “organizations produce designs which are copies of the communication structures of these organizations.” Sixty years later, this remains the single most underappreciated force in software architecture.
Your service boundaries will end up matching your team boundaries. If you have three teams, you will end up with three services — no matter what your domain model says. If you try to fight this, the services will leak responsibilities, on-call rotations will be miserable, and PRs will pile up across team lines.
This is why the question “should we use microservices?” is really the question “what does our org chart look like, and what do we want it to look like in 18 months?” Architecture decisions made in isolation from the org chart will be overturned by it.
A practical heuristic:
- 1–2 teams (under ~15 engineers): one monolith, full stop.
- 3–6 teams (~15–30 engineers): modular monolith, possibly one or two extracted services for genuinely different scaling profiles.
- 6+ teams (30+ engineers): microservices start to pay for themselves, assuming the DevOps maturity is there.
The middle range is where most companies live, and where most architecture mistakes happen.
When Microservices Actually Pay Off
There are three legitimate triggers. If you don’t have at least one of them, you’re cargo-culting.
1. Team size and deployment friction. When more than ~30 engineers commit to the same codebase, merge conflicts, CI queue times, and release coordination start eating real hours. Splitting deployable units along team lines recovers that time.
2. Genuinely different scaling profiles. Your video transcoding workload and your billing workload have nothing in common operationally. One needs GPUs and burst capacity; the other needs strong consistency and audit logs. Forcing them into the same deployable means you over-provision one and under-protect the other.
3. Deployment isolation for risk. Payments code that ships once a week with a four-eyes review should not share a deployment with the marketing CMS that ships fifteen times a day. Separating them is a regulatory and reliability win.
If your reasoning is “we want to use Go for some things and Python for others” or “the architecture diagram looks cleaner” — sit down. Those are not reasons.
The Hidden Costs Nobody Talks About in the Pitch Deck
Every microservices proposal undersells the operational surface area. Here’s the actual bill.
Distributed tracing is non-optional. The moment you have three services, you need OpenTelemetry instrumentation, a trace backend (Jaeger, Tempo, Honeycomb), and engineers trained to read flame graphs. Without this, debugging a 500ms latency regression becomes archaeology.
Network reliability is now your problem. TCP is not reliable enough. You need retries with exponential backoff, idempotency keys, circuit breakers (Hystrix-style), and timeouts tuned per call site. Each of these is a footgun if misconfigured. Set a timeout too low and you cascade failures; too high and you exhaust connection pools.
Transaction boundaries disappear. This is the big one. In a monolith, BEGIN; ... COMMIT; gives you atomicity for free. Across services, you get the saga pattern — and sagas are hard.
The Saga Pattern, Concretely
Say you’re processing an order: reserve inventory, charge payment, create shipment. In a monolith:
with db.transaction():
inventory.reserve(order)
payment.charge(order)
shipment.create(order)
Three services means three databases means no shared transaction. You need a saga — a sequence of local transactions with compensating actions for rollback:
# Orchestrator-based saga
async def place_order_saga(order):
try:
reservation = await inventory_service.reserve(order)
try:
charge = await payment_service.charge(order)
try:
await shipment_service.create(order)
except Exception:
await payment_service.refund(charge.id)
raise
except Exception:
await inventory_service.release(reservation.id)
raise
except Exception as e:
await order_service.mark_failed(order.id, reason=str(e))
raise
This is the simple version. The real version handles partial failures of the compensations themselves, duplicate events, and the case where payment.refund succeeds but the network call fails. You need an outbox table, idempotent handlers, and a way to replay sagas from any point. None of this exists in your monolith because you never needed it.
If your domain doesn’t have natural compensating actions — “unsend an email,” “uncalculate a tax” — sagas get philosophically and practically painful. Think hard before splitting services across that kind of boundary.
Service Mesh: Almost Always Premature
Istio, Linkerd, and Consul Connect solve real problems: mTLS between services, traffic splitting, retries and timeouts at the platform layer, observability without per-service instrumentation. They also add a sidecar to every pod, a control plane to operate, and a learning curve that will eat a quarter of someone’s year.
Rule of thumb: under 50 services, you do not need a service mesh. You need an API gateway (Kong, Envoy as a standalone, or a cloud load balancer), library-level retry/timeout logic, and disciplined observability. Adding Istio to a 12-service deployment is how you turn a manageable architecture into an unmanageable one.
The exception: regulated environments where mTLS between services is mandatory. Even then, evaluate whether a simpler mesh like Linkerd does the job before reaching for Istio.
The Modular Monolith Is the Right Answer for Most Teams
This is the architecture nobody puts on a conference slide because it’s not exciting. It’s also what we recommend to most clients.
A modular monolith is a single deployable artifact organized around bounded contexts, with strict internal boundaries enforced by the build system or by convention. Modules talk through explicit interfaces, not by reaching into each other’s tables. The database is shared but schemas are owned per module.
src/
modules/
billing/ # owns billing.* tables, exposes BillingAPI
inventory/ # owns inventory.* tables, exposes InventoryAPI
shipping/ # owns shipping.* tables, exposes ShippingAPI
shared/
auth/
events/ # in-process event bus
Crucially, an in-process event bus replaces Kafka. Direct calls replace HTTP. ACID transactions still work. When a module genuinely needs to be extracted — different scaling, different team, different release cadence — you have a clean seam to cut along.
Shopify operates this way at extraordinary scale. So does GitHub. You are almost certainly not bigger than them.
For data layer concerns inside a modular monolith, our guide to database optimization covers schema isolation, connection pooling, and read replicas — most of the perceived “database doesn’t scale” problems are solved here before you reach for a service split.
Database-Per-Service: The Trade You Probably Don’t Want
The textbook says each microservice owns its database. The textbook is right in principle and brutal in practice.
You lose: joins across business entities, referential integrity, single-snapshot reporting, cheap analytics queries. You gain: independent schema evolution, blast radius isolation, freedom to pick the right storage per service (Postgres for billing, DynamoDB for sessions, ClickHouse for events).
In practice, most teams adopt a hybrid: a service owns its write path, but read models are materialized into a shared analytics store via CDC (Debezium, Fivetran). This is the pragmatic compromise. Pure database-per-service with no shared read store works for Netflix and almost nobody else.
DevOps Overhead Goes Up By an Order of Magnitude
A monolith needs: one CI pipeline, one deployment target, one set of secrets, one log stream, one dashboard. Microservices need all of that per service, plus orchestration, service discovery, mesh or gateway config, distributed tracing, per-service alerting, dependency graphs, and a release coordination story.
If your DevOps practice is two people running Heroku, you are not ready. Read our DevOps best practices guide and our cloud migration guide before you split a single service. Get those foundations right first.
Security surface area also multiplies. Every internal API is a potential attack path. Service-to-service auth, secrets rotation, and network policy enforcement become full-time concerns. Our notes on building secure web applications apply here at every service boundary, not just at the edge.
The honest version of “microservices scale your engineering org” is “microservices scale your engineering org if you have already solved CI, observability, on-call, and platform tooling. Otherwise they scale your incidents.”
Further Reading That’s Actually Worth Your Time
Martin Fowler’s original microservices article from 2014 still holds up — particularly the prerequisites section, which most teams skip. Sam Newman’s Building Microservices (2nd edition, O’Reilly) is the working reference; the chapter on splitting a monolith is the most useful 40 pages on the topic.
What you should not read: any blog post titled “How We Moved to Microservices” written by a company under 100 engineers. Survivorship bias is doing most of the work in those.
Need an Architecture Review?
If you’re staring at a fast-growing monolith and wondering whether it’s time to split — or you’re three years into a microservices migration and the velocity gains never showed up — talk to us. We do paid architecture reviews that produce a concrete decision document, not a 200-slide deck. Get in touch and we’ll book a call.
The goal is never “more services.” The goal is shipping faster with fewer incidents. Sometimes that means splitting. More often than the industry will admit, it means staying put and fixing the modules you already have.