00:00

Monolithic vs Microservices Architecture in System Design

Last updated:

Choosing between a monolithic and a microservices architecture is one of the most consequential decisions in system design. A monolith runs the whole application as one deployable unit; microservices split it into many small services that are deployed and scaled independently. Neither is better in general. Each fits different team sizes, product stages and operational capabilities.

This tutorial compares the two side by side across development, deployment, scaling, data, performance, reliability, cost and testing. It includes a runnable Java example showing how the same business operation behaves in each style, a practical decision checklist, the modular monolith middle ground and a step-by-step migration approach. For the fundamentals of each style, see Monolithic Architecture and Microservices Architecture.

1. The Short Answer

  • Choose a (modular) monolith when you have a small or medium team, a new or fast-changing product, a domain you are still learning, or a need for strong consistency, and you want to ship quickly with low operating cost.
  • Choose microservices when many teams are blocked by shared releases, parts of the system need independent scaling or technology, failures must be isolated, and you already have strong automation, monitoring and on-call practices.
  • Most teams should start with a modular monolith and extract services one at a time when a specific, measurable need appears.

2. Monolith vs Microservices at a Glance

A monolith compared with microservices Left: a monolith is one deployable box containing users, orders, inventory and payments modules, all using one shared database. Right: microservices are separate services behind an API gateway, each with its own small database. MONOLITH one deployable · one process Users Orders Inventory Payments One shared database MICROSERVICES API Gateway Users Orders Inventory Payments DB DB DB DB separate deployables, separate data, talking over the network
Figure 1: A monolith keeps every module in one deployable with one database. Microservices give each capability its own deployable and its own data.

3. Detailed Comparison Table

AspectMonolithMicroservices
Deployable unitsOneMany, one per service
CodebaseOne repository and projectOne per service (or a monorepo with many projects)
Internal communicationIn-process method calls (nanoseconds)Network calls and messages (milliseconds)
DataUsually one shared databaseDatabase per service
TransactionsSimple ACID transactionsSagas, outbox, eventual consistency
DeploymentWhole application at onceEach service independently
ScalingCopy the whole applicationScale each service separately
Fault isolationA crash can take everything downFailures can be contained to one service
TechnologyOne stack for everythingCan differ per service
Team structureWorks best for one or a few teamsSuits many autonomous teams
TestingEasy end-to-end testsNeeds contract tests and test environments
DebuggingOne log, one stack traceDistributed tracing across services
Operational complexityLowHigh: orchestration, discovery, observability
Infrastructure costLowerHigher
Time to first releaseFastSlower: platform must be built first
Best forStart-ups, small teams, internal systems, consistency-heavy domainsLarge products with many teams and uneven scaling needs

4. Development and Team Organisation

In a monolith, everyone works in one codebase. A developer can run the entire application on a laptop, trace any feature end to end and refactor across modules with IDE support. This is extremely productive for small teams. The problems appear as teams grow: more merge conflicts, more accidental coupling and more coordination before every release.

In microservices, each team owns a few services. Teams can choose their own pace, tools and release schedule, and new developers only need to understand one small codebase. The price is that features spanning several services need cross-team coordination, API negotiation and careful versioning.

This is where Conway's law matters: systems tend to mirror the communication structure of the organisation that builds them. Microservices fit organisations with many independent teams; a single team building ten services usually just creates extra work for itself.

5. Deployment and Release

  • Monolith: one pipeline and one artifact. Releasing is simple, but every change, however small, ships the whole application. If one team's code has a bug, the release is blocked or rolled back for everyone. Large monoliths therefore tend to release less often.
  • Microservices: each service has its own pipeline and can be released many times a day without affecting others. In exchange, you must keep APIs backward compatible, manage versions and coordinate the rare changes that span several services.

6. Scaling

Scaling a monolith compared with scaling microservices Left: to handle more search traffic, the monolith runs three full copies, each containing search, orders, payments and admin. Right: with microservices, only the search service runs three instances while orders, payments and admin run one instance each. MONOLITH · COPY EVERYTHING search · orders · payments · admin search · orders · payments · admin search · orders · payments · admin 3 copies of code that is mostly idle MICROSERVICES · SCALE WHAT IS BUSY search · 1 search · 2 search · 3 orders · 1 payments · 1 admin · 1 extra capacity only where it is needed
Figure 2: A monolith scales by copying everything. Microservices scale only the parts under load, here the search service.

Microservices clearly win on fine-grained scaling. But the advantage is often smaller than it looks: idle code in a monolith costs little memory and no CPU, and the real bottleneck for both styles is usually the database. A monolith with caching and read replicas can handle very large traffic. Independent scaling matters most when parts of the system have genuinely different resource profiles, such as CPU-heavy video encoding next to lightweight user profiles.

7. Data and Transactions

This is the biggest practical difference. In a monolith, one database transaction can cover any business operation, so a failure anywhere rolls everything back. In microservices, each service owns its data, so a business operation spans several databases and must be coordinated with a saga: if a later step fails, earlier steps are undone by compensating actions.

The program below runs the same "place order" flow both ways. In both cases, the shipping step fails for the second order:

PlaceOrderComparison.java
package com.javabytechie.architecture;

import java.util.ArrayList;
import java.util.List;

/** The same "place order" flow done in-process (monolith) and across services (saga with compensation). */
public class PlaceOrderComparison {

    static class Store {
        int stock = 5;
        int walletBalance = 1_000;
        final List<String> orders = new ArrayList<>();

        @Override
        public String toString() {
            return "stock=" + stock + ", wallet=" + walletBalance + ", orders=" + orders;
        }
    }

    // ---------- Monolith: one process, one database transaction ----------
    static void monolithPlaceOrder(Store db, String orderId, int qty, int price, boolean failAtShipping) {
        Store snapshot = copy(db);                         // what a database rollback would restore
        try {
            db.orders.add(orderId);
            db.stock -= qty;
            db.walletBalance -= qty * price;
            if (failAtShipping) {
                throw new IllegalStateException("shipping address invalid");
            }
            System.out.println("  monolith: committed " + orderId);
        } catch (RuntimeException e) {
            restore(db, snapshot);                         // ROLLBACK: every change undone at once
            System.out.println("  monolith: rolled back " + orderId + " (" + e.getMessage() + ")");
        }
    }

    // ---------- Microservices: each step is a separate service with its own data ----------
    static void sagaPlaceOrder(Store orderDb, Store inventoryDb, Store paymentDb,
                               String orderId, int qty, int price, boolean failAtShipping) {
        orderDb.orders.add(orderId);                              System.out.println("  order-service:     order " + orderId + " created");
        inventoryDb.stock -= qty;                                 System.out.println("  inventory-service: reserved " + qty);
        paymentDb.walletBalance -= qty * price;                   System.out.println("  payment-service:   charged " + qty * price);
        if (failAtShipping) {
            System.out.println("  shipping-service:  FAILED (shipping address invalid) -> run compensations");
            paymentDb.walletBalance += qty * price;               System.out.println("  payment-service:   refunded " + qty * price);
            inventoryDb.stock += qty;                             System.out.println("  inventory-service: released " + qty);
            orderDb.orders.remove(orderId);                       System.out.println("  order-service:     order " + orderId + " cancelled");
        } else {
            System.out.println("  shipping-service:  shipment booked");
        }
    }

    static Store copy(Store s) {
        Store c = new Store();
        c.stock = s.stock;
        c.walletBalance = s.walletBalance;
        c.orders.addAll(s.orders);
        return c;
    }

    static void restore(Store target, Store from) {
        target.stock = from.stock;
        target.walletBalance = from.walletBalance;
        target.orders.clear();
        target.orders.addAll(from.orders);
    }

    public static void main(String[] args) {
        System.out.println("MONOLITH");
        Store db = new Store();
        monolithPlaceOrder(db, "A-1", 2, 100, false);
        monolithPlaceOrder(db, "A-2", 1, 100, true);
        System.out.println("  state: " + db);

        System.out.println("MICROSERVICES (saga)");
        Store orders = new Store(), inventory = new Store(), payments = new Store();
        sagaPlaceOrder(orders, inventory, payments, "B-1", 2, 100, true);
        System.out.println("  state: orders=" + orders.orders + ", stock=" + inventory.stock + ", wallet=" + payments.walletBalance);
    }
}
Output
MONOLITH
  monolith: committed A-1
  monolith: rolled back A-2 (shipping address invalid)
  state: stock=3, wallet=800, orders=[A-1]
MICROSERVICES (saga)
  order-service:     order B-1 created
  inventory-service: reserved 2
  payment-service:   charged 200
  shipping-service:  FAILED (shipping address invalid) -> run compensations
  payment-service:   refunded 200
  inventory-service: released 2
  order-service:     order B-1 cancelled
  state: orders=[], stock=5, wallet=1000

Both approaches end in a correct state, but notice the difference in effort. The monolith gets correctness for free from the database transaction. The microservices version needs explicit compensation logic for every step, and in real life each compensation is a network call that can itself fail, so it needs retries, idempotency and monitoring. During the saga, other users may briefly see reserved stock or a pending charge. This is the hidden cost of splitting data. See the Saga Pattern for production techniques.

8. Performance and Reliability

  • Latency: a method call inside a monolith takes nanoseconds to microseconds. A call between services takes milliseconds and adds serialisation, network and security overhead. A request that touches five services in sequence pays that cost five times.
  • Reliability of the whole: if a request needs five services that are each available 99.9% of the time, the chance that all five work is about 99.5%, so more moving parts can mean more failure points unless you add resilience patterns.
  • Fault isolation: on the other hand, microservices can contain failures. If the recommendations service crashes, checkout can keep working with a fallback. In a monolith, a memory leak in one feature can take down everything.

Microservices are therefore not automatically more reliable. They become more reliable only with timeouts, circuit breakers, bulkheads and graceful degradation. Learn more in Reliability.

9. Cost and Operations

A monolith can run on a few servers with a simple pipeline and basic monitoring. Microservices need an operating platform: container orchestration, service discovery, a message broker, centralised logging, metrics, distributed tracing, secret management, many pipelines and engineers who know how to run all of it. For a small team, this platform work can take more effort than building the product itself.

Cost also cuts both ways. Some teams have publicly reported large savings by consolidating over-split services back into a single application, while large organisations report that microservices let hundreds of engineers deliver in parallel. The right answer depends on scale.

10. Testing and Debugging

ActivityMonolithMicroservices
Run locallyStart one applicationStart several services and their dependencies, or use shared test environments
Integration testsStraightforward against one databasePer service, with real dependencies via containers
Cross-component testsNormal end-to-end testsConsumer-driven contract tests between services
Debugging a bugOne stack trace, one log fileCorrelation IDs, centralised logs and distributed traces
Reproducing production issuesUsually easierHarder: timing and partial failures across services

11. How to Decide: A Practical Checklist

Answer these questions honestly. The more answers fall in the right-hand column, the stronger the case for microservices:

QuestionLeans monolithLeans microservices
How many developers work on the product?Up to a few teamsMany teams, often dozens of engineers or more
How well do you understand the domain?Still discovering itStable, clear boundaries
Do parts have very different scaling needs?Fairly uniform loadSome parts are much busier or heavier
Are teams blocked by shared releases?RarelyFrequently
How much do features need strong consistency?A lot (finance, inventory)Eventual consistency is acceptable for most data
Do you have CI/CD, containers, monitoring and on-call?Basic or missingMature and automated
Is time to market the top priority?YesLong-term team independence matters more
Must failures in one area be isolated from others?Acceptable to share fateCritical paths must survive other failures

12. The Middle Ground: Modular Monolith

You do not have to choose between a tangled monolith and a fleet of services. A modular monolith is one deployable application divided into strict business modules, each with its own public API and its own database tables. Modules communicate through interfaces and in-process events.

  • You keep the monolith's simple deployment, fast calls and easy transactions.
  • You gain clear ownership, enforced boundaries and code that is easy to reason about.
  • If one module later needs independence, it is already separated, so extracting it into a service is much less risky.

For most new systems, this is the recommended starting point. Tools such as Spring Modulith and ArchUnit help verify module boundaries automatically.

13. Migrating from Monolith to Microservices: The Strangler Fig Pattern

Rewriting a whole monolith from scratch ("big bang") is risky and often fails. The strangler fig pattern, named after a vine that gradually grows around a tree, replaces the monolith piece by piece:

The strangler fig migration pattern Three stages. Stage 1: a routing proxy sends all traffic to the monolith. Stage 2: the proxy sends payment requests to a new payment service and everything else to a smaller monolith. Stage 3: the proxy routes to several services and only a small remainder of the monolith is left. STAGE 1 Routing proxy Monolith all features add a router in front STAGE 2 Routing proxy Monolith minus payments Payment new service move one capability STAGE 3 Routing proxy Payment Orders Search small remainder repeat until done
Figure 3: The strangler fig pattern: put a routing layer in front, move one capability at a time into a new service, and shrink the monolith gradually until little or nothing remains.
  1. Put a routing layer in front of the monolith, such as an API gateway or reverse proxy. At first it sends everything to the monolith.
  2. Pick one capability to extract, ideally one with a clear boundary and a real need for independence, such as payments or search.
  3. Build the new service with its own database, and migrate or synchronise its data (change data capture helps here).
  4. Route traffic gradually: send a small percentage of requests for that capability to the new service, compare results and increase the share.
  5. Remove the old code from the monolith once the new service handles all traffic.
  6. Repeat only for capabilities that benefit. It is perfectly fine to stop with a smaller monolith and a handful of services.

14. Real-World Journeys

  • Moving to microservices: several large streaming and e-commerce companies have publicly described migrating from a monolith to microservices over several years, driven by the need for many teams to deploy independently and to isolate failures at very large scale.
  • Moving back towards a monolith: engineering teams have also published cases where they merged many small services back into one application to cut infrastructure cost and latency, when the services always scaled and changed together.
  • Staying modular: some very large platforms have chosen to invest in a single, strongly modular codebase instead of splitting into many services, showing that a monolith can scale with discipline.

The lesson is that architecture should follow the needs of the product and the organisation at each stage, not fashion.

15. Common Mistakes in This Decision

  • Choosing microservices for a new product with a small team, then spending months on infrastructure instead of features.
  • Splitting by technical layer (a "database service", a "UI service") instead of by business capability.
  • Sharing one database between services, creating a distributed monolith.
  • Rewriting everything at once instead of migrating incrementally.
  • Ignoring data consistency until after the split, then discovering that critical workflows can no longer be atomic.
  • Keeping a tangled monolith because "microservices are too hard", when a modular monolith would solve most of the pain.

16. Summary

  • A monolith is simpler to build, test, deploy and keep consistent; microservices offer independent deployment, scaling and fault isolation at the cost of distributed-system complexity.
  • The biggest practical difference is data: one ACID transaction versus sagas and eventual consistency.
  • Microservices suit many teams, clear domains, uneven scaling needs and mature automation; monoliths suit small teams and evolving products.
  • A modular monolith is the recommended default, giving clean boundaries without network costs.
  • Migrate with the strangler fig pattern, one capability at a time, only where there is a clear benefit.

Start simple, measure where the pain really is, and let those needs drive the architecture. Open the FAQ tab for answers to common questions about monolithic versus microservices architecture.