PCSalt
YouTube GitHub
Back to Architecture
Architecture · 5 min read

Microservices vs Monolith — A Practical Decision Framework

A no-hype comparison of microservices and monoliths — when each architecture makes sense, the real tradeoffs, and how to decide for your project.


“Should we use microservices?” is the wrong question. The right question is: “What problems do we have, and does splitting services solve them?”

Most teams that adopt microservices early regret it. They trade one set of problems (large codebase, slow deploys) for a harder set (distributed transactions, network failures, operational complexity). Some teams genuinely need microservices — but usually later than they think.

This post gives you a practical framework for making the decision.

What is a monolith?

A monolith is a single deployable unit. One codebase, one build, one deployment artifact.

┌─────────────────────────────────┐
│           Monolith              │
│  ┌─────────┐ ┌──────────────┐  │
│  │ Orders  │ │   Payments   │  │
│  └─────────┘ └──────────────┘  │
│  ┌─────────┐ ┌──────────────┐  │
│  │ Users   │ │ Notifications│  │
│  └─────────┘ └──────────────┘  │
│          Shared Database        │
└─────────────────────────────────┘

“Monolith” doesn’t mean “messy.” A well-structured monolith has clear module boundaries, separated concerns, and clean interfaces between modules. It’s one deployment unit, not one file.

What are microservices?

Microservices are independently deployable services, each owning its own data and communicating over the network.

┌──────────┐   ┌──────────────┐
│  Orders  │   │   Payments   │
│  Service │   │   Service    │
│   [DB]   │   │    [DB]      │
└────┬─────┘   └──────┬───────┘
     │                 │
     └────── API ──────┘
     │                 │
┌────┴─────┐   ┌──────┴───────┐
│  Users   │   │ Notifications│
│  Service │   │   Service    │
│   [DB]   │   │    [DB]      │
└──────────┘   └──────────────┘

Each service is small, focused, and independently deployable. But they communicate over the network, which introduces latency, failure modes, and complexity.

The honest tradeoffs

What monoliths give you

Simple development: One codebase, one IDE project. Find references, refactor, debug — all tools work. No network calls between modules.

Simple deployment: Build one artifact. Deploy it. Done. No orchestration, no service mesh, no version compatibility matrix.

Simple transactions: Need to update orders and inventory in one transaction? Just use a database transaction. No Saga pattern, no eventual consistency.

Simple debugging: A request enters, you step through the code, it exits. Stack traces are meaningful. No distributed tracing needed.

Simple testing: Integration tests start the app and hit real endpoints. No need to mock 5 other services.

What monoliths cost you

Scaling is all-or-nothing: If the payment module needs 10x resources but the user module doesn’t, you scale everything. Wasteful.

Deploy risk: Every deploy includes every module. A bug in notifications can block deploying a fix for payments.

Team coupling: As the team grows, everyone works in the same codebase. Merge conflicts, broken builds, stepping on each other’s work.

Technology lock-in: One language, one framework, one version for everything. Can’t use Python for ML if the monolith is Java.

What microservices give you

Independent scaling: Scale the hot service without scaling everything else.

Independent deployment: Deploy payment fixes without touching the order service. Ship faster, with less risk.

Team autonomy: Each team owns a service. They choose their tech stack, deploy schedule, and internal design.

Failure isolation: If the notification service crashes, orders still work (if designed correctly).

What microservices cost you

Network complexity: Every inter-service call can fail, time out, or return stale data. You need retries, circuit breakers, and timeouts everywhere.

Data consistency: No cross-service transactions. You need Sagas, eventual consistency, and compensation logic.

Operational overhead: Each service needs its own CI/CD pipeline, monitoring, logging, alerting, deployment infrastructure. Multiply by 10 services.

Debugging difficulty: A request touches 5 services. You need distributed tracing (Jaeger, Zipkin) and centralized logging (ELK, Grafana Loki) to follow what happened.

Testing complexity: Integration tests need multiple services running. Service mocks drift from reality. End-to-end tests are slow and flaky.

The decision framework

Team size

TeamRecommendation
1–5 developersMonolith. No question.
5–15 developersModular monolith. Split if pain appears.
15+ developersConsider microservices for independent team boundaries.

Microservices are an organizational pattern as much as a technical one. They make sense when you have multiple teams that need to move independently. A team of 3 doesn’t have this problem.

Rate of change

If different parts of your system change at very different rates — payments changes monthly, but the recommendation engine changes daily — microservices let the fast-changing part deploy independently.

If everything changes at roughly the same rate, a monolith deploys just as fast.

Scaling needs

If one component needs 100x the resources of another, separate it. Run 50 instances of the image processing service while keeping one instance of the admin panel.

If load is roughly uniform, scale the monolith horizontally behind a load balancer.

Domain boundaries

Microservices work best when you have clear, stable domain boundaries with minimal cross-cutting concerns. If every request touches 5 services, you’ve created a distributed monolith — all the pain of both approaches.

Ask: “Can this service do its job without synchronously calling other services?” If the answer is often “no,” the boundary is wrong.

The modular monolith — the middle ground

Most teams should start here. It’s a monolith internally but with strict module boundaries:

┌──────────────────────────────────────┐
│              Monolith                │
│  ┌──────────┐      ┌─────────────┐  │
│  │  Orders  │ API  │  Payments   │  │
│  │  Module  │ ←──→ │  Module     │  │
│  │  [tables]│      │  [tables]   │  │
│  └──────────┘      └─────────────┘  │
│  ┌──────────┐      ┌─────────────┐  │
│  │  Users   │ API  │ Notifications│  │
│  │  Module  │ ←──→ │  Module     │  │
│  │  [tables]│      │  [tables]   │  │
│  └──────────┘      └─────────────┘  │
└──────────────────────────────────────┘

Rules:

  • Each module has its own package/directory and its own database tables
  • Modules communicate through defined interfaces (not direct table access)
  • No module reaches into another module’s database tables
  • Shared code is minimal and explicitly defined

This gives you clean boundaries without network overhead. And when the time comes to extract a service, the boundary is already clear — you just move the module to its own deployment and replace in-process calls with network calls.

Signs you need microservices

  • Deploys take hours and require coordination across teams
  • One team’s broken code blocks another team’s release
  • A component needs radically different scaling than the rest
  • You have 3+ teams working on the same codebase with constant conflicts
  • Parts of the system need different technology stacks
  • Compliance requires isolating certain data (PCI, HIPAA)

Signs you don’t

  • Your team is small (< 10 developers)
  • The system is young and requirements are still changing
  • You’re spending more time on infrastructure than features
  • Most requests touch multiple “services”
  • You don’t have a mature CI/CD and monitoring setup
  • “Because Netflix does it” is the primary motivation

Migration path

If you start with a modular monolith and later need microservices:

  1. Enforce module boundaries — no cross-module database access
  2. Introduce async events — modules communicate via in-process events
  3. Extract the highest-value service first — the one with the clearest boundary and biggest scaling need
  4. Replace in-process events with message broker — Kafka, RabbitMQ
  5. Repeat — extract services one at a time, not all at once

This is cheaper and safer than starting with microservices and later discovering you drew the boundaries wrong (which you will, because requirements always change).

Summary

FactorMonolith winsMicroservices win
Team sizeSmall (< 10)Large (15+)
ComplexityLowerHigher operational cost
ScalingUniform loadUneven load
Data consistencyStrong (transactions)Eventual (Sagas)
Deployment speedFast (one artifact)Fast (per service)
Development speedFast (no network)Fast (team independence)
DebuggingSimpleRequires tooling

Start with a modular monolith. Extract services when the organizational or scaling pain justifies the distributed systems complexity. The best architecture is the simplest one that solves your actual problems.