Monolith vs Microservices — 60% Spent on Service Calls
- You now understand what Monolith vs Microservices is and why it exists
- You've seen it working in a real runnable example
- Practice daily — the forge only works when it's hot 🔥
- A monolith is one deployable unit; microservices are independent services that communicate over a network.
- Monoliths win when your team is small, your domain is well-understood, and you need speed of change.
- Microservices win when you need independent scaling, polyglot stacks, or separate deploy cycles per team.
- The wrong choice adds 10x operational complexity: network latency, distributed tracing, and eventual consistency headaches.
- Most production failures come from premature splitting — adding service boundaries before the domain boundaries are clear.
- The modular monolith is the pragmatic third option: enforced boundaries without network overhead.
Quick Decision Cheat Sheet: Monolith vs Microservices
Team size under 10 engineers
Draw a dependency graph of your current codebase: jdeps -dotoutput graph.dot myapp.jarMeasure build time: mvn clean install -T 1C | grep 'BUILD'Multiple teams deploying independently
List shared tables: SELECT table_schema, table_name FROM information_schema.tables WHERE table_type='BASE TABLE';Count cross-service API calls: grep -r 'http://' src/ | wc -lUnpredictable scaling requirements
docker stats --no-stream (or kubectl top pods)curl -s http://localhost:8080/actuator/health | jqYou cannot deploy without breaking something unrelated
Find all `FOREIGN KEY` constraints pointing to tables owned by other modules.Check if any module directly reads another module's tables: `SELECT * FROM information_schema.views;`Production Incident
Production Debug GuideUse these symptom-to-action pairs to decide whether to stay monolithic or split into microservices
Every system design interview, every startup pitch deck, every 2am engineering debate eventually arrives at the same crossroads: should we build one big application or break it into small, independent services? This question isn't academic — the wrong answer has killed products, burned engineering teams, and wasted millions of dollars in cloud bills. Netflix famously spent years migrating from a monolith to microservices. Amazon did it in the early 2000s and invented AWS partly because of what they learned. The architecture you choose on day one will shape your team structure, your deployment pipeline, your on-call rotation, and your ability to hire. It matters enormously.
The core problem both architectures are trying to solve is the same: how do you build software that can grow without collapsing under its own weight? A monolith solves this by keeping everything in one place — simple to reason about, easy to test, one deployment. Microservices solve it by drawing hard boundaries — each service owns its own data, its own deployment, its own failure mode. The tension between them is really a tension between simplicity now and flexibility later.
By the end of this article you'll understand exactly how each architecture works under the hood, you'll see real code patterns that show the day-to-day differences, you'll have a concrete decision framework you can apply to your own project, and you'll be able to answer the tough system design interview questions that trip up even experienced engineers.
What Is Monolith vs Microservices?
Monolith and microservices are two ends of a spectrum. A monolith packages all logic into one deployable unit. Microservices split that logic into independent processes that talk over a network. But the real difference isn't the number of services – it's about how you manage dependencies.
In a monolith, modules communicate via method calls within the same process. That's fast – sub-microsecond latency. In microservices, they communicate over HTTP or message queues – milliseconds at best. That latency gap forces you to think differently about data, consistency, and failure.
Here's what that looks like in code. A monolith's order processing might call inventory directly:
```java // io.thecodeforge.monolith.OrderService public class OrderService { private final InventoryService inventory = new InventoryService();
public void placeOrder(Order order) { if (!inventory.hasStock(order.getProductId(), order.getQuantity())) { throw new InsufficientStockException(order.getProductId()); } // save order } } ```
In a microservices world, the same call becomes a network request:
```java // io.thecodeforge.microservices.order.OrderService public class OrderService { private final RestTemplate restTemplate = new RestTemplate();
public void placeOrder(Order order) { ResponseEntity<StockResponse> response = restTemplate.getForEntity( "http://inventory-service/api/stock/{productId}?quantity={qty}", StockResponse.class, order.getProductId(), order.getQuantity() ); if (!response.getBody().hasStock()) { throw new InsufficientStockException(order.getProductId()); } // save order } } ```
The monolith version is simpler, but the microservices version is more resilient – the inventory service can fail independently. That trade-off appears everywhere.
// io.thecodeforge.monolith.OrderService public class OrderService { private final InventoryService inventory = new InventoryService(); public void placeOrder(Order order) { if (!inventory.hasStock(order.getProductId(), order.getQuantity())) { throw new InsufficientStockException(order.getProductId()); } // save order } }
The Monolith: When One Box Is Enough
A monolith is a single deployable unit containing all the application's logic. It's the simplest architecture: you have one codebase, one build pipeline, one deployment. All modules – user management, inventory, payments – run in the same process and communicate via method calls.
This simplicity is its superpower. You don't need service discovery, distributed tracing, or eventual consistency. A single transaction spans the entire request without network overhead. For small teams and simple domains, nothing beats it.
But here's the catch: as the codebase grows, so does cognitive load. A 500,000-line monolith is hard to reason about. Deployment risk increases because any change touches the whole system. The monolith doesn't fail gracefully – a null pointer in the PDF generator can bring down the entire REST API.
Senior engineers know that monoliths are not a failure of vision. They're a strategic choice for the right phase of a product. Shopify ran a monolith for years. GitHub's core is still largely monolithic. What matters is that the monolith is well-structured: modules have explicit boundaries, shared code is minimal, and cross-module dependencies are enforced by convention or tooling.
Microservices: Independence at a Cost
Microservices decompose the application into independently deployable services that communicate over a network. Each service owns its own data, scales independently, and can be written in different languages. This buys you team autonomy, independent scaling, and fault isolation.
The cost is significant: network latency, distributed data consistency, complex observability, and the need for infrastructure like service meshes, API gateways, and message brokers. You trade simplicity for flexibility.
One hidden cost: you now need to manage inter-service contracts. Changing an API requires coordinating with downstream consumers. Without a robust versioning strategy and consumer-driven contracts, you'll break production more often than you'd like.
Also, debugging a request that touches five services means you need distributed tracing (Jaeger, Zipkin), structured logging across all services, and a centralised metrics pipeline. That's a lot of infrastructure before you write any business logic.
The Modular Monolith: The Third Option
A modular monolith keeps a single deployment but enforces strict module boundaries. Modules communicate through well-defined interfaces (Java modules, .NET assemblies, or simply package conventions). Data access is encapsulated within each module – no cross-module database queries.
This approach gives you many microservices benefits (code ownership, testability, concurrency) without the deployment complexity. Many successful systems (Shopify, GitHub, Stack Overflow) run this way. It's the perfect stepping stone: you can extract a module into a microservice later if needed.
Enforcing boundaries is the hard part. Without tooling, engineers will inevitably take shortcuts: a quick SELECT * FROM orders from the inventory module, a direct class import from a different bounded context. CI checks can prevent this – for example, a Gradle module that only exposes certain packages, or an ArchUnit test that verifies layer dependencies.
// io.thecodeforge.modularmonolith.ArchUnitTest import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; public class ModularMonolithBoundariesTest { @Test void inventory_should_not_depend_on_orders() { classes() .that().resideInAPackage("..inventory..") .should().onlyDependOnClassesThat() .resideInAnyPackage("..inventory..", "..shared..", "java..") .check(importedClasses); } }
- Each module owns its own data – no peeking into neighbours' tables.
- Communication happens through defined corridors – interfaces, not direct SQL.
- You can still renovate one apartment without moving out of the building.
- You only split into separate buildings (microservices) when you need distinct fire codes (deployment lifecycle).
The Decision Framework: 5 Questions to Answer Before Splitting
Use these five questions to decide whether your organisation is ready for microservices:
- Can your team operate the infrastructure? Microservices require CI/CD pipelines, container orchestration, monitoring, and incident management per service. If your ops team is two people, you're not ready.
- Do you have clear domain boundaries? Stable bounded contexts (from Domain-Driven Design) are essential. If you can't define what each service owns, you're not ready.
- Is the monolith actually hurting? Measure concrete signals: deployment frequency, mean-time-to-recovery, developer onboarding time. If these aren't causing pain, don't split.
- Can you run multiple databases? Each microservice should own its data. If you can't manage multiple DB engines, you'll end up with a distributed monolith.
- Is your team aligned with the architecture? Conway's Law: a system's architecture mirrors the communication structure of the organisation. Split services along team boundaries, not technical ones.
Let's apply this to a real scenario. Say you're a 25-person startup with a Rails monolith that takes 10 minutes to build and 20 minutes to test. The team is organised into three squads: payments, user management, and analytics. The payments squad deploys every two weeks because the full test suite must pass. That's a measurable pain point – question 3 is a 'yes'. Payments has a clear domain boundary (question 2), and the team is three developers who can own the full lifecycle (question 1 is borderline). You start by extracting the payments module as a microservice, leaving the rest monolithic. Six months later, you evaluate whether the additional operational cost was worth the release velocity gain.
Migration Pattern: The Strangler Fig in Practice
The strangler fig pattern is the safest way to migrate from monolith to microservices. You intercept incoming requests and route them to either the monolith or the new service, based on the URL path, feature flag, or user segment. Over time, the new service handles more cases, and the monolith 'shrinks' until it can be decommissioned.
Here's a concrete example using an NGINX reverse proxy:
``nginx # nginx.conf – io.thecodeforge.strangler server { listen 80; location /api/payments { proxy_pass http://payment-service:8080; } location / { proxy_pass http://monolith:3000; } } ``
This allows incremental extraction. You don't need a big-bang rewrite. You extract one endpoint at a time, run both systems in parallel, and roll back by re-routing to the monolith if the new service fails.
But beware: the strangler fig creates an implicit dependency between the monolith and the new service if they share data. You must ensure idempotency on both sides – a request that hits the monolith first and then the new service on retry must not duplicate effects.
server {
listen 80;
location /api/payments {
proxy_pass http://payment-service:8080;
}
location / {
proxy_pass http://monolith:3000;
}
}
| Aspect | Monolith | Microservices |
|---|---|---|
| Deployment | Single artifact, easy to deploy | Multiple services, complex orchestration |
| Scaling | Scale entire application even if one part is busy | Scale only the busy service, save costs |
| Development speed | Fast for small teams, slows down as codebase grows | Slower initial setup, faster per service as team scales |
| Fault isolation | Failure can bring down whole system | Failure is contained to one service |
| Data consistency | Strong consistency within one database | Eventual consistency across services |
| Operational complexity | Low – one set of logs, metrics, and monitoring | High – need distributed tracing, service mesh, etc. |
| Best for | Startups, small teams, stable domains | Large teams, high scale, polyglot requirements |
| Migration path | Start here, extract later using strangler fig | Requires more investment upfront; hard to reverse |
| Failure cost | Low – one process to debug, one set of logs | High – requires distributed tracing to find root cause |
🎯 Key Takeaways
- You now understand what Monolith vs Microservices is and why it exists
- You've seen it working in a real runnable example
- Practice daily — the forge only works when it's hot 🔥
- Monoliths are optimal for small teams and simple domains; microservices add complexity for growth.
- Use the 5-question decision framework before splitting: team size, domain boundaries, pain, data ownership, team structure.
- Consider a modular monolith as the pragmatic middle ground – it enforces boundaries without network cost.
- Premature splitting is the #1 architectural mistake: start monolithic, extract one service at a time.
- Use the strangler fig pattern for safe migration: extract endpoints incrementally, not whole modules.
- A distributed monolith is worse than a plain monolith – shared databases kill independence.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhen would you choose a monolith over microservices for a new project?Mid-levelReveal
- QWhat are the biggest operational challenges you've faced in a microservices architecture?SeniorReveal
- QExplain the strangler fig pattern and when you would use it to migrate from a monolith to microservices.SeniorReveal
- QWhat is a distributed monolith and how do you avoid it?Mid-levelReveal
- QHow do you decide when to extract a module from a monolith?SeniorReveal
Frequently Asked Questions
What is Monolith vs Microservices in simple terms?
Monolith vs Microservices is a fundamental concept in System Design. Think of it as a tool — once you understand its purpose, you'll reach for it constantly.
Is microservices always better than monolith?
No. Microservices are better only when you have multiple teams working independently, need to scale components separately, or require polyglot technologies. For most startups and small-to-medium products, a well-structured monolith is faster to build, debug, and deploy.
Can I start with a monolith and later migrate to microservices?
Yes, and that's the recommended approach. Ensure your monolith is modular with clear boundaries (e.g., separate packages/modules, no circular dependencies, shared database accessed only through service layers). Then extract modules one by one using the strangler fig pattern.
What is a distributed monolith and why is it bad?
A distributed monolith is a set of services that are tightly coupled – they share a database, require synchronous calls for every feature, and need coordinated deployments. It combines the complexity of microservices with the inflexibility of a monolith. It's worse than both because debugging is hard and you get none of the scaling or autonomy benefits.
What's the smallest team size that should consider microservices?
I wouldn't recommend microservices for fewer than 15-20 engineers. Even then, start with a modular monolith. The inflection point is usually when you have multiple teams that need independent deploy cycles. At that point, the communication overhead of the monolith outweighs the operational overhead of microservices.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.