System Design · Architecture Decisions

Microservices vs Monolith: When to Choose Which

An honest look at what microservices cost, why the modular monolith is the right default for most teams, how to find service boundaries that hold, and how to migrate safely when the time comes.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 16 min read
Illustration comparing one large monolith block with many small connected microservice cubes on a balance
TL;DR
  • Microservices are an organisational scaling tool more than a technical one. They let many teams deploy independently, at the cost of network calls, distributed data, and a large operations bill.
  • The right default for most products and teams under ~30–50 engineers is a modular monolith: one deployable, strict module boundaries, each module owning its tables.
  • Extract a service when there is a concrete reason: independent scaling, different reliability or tech needs, or teams blocking each other.
  • Draw boundaries around business capabilities / bounded contexts, never around technical layers or database tables. Bad boundaries produce a distributed monolith.
  • Migrate with the strangler fig pattern, one capability at a time, with data ownership moving along with the code.

"Should this be microservices?" is one of the most expensive questions in software, and it is usually answered by fashion rather than by analysis. Microservices solved real problems at companies with thousands of engineers. They also created real problems at companies with fifteen. This guide lays out what each architecture really costs, a decision framework you can apply, how to find boundaries that will not need redrawing, and what publicly documented migrations (in both directions) teach.

Monolith, Modular Monolith, Microservices: Definitions That Matter

Option 1

Monolith ("big ball of mud" variety)

One codebase, one deployable, one database. Any code can call any other code and read any table. Simple to start; becomes hard to change as coupling grows.

Option 2

Modular monolith

One deployable, but organised into modules with enforced boundaries: public interfaces, private internals, each module owns its tables. In-process calls, one deployment, clear seams for later extraction.

Option 3

Microservices

Many independently deployable services, each owning its data, communicating over the network (HTTP/gRPC or messaging). Each is typically owned by one team.

The important distinction is not "one process vs many". It is coupling. A well-modularised monolith is far easier to evolve than a set of services that share a database and must be deployed in lockstep. That last thing has a name, the distributed monolith, and it is the worst of both worlds.

The Real Costs of Microservices

Articles listing "pros and cons" usually understate the costs, because those costs are spread across many teams and show up months later. Here is what you sign up for.

1. The network is now in every call path

An in-process call takes nanoseconds and cannot partially fail. A network call takes on the order of a millisecond inside a data centre, can time out, can succeed on the server while the client sees a failure, and can be retried into a duplicate. A request that fans out through 5 services sequentially pays that latency and failure probability 5 times. If each hop has 99.9% success, a 5-hop chain succeeds about 99.5% of the time before retries. Every call now needs timeouts, retries with backoff, idempotency, and circuit breakers.

2. Distributed data and lost transactions

Each service owns its database, so operations spanning services lose ACID guarantees. "Create order and reserve inventory atomically" becomes a saga with compensations, an outbox, and idempotent consumers (see saga pattern and distributed transactions). Reporting that used to be a SQL join now requires a data pipeline into a warehouse.

3. Operational overhead per service

Every service needs: a CI/CD pipeline, container images, deployment config, autoscaling settings, dashboards, alerts, log routing, on-call ownership, dependency upgrades, security patching, secrets, and documentation of its API. Multiply by the number of services. Platform teams exist to amortise this cost, and you need one sooner than you think.

4. Observability becomes mandatory

"Why was this checkout slow?" used to be one stack trace. Now it needs distributed tracing with propagated trace IDs across every hop, correlated logs, and per-service metrics (see observability: logs, metrics and tracing).

5. Testing and local development get harder

Running the system on a laptop may require a dozen containers. End-to-end tests become slow and flaky. Teams move to contract testing (Pact) and test environments per branch, which are themselves infrastructure to maintain.

6. Changes that cross services are slow

If a feature touches three services owned by three teams, it now needs coordinated API changes, backwards-compatible rollout, and three release trains. Microservices make changes within a boundary faster and changes across boundaries slower. That is why boundaries matter so much.

7. Infrastructure cost

Each service has baseline resource usage (JVM heap, sidecars, minimum replicas for availability). Twenty services each running three replicas for redundancy is 60 pods before any real load. Serialization and network traffic add CPU and bandwidth costs.

⚠️
The microservice premiumMartin Fowler's phrase for this: microservices carry a fixed productivity tax that only pays off once the system and organisation are complex enough. Below that point, they make you slower.

The Real Benefits (When They Apply)

None of this means microservices are a mistake. They are the right tool when these benefits are worth the costs:

  • Independent deployment for many teams. With 300 engineers in one monolith, the release train becomes the bottleneck: one team's broken test blocks everyone's release. Services let each team deploy many times a day without coordination.
  • Independent scaling. During IPL, a streaming platform's playback and live-score services need to scale by orders of magnitude while account settings barely change. Scaling only the hot path is cheaper (see Hotstar live streaming design).
  • Fault isolation. A memory leak in the recommendations service should not take down payments. In a monolith, every module shares a process and its fate (bulkheads within a monolith help but are weaker).
  • Technology fit. A Go service for a high-throughput gateway, Python for ML inference, Java for core business logic.
  • Different compliance or security zones. Payment card data handling (PCI DSS) is easier to scope when it lives in a small isolated service.
  • Ownership clarity. A service with one owning team is easier to keep healthy than a shared module everyone touches.

Notice that most benefits are about people and organisation. That is Conway's Law in action: systems end up mirroring the communication structure of the organisations that build them. Microservices work when service boundaries match team boundaries.

The Modular Monolith: The Default for Most Teams

A modular monolith captures most of the design benefits of microservices (clear ownership, explicit interfaces, isolated data) without the distributed systems tax. Shopify has written publicly about choosing to modularise its large Rails monolith rather than split it into services, and built a tool (Packwerk) to enforce module boundaries.

Rules that make a monolith modular

  1. Modules map to business capabilities: ordering, payments, catalog, delivery. Not controllers, services, repositories.
  2. Each module exposes a small public API (interfaces, DTOs). Everything else is internal.
  3. Each module owns its tables. No other module queries them directly. Ideally separate schemas in the same database.
  4. Cross-module communication goes through the API or in-process events, not shared entities.
  5. Boundaries are enforced by tooling, not good intentions: Java modules (JPMS), ArchUnit tests, Spring Modulith, Packwerk, Nx module boundaries, or separate Gradle/Maven modules.

What it looks like in Java

Java (package layout)
com.example.shop
├── ordering
│   ├── api
│   │   ├── OrderingApi.java          // public interface
│   │   └── OrderPlaced.java          // public event record
│   └── internal                      // not accessible to other modules
│       ├── OrderService.java
│       ├── OrderRepository.java      // owns schema "ordering"
│       └── OrderEntity.java
├── payments
│   ├── api
│   │   └── PaymentsApi.java
│   └── internal
│       └── ...
└── catalog
    ├── api
    └── internal
Java (ArchUnit boundary test)
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.domain.JavaClasses;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

class ModuleBoundaryTest {
    private final JavaClasses classes =
        new ClassFileImporter().importPackages("com.example.shop");

    @Test
    void paymentsMustNotTouchOrderingInternals() {
        noClasses().that().resideInAPackage("..payments..")
            .should().dependOnClassesThat().resideInAPackage("..ordering.internal..")
            .check(classes);
    }

    @Test
    void orderingMustNotTouchPaymentsInternals() {
        noClasses().that().resideInAPackage("..ordering..")
            .should().dependOnClassesThat().resideInAPackage("..payments.internal..")
            .check(classes);
    }
}

Inside ordering, placing an order calls paymentsApi.authorize(...), an in-process interface call. If payments later becomes a service, you replace the implementation of PaymentsApi with an HTTP or gRPC client and the calling code barely changes. The seams are already there.

💡
Test of a good module boundaryCould you move this module into its own service by changing only the implementation of its public API and pointing it at its own database? If yes, your monolith is modular. If it would require untangling shared tables and entities, it is not.

Decision Framework: Monolith or Microservices?

Ask these questions in order. The first few "no" answers usually settle it.

QuestionIf yesIf no
Do you have product-market fit and a reasonably stable domain?Boundaries can be drawn with confidenceMonolith: boundaries will move as you learn
Are there more than ~4–6 teams working on the same codebase, blocking each other's releases?Services can decouple themModular monolith
Does one component have very different scaling or load patterns?Extract that componentScale the monolith horizontally
Does one component need different reliability, security or compliance isolation?Extract itKeep it in
Do you have platform capabilities (CI/CD templates, container orchestration, tracing, on-call)?You can absorb the operational costBuild these first or stay monolithic
Is a component a natural fit for a different language or runtime (ML, streaming)?Separate service makes senseSame runtime is fine

A rough maturity path

  1. Seed to early growth (1–15 engineers): one deployable, modular from day one. Separate only truly different workloads (an async worker process, maybe an ML service).
  2. Growth (15–60 engineers): still mostly monolith; extract 2–5 services with clear reasons (payments, notifications, search, media processing).
  3. Scale (many teams): services aligned to teams and domains, a platform team, standard templates, API gateway, service mesh if needed.

The headcount figures are rough guides, not laws; a 10-person team building a data-intensive platform may legitimately need several services.

Decomposing by Bounded Context (Finding Service Boundaries)

The most consequential decision is where to cut. Domain-Driven Design's bounded context is the best tool: a boundary within which a term has one consistent meaning and model.

Example: "Order" in a food delivery company

ContextWhat "order" means hereData it cares about
OrderingThe customer's cart turned into a purchaseItems, prices, customer, address, offers applied
Restaurant operationsA ticket to prepareItems, prep time, special instructions, kitchen status
DeliveryA pickup and drop jobPickup and drop locations, rider, ETA, distance
PaymentsAn amount to collect and settleAmount, payment method, gateway reference, refunds
SupportA case historyTimeline, complaints, refunds issued

A single shared Order class with 80 fields serving all five is how monoliths become balls of mud, and splitting that class into services without separating the models is how distributed monoliths are born. Each context should own its own model, sharing only IDs and the events that connect them (OrderPlaced, OrderReady, OrderDelivered). See how these contexts interact in Swiggy/Zomato system design.

Heuristics for good boundaries

  • High cohesion inside, low chattiness across. If two candidate services call each other on every request, they belong together.
  • A boundary should own a business capability end to end, including its data, rules and API.
  • Align with teams. One team per service; a team can own several.
  • Avoid entity services. "CustomerService", "ProductService" that are thin CRUD wrappers around tables force every business operation to orchestrate many calls.
  • Look at change history. Files that change together in commits should live together.

Signs of a distributed monolith

  • Several services read and write the same database tables.
  • A typical feature requires coordinated deploys of three or more services.
  • Long synchronous call chains (A → B → C → D) for a single user request.
  • A shared "common" library containing domain models that every service must upgrade in lockstep.
  • An outage in one non-critical service takes down the whole product.

Migrating with the Strangler Fig Pattern

Named by Martin Fowler after vines that grow around a host tree until they replace it, the strangler fig pattern migrates incrementally instead of via a big-bang rewrite (which historically has a poor track record).

Phase 1 Phase 2 Phase 3 Clients Clients Clients │ │ │ ┌──▼───────┐ ┌────▼─────┐ ┌────▼─────┐ │ Proxy │ │ Proxy │ │ Proxy │ └──┬───────┘ └─┬──────┬─┘ └─┬──────┬─┘ │ all /payments│ │ rest /payments│ │ /orders ... ┌──▼───────┐ ┌───────▼─┐ ┌──▼───────┐ ┌───────▼─┐ ┌──▼──────┐ │ Monolith │ │Payments │ │ Monolith │ │Payments │ │ Orders │ └──────────┘ │ service │ └──────────┘ │ service │ │ service │ └─────────┘ └─────────┘ └─────────┘ (monolith retired)
  1. Put a routing layer in front. An API gateway or reverse proxy that can route by path, header or percentage. See API gateway design.
  2. Pick the first capability carefully. Something valuable, relatively self-contained, with few inbound dependencies. Notifications or search are classic first extractions; the core order flow is not.
  3. Create a seam inside the monolith first. Refactor so all access to that capability goes through one interface. This step is often most of the work.
  4. Build the new service behind the same contract.
  5. Move the data. Give the service its own database. Sync data during transition via CDC from the monolith's tables, then flip ownership so the service is the writer and the monolith reads via API or events. Data migration is the hardest part; do not skip it by letting the new service read the old tables forever.
  6. Shift traffic gradually. Shadow traffic (compare responses), then 1%, 10%, 50%, 100%, with a quick rollback switch.
  7. Delete the old code. Migrations that never remove the old path double the maintenance cost.

Related techniques: branch by abstraction (swap implementations behind an interface inside the monolith), an anti-corruption layer (translate between the legacy model and the new service's model so the old model does not leak in), and parallel run (run both and compare outputs for risky logic like pricing).

Case Studies: Migrations in Both Directions

These are drawn from public engineering blogs and talks; details are as those companies described them.

Netflix: monolith to microservices

Netflix has described how a major database corruption in 2008 that disrupted DVD shipping pushed it to move from a monolithic application in its own data centres to a cloud-based, service-oriented architecture on AWS, a migration that took several years. With a large engineering organisation and extreme scale variation, independent deployment and fault isolation paid off, and Netflix built much of the tooling (Eureka, Hystrix, Zuul) that popularised the pattern.

Segment: microservices back to a monolith

Segment's 2018 post "Goodbye Microservices" described splitting its event-delivery destinations into a separate service and queue per destination, growing to well over a hundred services. Shared library versions drifted across services, operational load grew with each new destination, and a small team could not keep up. They consolidated the destinations into a single service and reported major improvements in developer productivity.

Amazon Prime Video: serverless microservices to a monolith (for one component)

In 2023 the Prime Video team wrote about an audio/video quality monitoring tool built as distributed components orchestrated by AWS Step Functions with S3 for intermediate frames. Moving the pipeline into a single process removed the orchestration and data-transfer overhead, and the team reported reducing infrastructure cost for that component by over 90%. The lesson is not "microservices are bad" but that a high-volume, tightly coupled pipeline should not pay network costs between every step.

Uber: from thousands of services to domains

Uber wrote about growing to thousands of microservices and the resulting complexity in understanding dependencies. Its response, Domain-Oriented Microservice Architecture (DOMA), groups related services into domains with a single gateway each and layers that restrict dependencies: essentially reintroducing coarser boundaries on top of fine-grained services.

Shopify: deliberately a modular monolith

Shopify has written about keeping its core commerce application as a large Rails monolith, organised into components with enforced boundaries, rather than splitting it into services, while still running some separate services where justified.

✅
The common threadEvery successful story matches architecture to organisation and workload. Netflix and Uber had thousands of engineers; Segment's team was small relative to its service count; Prime Video's component was a single tight pipeline. Granularity should follow team structure and coupling, not trends.

How This Comes Up in Interviews

Architecture questions appear in system design rounds ("would you build this as microservices?"), in senior behavioural rounds ("tell me about an architecture decision you made"), and in backend rounds at companies running many services (see microservices interview questions).

Questions to prepare

  1. "Your startup has 8 engineers. The CTO wants microservices. What do you advise?"
  2. "How would you split this e-commerce monolith? Which service first?"
  3. "How do two services share customer data?"
  4. "How do you handle a transaction spanning orders and inventory services?"
  5. "What's a distributed monolith and how would you detect one?"
  6. "How would you migrate a 10-year-old monolith without downtime?"

Model answer for question 1

"I'd advise a modular monolith. With 8 engineers, the main risk is product iteration speed, and microservices would add CI/CD, observability and distributed-transaction overhead before we get any benefit from independent deploys. I'd enforce module boundaries with separate schemas per module and ArchUnit tests, so each module could become a service later. Exceptions: if we have a workload with very different scaling, like video transcoding, that would run as a separate worker from day one. We'd revisit when we have five or more teams stepping on each other's releases."

In a design round

When designing a large system, it is normal to draw several services. What interviewers check is whether each service has a reason to exist and owns its data, and whether cross-service flows are handled correctly (async events, sagas, idempotency). Drawing 15 boxes with arrows between all of them, each reading the same database, is a red flag. Drawing 5 services aligned to capabilities, saying which communicate synchronously and which via events, and naming the data each owns, is a strong answer. Messaging choices are covered in Kafka vs RabbitMQ vs SQS.

Red flags

  • "Microservices are always more scalable." (A monolith scales horizontally behind a load balancer just fine.)
  • Services per database table.
  • Shared database across services, presented as normal.
  • No mention of operational or organisational cost.
  • Big-bang rewrite as the migration plan.

Frequently Asked Questions

Should a startup use microservices?▾
Usually not at the start. An early startup's main risk is building the wrong product, and a well-structured monolith lets a small team change direction quickly with simple deployments and debugging. Extract services later when specific modules need independent scaling, different technology, or separate teams that are slowing each other down.
What is a modular monolith?▾
A modular monolith is a single deployable application divided into modules with explicit boundaries: each module owns its data and exposes a defined interface, and other modules may not reach into its internals. It keeps the simplicity of one deployment and in-process calls while making a later split into services much easier.
What is a distributed monolith?▾
A distributed monolith is a system split into services that still have to be changed, tested and deployed together because they share a database, make chains of synchronous calls, or share tightly coupled libraries and data models. It has the operational cost of microservices without their independence, and is the most common failed outcome of a microservices migration.
How many microservices should a team own?▾
There is no fixed rule, but a common guideline is that each service is owned by exactly one team and a team owns a small number of services it can understand fully. If a team of five engineers owns twenty services, operational overhead is likely eating into feature work and some services should be merged.
What is the strangler fig pattern?▾
The strangler fig pattern, named by Martin Fowler, migrates a legacy system incrementally. You put a routing layer in front of the monolith, build new functionality or extract existing functionality into a new service, route the relevant traffic to it, and repeat until the old system can be retired. It avoids risky big-bang rewrites.
Are microservices faster than a monolith?▾
Not per request. A call that was an in-process function call becomes a network call with serialization, so latency usually goes up. Microservices can improve throughput and cost efficiency by scaling hot components independently, and they can improve delivery speed for large organizations, but raw performance is not their advantage.
How do microservices share data?▾
Each service should own its data store and expose data through APIs or events rather than letting other services query its tables. Other services that need the data either call the owning service, or subscribe to its events and keep a local copy of the fields they need. Cross-service consistency is handled with patterns such as sagas and the transactional outbox.

Make Architecture Calls Like a Senior Engineer

Prepflix's system design course trains you to justify architecture decisions with trade-offs and numbers, the way senior interviewers expect.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.