Tech · 6 min read
Microservices vs Monolith: How to Actually Decide
When a monolith is the right answer, when microservices earn their keep, and why most teams pick wrong because they're optimizing for the wrong constraint.
By Jarviix Engineering · Apr 19, 2026
The microservices-vs-monolith debate produced more bad architecture in the 2010s than almost any other tech trend. Teams of five engineers split into 30 services because Netflix did. The bill came due in operational complexity, distributed-systems bugs, and "where does this logic actually live" archaeology.
This post is a calm guide to when each is right — and how to recognize you've picked wrong.
What each architecture actually optimizes for
A monolith optimizes for single-team velocity and operational simplicity. One repo, one deploy, one runtime, one process to debug. Adding a new feature touches one codebase and ships in one release. Refactoring across modules is a single PR.
Microservices optimize for team independence and selective scaling. Each team owns a service and can deploy on their own schedule. A service that needs more capacity scales independently. A team that wants a different language or framework can have one. The cost: every interaction across services becomes a network call, with all the failure modes that implies.
Notice neither list says "performance". The performance argument for microservices is mostly fictional — well-tuned monoliths are routinely faster, because every cross-service call is a network round trip you didn't need.
The real reason to break a monolith
The decision is almost always about organizational scaling, not technical scaling.
A monolith is great when:
- One team can fit in one room (or one Slack channel).
- Coordinating a deploy means one engineer's calendar invite.
- Onboarding a new developer means one repo to learn.
- "Where does X live?" has one obvious answer.
A monolith breaks down when:
- Five teams want to ship features independently and each is blocked on the others' deploys.
- One team's bug ships in a release that shouldn't include their changes.
- The build takes 40 minutes because it tests everyone's code.
- Code ownership becomes ambiguous and bugs land in no-one's queue.
- A team needs to scale one workload (image processing) without scaling the rest of the application.
Notice these are all coordination costs, not technical limits. The right time to split is when coordination cost exceeds operational cost — and that's a function of team size, deploy frequency, and feature concurrency, not lines of code.
What microservices actually cost
The honest list of bills you pay when you go microservices:
- Distributed systems bugs. Network failures, retries, timeouts, partial failures. Things that worked in-process now require careful design.
- Eventual consistency. Data spread across services means writes propagate over time. Either you accept staleness or you build complex coordination (sagas, two-phase commit-ish patterns).
- Operational tooling. Service discovery, mesh, distributed tracing, log aggregation, service-aware monitoring — all things you didn't need with a single binary.
- Deployment surface. N services × M environments × K versions in flight. Release coordination becomes a real engineering activity.
- Testing. Integration tests cross services. Local development requires running (or mocking) the whole graph. Test environments are expensive to maintain.
- Latency. Every service-to-service call is a network hop. Trace 12 services per request and you're paying serious tail latency.
- Skill demand. Every team now needs operational maturity — on-call, monitoring, deploys. Previously this was one team's job.
The wins exist — independent deploys, fault isolation, polyglot freedom — but they're contingent on having enough scale to amortize the costs.
The modular monolith
The architecture most teams should actually pick: a single deploy unit with strong internal modularity.
Concretely:
- One repo, one binary, one deploy.
- Code is organized into modules with explicit interfaces (think Java packages, Python sub-packages, .NET assemblies).
- Modules can only call each other via their public interfaces, not by reaching into each other's internals.
- Tests run per module; the build is fast.
- The same modules can later be extracted into services if and when team scaling demands it.
This gives you:
- The velocity of a monolith.
- The clean boundaries of microservices (so future extraction is easy).
- The ability to evolve module-by-module rather than committing to "we are now a microservices org" up front.
Most successful "we migrated to microservices" stories are actually "we built a modular monolith first, then extracted the modules where it paid off."
When microservices are unambiguously right
A few cases where the decision is easy:
- Different scaling profiles. One workload needs GPUs, the rest don't. One needs 100x the capacity at certain hours. Splitting them is obvious.
- Different runtime needs. ML inference in Python, video processing in Go, business logic in Java. Trying to make all three coexist in one process is misery.
- Multiple teams shipping independently and stepping on each other's deploys. The cost is real and recurring.
- Strong fault isolation requirements. A bug in module A must not be able to crash the entire system. Process isolation is the cleanest enforcement.
- Sensitive workloads. PCI-scope code, regulated data — easier to compliance-isolate as its own service.
When monolith is unambiguously right
- Pre-product-market-fit. You don't yet know what the boundaries are. Premature decomposition locks you into wrong ones.
- Small team. Under ~10 engineers, a monolith is almost always the right call. The operational savings dwarf the velocity loss.
- Tight transactional consistency. When most operations need ACID across what would be multiple services, the cost of distributing them is brutal.
- Constrained ops budget. You don't have an SRE team, you can't run a service mesh, you'd rather not be paged at 3am for cascading service failures.
Migration patterns
If you've decided to extract from a monolith:
- Strangler fig. Don't rewrite. Add new functionality alongside the monolith. Migrate one capability at a time. The monolith shrinks; the services grow.
- Anti-corruption layer. When extracting a service, put a translation layer between it and the monolith. The monolith continues to use its existing interfaces; the new service has clean ones.
- Database extraction first. The hardest part is usually splitting the database. Often you extract the data path before the application path.
- One service at a time. Resist the urge to extract three services in parallel. Each extraction has its own pain; do them sequentially.
Three rules, regardless of choice
- Architecture decisions are reversible, with effort. Pick the simpler thing now and migrate later if needed. Don't pick complexity for theoretical future scale.
- Conway's Law is real. Your services will mirror your team structure. Design org and architecture together or you'll fight the org chart with your codebase.
- Operational maturity precedes architectural complexity. If you can't reliably deploy and observe one service, you can't deploy and observe twenty. Earn the right to microservices first.
What to read next
If you're thinking about microservices, event-driven architecture is the natural next read — most successful microservices systems are also event-driven. Microservices observability covers the operational layer that makes them survivable. The Uber HLD writeup is a worked example of a service-oriented architecture where domain boundaries (rider, driver, dispatch, payments) genuinely earn the operational cost.
Frequently asked questions
Are monoliths bad?
No. A well-structured monolith is the right architecture for most teams under ~30 engineers. The Bad Monolith stereotype is bad code organization, not the deployment model.
When does it make sense to break the monolith?
When team coordination cost (deploys, releases, ownership) is consistently higher than the cost of operating a few services. Usually somewhere past 30-50 engineers, sometimes earlier if you have very different scaling profiles.
What about a 'modular monolith'?
Often the best of both worlds — strong internal boundaries (modules with explicit interfaces) and one deploy unit. You get most of the testing/refactoring benefits of microservices without the operational tax.
Read next
Apr 19, 2026 · 6 min read
Event-Driven Architecture: When Events Pay Off (and When They Don't)
What event-driven really means, the patterns that work — events vs commands, choreography vs orchestration, sagas, outbox — and the failure modes nobody warns you about.
Apr 18, 2026 · 8 min read
System Design Basics: The Building Blocks Every Backend Engineer Should Know
A practical, no-fluff tour of the building blocks behind every large system — load balancers, caches, databases, queues, CDNs — and the trade-offs that decide how to wire them together.
Apr 19, 2026 · 6 min read
API Versioning Strategies: URL, Header, and the Trade-offs Nobody Tells You
URL versioning, header versioning, content negotiation, and 'no versioning at all' — what each costs, what each gets you, and how to pick a strategy you won't regret in three years.