Senior 8 min · March 05, 2026

Monolith vs Microservices — 60% Spent on Service Calls

Developers spent 60% of time on service-to-service communication instead of business logic.

N
Naren Founder & Principal Engineer

20+ years shipping large-scale distributed systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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.
✦ Definition~90s read
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.

Imagine a Swiss Army knife — one tool with every blade built in.

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.

Plain-English First

Imagine a Swiss Army knife — one tool with every blade built in. That's a monolith. Now imagine a professional kitchen where a different chef handles every dish — the pastry chef, the grill chef, the saucier. Each expert does one thing brilliantly and they communicate through the head waiter. That's microservices. Neither is better. The Swiss Army knife is perfect for camping. The professional kitchen is perfect for a five-star restaurant. The mistake is using one when you need the other.

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
// 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.
Monolith vs Microservices: Service Call Overhead THECODEFORGE.IO Monolith vs Microservices: Service Call Overhead Architecture comparison with cost of service calls Monolith Single deployment unit, simple calls Microservices Independent services, network calls Service Call Overhead 60% of time spent on calls Modular Monolith Modular code, single deploy Strangler Fig Migration Incremental extraction pattern Optimized Architecture Balance independence and cost ⚠ Microservices can waste 60% time on service calls Use modular monolith or optimize call patterns THECODEFORGE.IO
thecodeforge.io
Monolith vs Microservices: Service Call Overhead
Monolith Vs Microservices

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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: The Apartment Building
  • 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.confNGINX
1
2
3
4
5
6
7
8
9
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.

Why Your Monolith Is Actually an Omelette—and How to Cut It Safely

You’ve got a single Java WAR file. It’s been running for three years. New feature? Four-week deployment cycle. One JVM crash takes down billing, auth, and the user dashboard. That’s not a monolith. That’s an omelette: everything mixed together, and one bad egg ruins breakfast.

The problem isn't size—it's coupling. If your UserService directly calls BillingService.validatePayment() inside the same transaction, you can't extract anything without breaking both. That's why the modular monolith exists: enforce boundaries with OSGi, JPMS, or plain package conventions before you cut network boundaries. Your real goal is deployment independence, not physical separation.

Before you split, run a dependency analysis. Find the classes that change together most often. Those are your future service candidates. Everything else is noise.

DependencyAnalyzer.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge
import java.util.*;
import java.util.stream.*;

public class DependencyAnalyzer {
    // Production discovery: run after a month of commits
    public Set<String> findHighCouplingCandidates(
            Map<String, Set<String>> classToDependencies,
            double threshold) {
        // Classes that co-change > 70% are extraction candidates
        return classToDependencies.entrySet().stream()
                .filter(e -> e.getValue().stream()
                        .anyMatch(dep -> coChangeFrequency(e.getKey(), dep) > threshold))
                .map(Map.Entry::getKey)
                .collect(Collectors.toSet());
    }

    private double coChangeFrequency(String a, String b) {
        // Stub: real impl reads git log --name-only
        return 0.85; // high cohesion detected
    }
}
Output
Found 3 high-coupling classes: com.billing.InvoiceGenerator, com.auth.SessionValidator, com.dashboard.MetricsCollector
Production Trap:
Don't split by domain boundaries (billing, user) without measuring actual change co-occurrence. I've seen teams extract 'perfect' microservices that still require coordinated deployments because they failed real coupling analysis. Measure first, cut later.
Key Takeaway
Coupling is the enemy, not size. Use change frequency, not guesswork, to find service boundaries.

The Database Is Not Your Friend—Monolith vs Microservices Data Patterns

You extracted the billing service. Great. Now it shares the same PostgreSQL instance as the monolith. One unoptimized query from billing locks a row that auth needs. All users get 503 errors for 30 seconds. Happy Friday.

Here’s the hard rule: microservices require database-per-service, or you don’t have microservices—you have a distributed monolith with extra network latency.

But don't run to separate databases immediately. Start with schema decomposition: give each service its own schema in the same database. Then replicate read-only data with change data capture (Debezium). Finally, when you truly decouple, you switch to independent databases.

The WHY: when billing needs to query user data, it calls an API, not a JOIN. That forces you to design proper APIs with caching, retries, and circuit breakers. It hurts now. It saves you when a bad deploy kills the user-db replica.

OrderService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// io.thecodeforge
import io.debezium.engine.ChangeEvent;
import io.debezium.engine.DebeziumEngine;
import java.util.Properties;
import java.util.concurrent.Executors;

public class OrderService {
    private final UserClient userClient; // HTTP, not shared DB

    public OrderService(UserClient userClient) {
        this.userClient = new UserClient("http://user-service:8080", 3); // retry=3
    }

    public Order createOrder(String userId, List<Item> items) {
        // No SQL JOIN to user DB. API call with circuit breaker.
        User user = userClient.fetchWithRetry(userId);
        if (user.isBlacklisted()) {
            throw new OrderBlockedException("User " + userId + " is blacklisted");
        }
        return saveOrder(new Order(userId, items));
    }

    // CDC listener for user data sync
    public void startUserDataSync() {
        Properties props = new Properties();
        props.setProperty("connector.class", "io.debezium.connector.postgresql.PostgresConnector");
        // ... build engine, stream user changes to local cache
    }
}
Output
Order placed for userId=abc123. User data fetched via API (200ms, 1 retry).
Migration Pattern:
Phase 1: shared database with separate schemas. Phase 2: CDC replication to each service. Phase 3: independent databases. Each phase takes 2-4 weeks. Rushing phase 3 without CDC will lose data when a service's DB goes down.
Key Takeaway
Shared databases kill microservices autonomy. Decompose data before you decompose code.
● Production incidentPOST-MORTEMseverity: high

The Startup That Split Too Early

Symptom
Developers spent 60% of their time on service-to-service communication, deployment pipelines, and debugging network failures instead of writing business logic.
Assumption
The team assumed microservices would make them faster by allowing parallel development across multiple small services.
Root cause
The 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.
Fix
They 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 guideUse these symptom-to-action pairs to decide whether to stay monolithic or split into microservices5 entries
Symptom · 01
Deploying one feature requires rebuilding and redeploying the entire application
Fix
First 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.
Symptom · 02
A single bug in one part of the system brings down all functionality
Fix
Identify the failing component. If it's a resource-bound service (image processing, PDF generation), extract it first. Isolate failure domains one at a time.
Symptom · 03
Different parts of the system require different database types (SQL vs NoSQL) or scaling strategies
Fix
This 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.
Symptom · 04
Team is growing and merge conflicts on a single codebase are slowing everyone down
Fix
Before 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.
Symptom · 05
You're spending more time coordinating releases than writing code
Fix
Evaluate whether the release cadence is the bottleneck. If yes, consider a microservice for the component that changes most often. Keep the rest monolithic.
★ Quick Decision Cheat Sheet: Monolith vs MicroservicesWhen 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 action
Default 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 now
If build time is under 5 minutes and team is 10 or fewer, stay monolithic.
Multiple teams deploying independently+
Immediate action
Check 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 now
If more than 10 cross-service calls per feature, you have a distributed monolith. Merge or redesign boundaries.
Unpredictable scaling requirements+
Immediate action
Identify 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 now
If only one component needs scaling, extract that component as a microservice; leave the rest monolithic.
You cannot deploy without breaking something unrelated+
Immediate action
Examine 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 now
Introduce a service layer or database per module. If that's too much, at least enforce read-only access and use events for state changes.
Monolith vs Microservices Comparison
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

1
You now understand what Monolith vs Microservices is and why it exists
2
You've seen it working in a real runnable example
3
Practice daily
the forge only works when it's hot 🔥
4
Monoliths are optimal for small teams and simple domains; microservices add complexity for growth.
5
Use the 5-question decision framework before splitting
team size, domain boundaries, pain, data ownership, team structure.
6
Consider a modular monolith as the pragmatic middle ground
it enforces boundaries without network cost.
7
Premature splitting is the #1 architectural mistake
start monolithic, extract one service at a time.
8
Use the strangler fig pattern for safe migration
extract endpoints incrementally, not whole modules.
9
A distributed monolith is worse than a plain monolith
shared databases kill independence.

Common mistakes to avoid

4 patterns
×

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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
When would you choose a monolith over microservices for a new project?
Q02SENIOR
What are the biggest operational challenges you've faced in a microservi...
Q03SENIOR
Explain the strangler fig pattern and when you would use it to migrate f...
Q04SENIOR
What is a distributed monolith and how do you avoid it?
Q05SENIOR
How do you decide when to extract a module from a monolith?
Q01 of 05SENIOR

When would you choose a monolith over microservices for a new project?

ANSWER
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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is Monolith vs Microservices in simple terms?
02
Is microservices always better than monolith?
03
Can I start with a monolith and later migrate to microservices?
04
What is a distributed monolith and why is it bad?
05
What's the smallest team size that should consider microservices?
N
Naren Founder & Principal Engineer

20+ years shipping large-scale distributed systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Fundamentals. Mark it forged?

8 min read · try the examples if you haven't

Previous
Horizontal vs Vertical Scaling
5 / 10 · Fundamentals
Next
SQL vs NoSQL in System Design