Skip to content

Monolith vs Microservices vs Modular Monolith

Core Concept LearningJuly 4, 20265 min readUpdated July 21, 2026

Every new project faces the same question: one deployable application or separately deployed services? A monolith minimizes distributed-systems overhead; microservices can enable independent ownership and scaling but add network failures, tracing, and release coordination. A modular monolith keeps one deployment while enforcing internal boundaries that may later support extraction.

Company stage and headcount do not decide this mechanically. Choose from measured scaling bottlenecks, release coupling, ownership boundaries, reliability requirements, and operational capability. This article compares the three styles and gives evidence you can collect before splitting.

If services are justified, use the microservices design patterns to shape their boundaries and failure handling. Choose each synchronous or asynchronous hop with the service communication protocol map.

Three architecture styles compared
Three architecture styles compared

Monolith

A monolith is a single deployment and often one process and database. Features use in-process calls, which simplifies local development, transactions, and refactoring. It is often efficient while independent deployability is not worth its operating cost.

Pressure appears when evidence shows shared failure domains, release queues, or resource contention. One module can affect the process, and coarse scaling replicates the whole unit. Those are possible pressures, not inevitable outcomes: modular boundaries and safer deployment techniques can extend a monolith's useful life.

Skimmer: choose the boundary whose cost matches today's constraint.
PatternUse whenMain trade-off
MonolithOne release and failure domain are acceptableSimple operations; coarse scaling and deployment
Modular monolithDomain boundaries matter before independent deploys doBoundaries need enforcement; still one runtime unit
MicroservicesA boundary needs independent ownership, scaling, or isolationNetwork failure, data contracts, and multi-service operations
Monolith: all modules in one process
Monolith: all modules in one process

Quick reference

  • Good fit when one deployment cadence and failure domain are acceptable and the workload scales together.
  • Strengths: simple deployment, easy debugging, fast local development, no network latency between modules.
  • Weaknesses: scaling is all-or-nothing, deployment risk grows, team coordination overhead, tight coupling.
  • Keep modules in separate packages even inside a monolith to prepare for future extraction.
  • Use feature flags and canary deploys to reduce deployment risk as the monolith grows.

Remember this

Start with a monolith when speed and simplicity matter more than independent scaling.

Modular Monolith

A modular monolith keeps one deployment but enforces bounded contexts inside the codebase. The Users module owns its tables and exposes a public interface — other modules call UsersService.create(), not UsersRepository directly. Cross-module communication goes through well-defined APIs, not shared database joins.

It is a strong candidate before splitting because it tests whether boundaries are real without paying network and multi-deployment costs. Extraction is still a migration: an in-process interface does not automatically solve remote latency, partial failure, data ownership, or backward-compatible contracts.

Modular monolith: bounded modules, one deployment
Modular monolith: bounded modules, one deployment

Quick reference

  • Good fit when domain boundaries are clear but independent deployment or scaling has not earned its cost.
  • Strengths: one deploy, enforced module boundaries, easier extraction path, no distributed systems overhead.
  • Weaknesses: requires discipline — teams must resist cross-module shortcuts, still one scaling unit.
  • Use package-level boundaries (e.g. /modules/users, /modules/orders) with lint rules blocking cross-imports.
  • Each module owns its database schema — no shared tables between domains.
  • Communicate between modules via in-process events or explicit service interfaces.
Enforced package boundary — one deployment
1// modules/orders/public.ts is the only exported surface.2export interface Orders {3  place(input: PlaceOrder, idempotencyKey: string): Promise<Order>;4}5 6// modules/checkout/place-checkout.ts7export async function checkout(input: Checkout, orders: Orders) {8  return orders.place(input.order, input.requestId);9}10 11// Architecture test: checkout may import modules/orders/public,12// but CI rejects imports from modules/orders/internal or its tables.
Thin service cut — bounded remote failure
1async function placeOrder(input: PlaceOrder, requestId: string) {2  const response = await fetch("http://orders.internal/orders", {3    method: "POST",4    headers: {5      "content-type": "application/json",6      "idempotency-key": requestId,7    },8    body: JSON.stringify(input),9    signal: AbortSignal.timeout(2_000),10  });11 12  if (response.status === 409) {13    return response.json(); // same key: return the recorded result14  }15  if (!response.ok) throw new Error(`orders_unavailable:${response.status}`);16  return response.json();17}18 19// Verify: repeat the same requestId; exactly one order is created.20// Stop Orders; checkout fails within 2s instead of hanging.

Remember this

Choose a modular monolith when enforceable boundaries solve today's coupling without requiring distributed deployment.

Microservices

Microservices split the application into independently deployable services, each owning its data and communicating over the network through APIs or messages. Teams can deploy and scale a service independently when ownership and contracts support that autonomy.

The cost is operational complexity. A checkout may cross several services and data stores, requiring timeouts, tracing, contract compatibility, and recovery from partial failure. Adopt that cost when a concrete boundary needs independent ownership, deployment, scaling, or isolation and the team can operate it.

Microservices: independent deployable services
Microservices: independent deployable services

Quick reference

  • Good fit when specific services require independent release, ownership, scaling, or failure isolation.
  • Strengths: independent scaling, team autonomy, technology diversity, fault isolation.
  • Weaknesses: network latency, distributed transactions, operational overhead, harder local dev.
  • Start extraction with the most independent, highest-traffic module — not the core domain.
  • Invest in observability (OpenTelemetry, centralized logging) before splitting services.
  • Use an API Gateway as the single entry point for external clients.
Cross-service call with no timeout — a hang becomes everyone's outage
1// Order Service calling Payment Service — no timeout, no idempotency2async function chargeCustomer(orderId: string, amountCents: number) {3  const res = await fetch("http://payments.internal/charge", {4    method: "POST",5    body: JSON.stringify({ orderId, amountCents }),6  });7  return res.json();8  // A slow Payment Service holds this request open indefinitely —9  // and a retry with no idempotency key risks a second charge.10}
Bounded timeout + idempotency key — a timeout becomes 'unknown,' not 'failed'
1async function chargeCustomer(orderId: string, amountCents: number) {2  try {3    const res = await fetch("http://payments.internal/charge", {4      method: "POST",5      headers: { "idempotency-key": orderId },6      body: JSON.stringify({ orderId, amountCents }),7      signal: AbortSignal.timeout(2_000),8    });9    if (!res.ok) throw new Error(`payment_failed:${res.status}`);10    return res.json();11  } catch (err) {12    if (err instanceof DOMException && err.name === "TimeoutError") {13      // Unknown, not failed — query by the same idempotency key before retrying.14      return checkPaymentStatus(orderId);15    }16    throw err;17  }18}19// Verify: kill Payment Service mid-call — Order Service fails fast at 2s20// instead of hanging, and a retry never double-charges the same order.

Remember this

Microservices solve organizational scaling problems — adopt them when team boundaries demand it, not because it is trendy.

Recover One Partial Failure Across Services

Trigger. During checkout, Order Service calls Payment Service to charge the customer, and the call times out after 2 seconds — the network dropped the response, or Payment Service itself was slow. Symptom. Order Service doesn't know whether the charge actually succeeded on Payment Service's side before the timeout fired; treating the timeout as a definite failure and retrying blindly risks charging the customer twice, while treating it as a definite success without proof risks never charging them at all.

Root mechanism. A network timeout is fundamentally ambiguous — the request could have failed before reaching Payment Service, succeeded but the response was lost on the way back, or still be processing. This is exactly the boundary a monolith's in-process call never has to cross: an in-process function call doesn't leave this kind of ambiguity behind. Recovery: Order Service queries Payment Service for the charge's status using the same idempotency key it sent — Payment Service either confirms a charge exists for that key or confirms none does, resolving the ambiguity definitively. Verification: kill Payment Service mid-call in a test environment and confirm Order Service's retry logic queries status rather than blindly re-submitting the charge. Prevention: every cross-service call on a path with a side effect needs an idempotency key from the start — treating this as an edge case to add later is how double-charges make it to production.

Checkout crosses three services: Payment Svc times out mid-request
Checkout crosses three services: Payment Svc times out mid-request

Quick reference

  • Trigger: a cross-service network call times out with no way to know if the far side completed.
  • Symptom: blind retry risks a duplicate side effect; blind success assumption risks a missed one.
  • Root mechanism: a network timeout is ambiguous in a way an in-process function call never is.
  • Recovery: query the downstream service's actual state by the same idempotency key before deciding to retry.
  • Prevention: idempotency keys belong on every cross-service call with a side effect from day one, not added after the first incident.

Remember this

A network timeout between services is genuinely ambiguous — recovery means querying the downstream service's actual state by idempotency key, never assuming either success or failure from silence.

Key takeaway

Choose from observable constraints, not headcount thresholds. A monolith or modular monolith remains valid while one release unit meets reliability and scaling needs; extract a service only when the boundary has an owner, data contract, failure plan, and measured reason for independent operation.

Practice (30 min): pick one module and record deploy frequency, change-failure rate, p95 resource use, incidents caused by the shared process, and cross-team wait time for four weeks. Write an extraction threshold before collecting data. If it is not met, keep the module in-process and add an architecture test that enforces its boundary.

Share:

Related Articles

Decoupling microservices using Event-Driven Architecture (EDA) requires choosing an asynchronous messaging backbone. Eng

Read

In high-concurrency microservices architectures, preventing race conditions when multiple stateless worker instances acc

Read

System design interviews evaluate a candidate's ability to architect scalable, resilient, and cost-effective distributed

Read

Keep learning

Follow a structured path or browse all courses to go deeper.