Skip to content
Home System Design Monolith vs Microservices — 60% Spent on Service Calls

Monolith vs Microservices — 60% Spent on Service Calls

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Fundamentals → Topic 5 of 10
Developers spent 60% of time on service-to-service communication instead of business logic.
⚙️ Intermediate — basic System Design knowledge assumed
In this tutorial, you'll learn
Developers spent 60% of time on service-to-service communication instead of business logic.
  • 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 🔥
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • 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.
🚨 START HERE

Quick Decision Cheat Sheet: Monolith vs Microservices

When you're in the middle of an architecture debate, use this checklist to quickly assess whether your context favours a monolith or microservices.
🟡

Team size under 10 engineers

Immediate ActionDefault to monolith. Do not even consider microservices until you have at least 2 teams.
Commands
Draw a dependency graph of your current codebase: jdeps -dotoutput graph.dot myapp.jar
Measure build time: mvn clean install -T 1C | grep 'BUILD'
Fix NowIf build time is under 5 minutes and team is 10 or fewer, stay monolithic.
🟡

Multiple teams deploying independently

Immediate ActionCheck if they deploy to different endpoints or share a single database. Shared DB kills microservices benefits.
Commands
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 -l
Fix NowIf more than 10 cross-service calls per feature, you have a distributed monolith. Merge or redesign boundaries.
🟡

Unpredictable scaling requirements

Immediate ActionIdentify which component consumes the most CPU/memory under load.
Commands
docker stats --no-stream (or kubectl top pods)
curl -s http://localhost:8080/actuator/health | jq
Fix NowIf only one component needs scaling, extract that component as a microservice; leave the rest monolithic.
🟡

You cannot deploy without breaking something unrelated

Immediate ActionExamine your codebase for shared mutable state. The most common culprit: a shared database schema with no ownership boundaries.
Commands
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;`
Fix NowIntroduce a service layer or database per module. If that's too much, at least enforce read-only access and use events for state changes.
Production Incident

The Startup That Split Too Early

A 15-person engineering team decided to build a microservices architecture for a new e-commerce platform. Eight months later, the project was months behind schedule, the team was burning out, and the system was slower than the monolithic prototype they had thrown out.
SymptomDevelopers spent 60% of their time on service-to-service communication, deployment pipelines, and debugging network failures instead of writing business logic.
AssumptionThe team assumed microservices would make them faster by allowing parallel development across multiple small services.
Root causeThe team had not identified stable domain boundaries. Services were tightly coupled by shared database tables and chatty API calls. Every new feature required changes to five or six services simultaneously.
FixThey merged the services back into a single deployable monolith. They kept logical modularity (packages, bounded contexts) but removed the network overhead. After the merge, feature delivery speed increased by 3x.
Key Lesson
Do not split until your team and domain are large enough that the monolith's friction is measurable.Start with a well-structured monolith. Extract services only when you feel the pain of the monolith in a specific area.Network calls are never zero cost. Every microservice boundary adds latency, failure modes, and debugging complexity.A modular monolith can give you many of the same benefits without the organizational debt.
Production Debug Guide

Use these symptom-to-action pairs to decide whether to stay monolithic or split into microservices

Deploying one feature requires rebuilding and redeploying the entire applicationFirst check if the monolith has clear module boundaries. If not, start by modularising internally (Java modules, .NET assemblies). Only extract if that doesn't help.
A single bug in one part of the system brings down all functionalityIdentify the failing component. If it's a resource-bound service (image processing, PDF generation), extract it first. Isolate failure domains one at a time.
Different parts of the system require different database types (SQL vs NoSQL) or scaling strategiesThis is a strong signal for microservices. Each service should own its data store. Start by extracting the part that has the most different persistence requirement.
Team is growing and merge conflicts on a single codebase are slowing everyone downBefore splitting, try feature flags and branch policies. If that fails, extract services along team boundaries (Conway's Law). Each service should map to one team's area of ownership.
You're spending more time coordinating releases than writing codeEvaluate whether the release cadence is the bottleneck. If yes, consider a microservice for the component that changes most often. Keep the rest monolithic.

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.java · JAVA
1234567891011
// 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
    }
}
🔥Critical Insight
The added complexity of network calls is not just about latency – it's about every call potentially failing. In a monolith, a method call never times out. In microservices, you need retries, circuit breakers, and idempotency.
📊 Production Insight
In production, the monolith's method call is safe but fragile – a memory leak in any module can crash the whole process.
Microservices isolate failures but introduce partial failures: a timeout in inventory might leave the order in 'pending' forever.
Rule: never assume a network call succeeded unless the response is idempotent and the caller can recover from a half-failed request.
🎯 Key Takeaway
The fundamental difference is communication cost – method calls vs network calls.
Monoliths are simpler; microservices are more expensive but more resilient.
Choose based on whether you need to isolate failure or you can accept process-wide crashes.

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.

📊 Production Insight
The monolith's critical failure mode is not performance – it's team coordination. When 50 engineers work on the same codebase, merge conflict resolution takes half the sprint.
Deployments become a bottleneck: you need to coordinate release notes, rollback plans, and feature flags across the entire application.
Rule: if your CI pipeline takes more than 15 minutes and you have more than 30 engineers, it's time to consider extraction.
🎯 Key Takeaway
Monoliths are optimal until team size and codebase complexity cross a threshold.
Invest in modularity upfront – it's the cheapest insurance against future pain.
Do not split until you can point to a specific, measurable friction that microservices would reduce.

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.

📊 Production Insight
The most expensive microservices bug I've seen: a cascading failure where one service's transient error caused another to retry aggressively, which overloaded the database, which caused a third service to timeout, which kicked off a compensation saga that created duplicate orders.
Tracing the root cause took three teams two days.
Rule: every microservices architecture must have distributed tracing and structured request IDs from day one.
🎯 Key Takeaway
Microservices amplify team speed but also amplify infrastructure cost.
The hidden overhead is not compute – it's debugging time and contract management.
Only adopt when the team is ready to invest in observability and API governance.

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.java · JAVA
12345678910111213
// 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);
    }
}
Mental Model
Mental Model: The Apartment Building
Think of a modular monolith as an apartment building: shared plumbing (the deployment) but locked doors between units (modules).
  • 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).
📊 Production Insight
The biggest trap in modular monoliths is the 'just this once' cross-module query. A single shared table join between inventory and orders turns a modular design into a ball of mud.
Enforce boundaries with automated tests (ArchUnit, NDepend). If it passes code review, it's too late – the coupling is already in the codebase.
Rule: a modular monolith without enforced boundaries is just a mess with better intentions.
🎯 Key Takeaway
Modular monolith gives microservices' discipline without microservices' pain.
Enforce boundaries with CI checks – don't rely on developer discipline alone.
If you cannot modularise your monolith, you will absolutely fail at microservices.

The Decision Framework: 5 Questions to Answer Before Splitting

Use these five questions to decide whether your organisation is ready for microservices:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

📊 Production Insight
The most common failure scenario I've seen: an organisation that answers 'yes' to questions 1 and 2 but ignores question 4. They split into microservices but keep a single shared PostgreSQL database. Each service uses its own schema, but schema migrations require coordinated releases. The result is a distributed monolith – all the complexity of microservices with none of the independence.
Rule: if you share a database, you are not doing microservices. Period.
🎯 Key Takeaway
Ask yourself: does the monolith hurt more than microservices would?
Use the 5-question framework to avoid premature splitting.
Start with a modular monolith – it's the safest path to microservices if you ever need to go there.

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.

``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.

nginx.conf · NGINX
123456789
server {
    listen 80;
    location /api/payments {
        proxy_pass http://payment-service:8080;
    }
    location / {
        proxy_pass http://monolith:3000;
    }
}
⚠ Watch Out for Shared State
The strangler fig pattern is not a silver bullet. If the new service and the monolith both write to the same database table, you have a shared mutable state problem. Use distinct data stores from day one, and use event-driven propagation for updates.
📊 Production Insight
I've seen strangler fig migrations fail because the new service didn't handle all edge cases that the monolith did. For example, the monolith handled a 'payment declined' scenario by sending an email and logging. The new service only returned a 400 error, but the user got no email. The team had to rebuild the email notification logic.
Rule: for each endpoint you extract, copy all side effects (notifications, logging, analytics) to the new service before cutting over.
🎯 Key Takeaway
Use the strangler fig pattern for safe, incremental migration.
Extract one endpoint at a time, not entire modules.
Always handle side effects in the new service before routing traffic to it.
🗂 Monolith vs Microservices Comparison
Key differences at a glance
AspectMonolithMicroservices
DeploymentSingle artifact, easy to deployMultiple services, complex orchestration
ScalingScale entire application even if one part is busyScale only the busy service, save costs
Development speedFast for small teams, slows down as codebase growsSlower initial setup, faster per service as team scales
Fault isolationFailure can bring down whole systemFailure is contained to one service
Data consistencyStrong consistency within one databaseEventual consistency across services
Operational complexityLow – one set of logs, metrics, and monitoringHigh – need distributed tracing, service mesh, etc.
Best forStartups, small teams, stable domainsLarge teams, high scale, polyglot requirements
Migration pathStart here, extract later using strangler figRequires more investment upfront; hard to reverse
Failure costLow – one process to debug, one set of logsHigh – 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

    Splitting into microservices before identifying domain boundaries
    Symptom

    Services that require synchronous calls for every operation, resulting in high latency and tight coupling – a distributed monolith.

    Fix

    First, define bounded contexts using Domain-Driven Design (DDD). Split only when the monolith's deployment or team coordination becomes a measurable bottleneck.

    Sharing a single database across microservices
    Symptom

    Schema changes require coordinated deployments across services, removing the independence that microservices are supposed to provide.

    Fix

    Each microservice must own its data. Use separate databases or schemas, and enforce access only through service APIs.

    Adopting microservices for 'scalability' when the system handles 100 requests per second
    Symptom

    Team spends months setting up CI/CD, service mesh, and observability before writing any business logic.

    Fix

    Measure your actual traffic. If you can serve your load from a single moderate VM, start monolithic. Extract only when you need to scale a specific component independently.

    Ignoring Conway's Law – splitting by technical layers instead of team boundaries
    Symptom

    A 'services' team builds the API, a 'data' team owns the database, and every change requires multiple teams to coordinate. You've just recreated the monolith's problems at a higher cost.

    Fix

    Organise services around business capabilities. Each service should be owned by one team that can make end-to-end changes.

Interview Questions on This Topic

  • QWhen would you choose a monolith over microservices for a new project?Mid-levelReveal
    I'd choose a monolith when the team size is under 10 engineers, the business domain is relatively simple and stable, and there's no need for independent scaling of components. Monoliths are also better when time-to-market is critical because they avoid the initial complexity of service discovery, distributed logging, and CI/CD pipelines. The key is to design the monolith with modular boundaries so that extraction to microservices is possible later if needed.
  • QWhat are the biggest operational challenges you've faced in a microservices architecture?SeniorReveal
    Three things: 1) Observability – debugging a single user request that spans 10 services requires distributed tracing (e.g., Jaeger), structured logging, and centralised metrics. 2) Data consistency – ensuring eventual consistency without sagas turning into distributed transactions is hard; plenty of projects end up with 'read-your-writes' problems. 3) Deploy coordination – even with CI/CD, sometimes you need to deploy multiple services in order, which increases risk. I've seen teams spend 30% of their sprint on ops instead of features.
  • QExplain the strangler fig pattern and when you would use it to migrate from a monolith to microservices.SeniorReveal
    The strangler fig pattern involves gradually replacing specific monolith functionality with new microservices. You intercept requests to the monolith (usually via a reverse proxy or service mesh) and route them to the new service if it exists. Over time, the monolith 'shrinks' as more features are replaced. I used this pattern at a previous company to migrate the payments module from a Rails monolith to a Go service. The key is to keep both systems running in parallel until the new service is proven stable. It's safer than a big-bang rewrite because you can roll back by re-routing to the monolith.
  • QWhat is a distributed monolith and how do you avoid it?Mid-levelReveal
    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. To avoid it: each service must own its data (separate schemas or databases), minimise synchronous call chains (use events for coordination), and enforce independent deployability (no shared libraries that bundle domain logic).
  • QHow do you decide when to extract a module from a monolith?SeniorReveal
    I ask a series of questions: 1) Does this module change independently of other modules? If yes, consider extraction. 2) Does this module have a well-defined bounded context? If not, define it first. 3) Can the team that owns it manage its own deployment cycle? If yes, go ahead. 4) Is the extraction cost (new pipeline, new monitoring, new data store) worth the improvement in release velocity or fault isolation? I start with the module that causes the most pain – usually the one that's slowest to build or most frequently changed.

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.

🔥
Naren Founder & Author

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.

← PreviousHorizontal vs Vertical ScalingNext →SQL vs NoSQL in System Design
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged