Monolith vs Microservices
Short answer: start with a modular monolith. One deployable unit with strict internal module boundaries gives you most of what microservices promise — clear ownership, independent reasoning, replaceable components — without distributed transactions, network failure between every call, or the operational burden of running twenty services.
The industry has largely converged on this after a decade of experience. Microservices solve a team scaling problem: enabling many teams to deploy independently without coordinating. They do not make software simpler, faster, or more reliable. They make it distributed, and distribution introduces failure modes and complexity that a monolith simply doesn't have.
The failure mode to avoid is the distributed monolith: services split by technical layer or extracted before boundaries were understood, so they must be deployed together, share a database, and call each other synchronously in chains. You pay every cost of distribution and receive no independence. This is the common outcome of adopting microservices too early, and it's worse than either endpoint.
TL;DR
- Start with a modular monolith. One deploy, strict internal boundaries. It's the right default.
- Microservices solve organizational scaling, not technical performance or simplicity.
- The real costs: distributed transactions, network failure, tail latency amplification, operational overhead, debugging across services.
- A monolith is not spaghetti. Modularity is orthogonal to deployment topology.
- Split when teams block each other on deploys — not when the codebase feels large.
- Split along bounded contexts, never technical layers. Frontend/backend/database services is the anti-pattern.
- A shared database between services means they aren't independent. That's a distributed monolith.
- Use the strangler fig pattern to extract incrementally. Never rewrite.
Head-to-Head
What Microservices Actually Cost
These are the costs that get discovered after adoption, not before.
Transactions become sagas
This is the single largest cost and the most consistently underestimated. Every multi-entity operation becomes a distributed workflow with compensations, retry semantics, idempotency requirements, and a window of visible inconsistency. See Sagas & Distributed Transactions.
Every call can fail
Every in-process call that becomes a network call needs a timeout, a bounded retry with jitter, a circuit breaker, and a decision about what to do when it fails. Multiply by every call site.
Tail latency compounds
Fan-out amplifies this further: a request touching 20 services in parallel has an ~18% chance of hitting at least one p99. See Tail Latency.
Local development and debugging
A single stack trace becomes a trace across services, which requires distributed tracing to be already instrumented — and if it isn't, debugging is archaeology across four log streams.
Operational surface
The Modular Monolith
The default, and the option most often skipped. A monolith is not the same as an unstructured codebase.
Enforce the boundary mechanically, because a rule maintained by review discipline erodes within a quarter:
Crucially, this makes later extraction cheap: a module already communicating only through an interface can have that interface swapped for an HTTP or gRPC client without touching its consumers. You've kept the option without paying for it.
When to Actually Split
The signal to watch is deploy coordination cost. When shipping a change requires a release meeting because four teams' work is in the same artifact, that's the problem microservices solve. Codebase size alone is a modularity problem.
Team count matters more than service count: below roughly four or five teams contributing to one product, the coordination cost microservices remove is usually smaller than the distribution cost they add.
Split along bounded contexts, never layers
Each service owns its own database. Two services reading the same tables are one service with extra network hops — a schema change requires coordinating both, which is precisely the independence you were buying. See Domain-Driven Design and Team Topologies.
Extract with the strangler fig
Never rewrite. A rewrite means maintaining two systems while the business keeps changing both, and the estimate is always wrong by a large multiple. See Technical Debt.
Best Practices
Start with a modular monolith, with enforced boundaries
One deployable, strict module boundaries, an explicit api surface per module, and a lint rule that fails the build on a violation. This preserves the extraction option at almost no cost.
Enforce module boundaries mechanically
import-linter in Python, no-restricted-paths in JavaScript, ArchUnit in Java, or internal packages in Go. Without automation the boundary is aspirational — someone imports an internal type "just this once," and a year later the modules are inseparable.
Give each module its own schema, even inside the monolith
Separate PostgreSQL schemas per module, with no cross-schema foreign keys or joins. This is the highest-value preparatory step for future extraction, and it surfaces coupling you didn't know existed while it's still cheap to fix.
Split on team friction, not code size
Measure it: how often does a release require cross-team coordination? How long does a change wait for someone else's work? Those numbers justify a split. "The repo has 200k lines" does not.
One service, one database
If two services share tables, they are one service. A shared database is the defining characteristic of a distributed monolith and it eliminates the independence that justified the split.
Instrument distributed tracing before you split, not after
OpenTelemetry with trace context propagation, in place and verified. Debugging a cross-service issue without tracing is dramatically harder, and retrofitting it during an incident is not an option.
Prefer async messaging to synchronous chains
A chain of six synchronous calls compounds latency and means any one failure fails the request. Events over Kafka or NATS decouple availability, at the cost of eventual consistency you must design for. See Event-Driven Architecture.
Scale the monolith before distributing it
Vertical scaling, read replicas, caching, and query optimization take a monolith remarkably far, and they preserve transactions and in-process calls. Distribution is a one-way door. See Capacity Planning.
Common Mistakes
Microservices from day one
Splitting by technical layer
A shared database
Distributed transactions via two-phase commit
No timeouts on inter-service calls
Rewriting instead of strangling
FAQ
Are microservices dead?
No, and the consensus has shifted materially. The 2015-era default of "microservices for everything" has been replaced by "modular monolith first, extract when justified" — a position now advocated by many of the people who popularized microservices, and reflected in several prominent public migrations back to a monolith. Microservices remain the right answer for large organizations with many teams. They're no longer treated as an unqualified best practice.
How big is too big for a monolith?
Size isn't the metric. Shopify, GitHub, Stack Overflow, and Basecamp have run very large monoliths successfully. The relevant question is whether teams are blocking each other on deploys and whether module boundaries are being violated. A well-modularized million-line monolith with one team is fine; a 50k-line one with six teams fighting over releases is not.
What's a distributed monolith?
Services that must be deployed together — because they share a database, are coupled by synchronous call chains, or were split along technical layers rather than domain boundaries. You pay every cost of distribution (network failure, no transactions, operational overhead) and get no independent deployability. It's strictly worse than either a monolith or well-bounded microservices, and it's the most common outcome of splitting too early.
Can I use microservices with a small team?
You can, and it's usually a poor trade. Microservices are an organizational tool, and with one team there's no coordination cost to remove — you're adding distributed systems problems and operational surface to solve a problem you don't have. The exceptions are narrow: a genuinely different scaling profile for one component, or a regulatory isolation requirement.
How do I handle data consistency across services?
Accept eventual consistency and design for it explicitly. Sagas with compensating actions for multi-step workflows. The transactional outbox for reliable event publishing without dual writes. Idempotency on every consumer, because delivery is at-least-once. And a reconciliation process for the cases where compensation itself fails. This is real design work with no shortcut, and it's the cost most teams underestimate most severely. See Sagas and CQRS.
Should each microservice have its own repository?
Either works. A monorepo gives atomic cross-service changes, shared tooling, and one place to look — with Turborepo or Bazel handling per-service builds. Separate repos give harder boundaries and simpler per-service CI, with painful coordinated changes. Google, Meta, and Uber run monorepos at enormous scale; per-service repos are common too. It's a much smaller decision than the split itself.
What about serverless instead?
Functions are microservices with a smaller unit of deployment, so they inherit the same distribution problems — plus cold starts and per-invocation database connection pressure. They're excellent for event-driven, spiky, or genuinely independent workloads. For a request-serving application, a container on a managed platform is usually simpler. See Serverless Patterns.
Related Topics
- Microservices — In depth
- System Design — The broader decision space
- Domain-Driven Design — Finding the right boundaries
- Hexagonal Architecture — Module boundaries within a monolith
- Sagas & Distributed Transactions — The transaction replacement
- Event-Driven Architecture — Decoupling services asynchronously
- Team Topologies — Conway's law and why structure drives architecture
- Distributed Tracing — Non-negotiable before splitting
- Tail Latency — How network hops compound
- Technical Debt — Why the strangler fig beats a rewrite
- Docker vs Kubernetes — Running whichever you choose
- Idempotency — Required for at-least-once messaging