Microservices vs Monolith: Which Should You Choose?
Start with a well-structured monolith; move to microservices when team size and scaling needs justify the operational complexity. A practical comparison, honest pros and cons, and an e-commerce service-split example.
Start with a well-structured (modular) monolith. Move to microservices only when your team size and independent scaling needs justify the very real operational complexity. That's the answer for the overwhelming majority of projects — and the mistake I see most often is teams adopting microservices for an app that a single monolith would serve better, cheaper, and with far less pain.
Let me back that up.
The core difference
A monolith is one deployable application. All your features — auth, orders, payments, notifications — live in one codebase and ship as one unit.
Microservices split those features into independent services, each with its own codebase, deployment, and often its own database, communicating over the network (HTTP, gRPC, or a message queue).
The trade is fundamental: microservices buy you independence at the cost of operational complexity. Every benefit and drawback flows from that.
Side by side
| Dimension | Monolith | Microservices |
|---|---|---|
| Architecture | One app, one codebase | Many independent services |
| Deployment | Deploy everything together | Deploy each service independently |
| Scaling | Scale the whole app | Scale individual services |
| Development | Simple; one repo, one run command | Complex; many repos, service coordination |
| Performance | Fast in-process calls | Network latency between services |
| Reliability | One failure can affect all | Isolated failures (if designed well) |
| Operations | One thing to monitor/deploy | Distributed systems, orchestration, observability |
| Data | One database, easy transactions | Data per service, distributed consistency |
| Best team size | Small to medium | Large, multiple autonomous teams |
Monolith: pros and cons
Pros:
- Simple to build and run. One repo, one deploy, one
npm start. New developers are productive on day one. - Fast internal calls. Function calls, not network hops.
- Easy transactions. One database means real ACID transactions across your whole domain — no distributed-transaction headaches.
- Cheap to operate. One thing to deploy, monitor, and debug.
Cons:
- Scales as a whole. If only checkout is under load, you still scale the entire app.
- Tighter coupling risk. Without discipline, modules bleed into each other. (This is what a modular monolith prevents — clear internal boundaries.)
- Deploy contention. As the team grows, everyone shipping through one pipeline creates friction.
Microservices: pros and cons
Pros:
- Independent scaling. Scale the payment service without touching the catalog.
- Independent deployment. Teams ship on their own schedule.
- Fault isolation. A crash in one service doesn't have to take down the rest — if you build in the right resilience.
- Technology freedom. Each service can use the best tool for its job.
Cons:
- Operational complexity is large and real. Service discovery, distributed tracing, centralized logging, orchestration (usually Kubernetes), and network reliability all become your problem.
- Distributed data is hard. No easy cross-service transactions; you deal with eventual consistency and patterns like sagas.
- Harder local development and debugging. A bug can span five services and a message queue.
- Network latency and failure become everyday concerns.
The e-commerce example
Say you're running an online store — this is close to the kind of system I've worked on with Lightfunnels. As a modular monolith, it might have internal modules for catalog, cart, orders, payments, and notifications, all in one app with one database.
If that store grows to the point where the payment path needs to scale and deploy independently — because payment traffic spikes during sales and a separate team owns it — a sensible first split is:
┌──────────────┐
│ API Gateway │
└──────┬───────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌──────────────┐
│ Catalog │ │ Orders │ │ Payments │
│ service │ │ service │ │ service │
└─────┬─────┘ └─────┬─────┘ └──────┬───────┘
▼ ▼ ▼
catalog DB orders DB payments DB
│
▼ (async event)
┌────────────────┐
│ Notifications │
│ service │
└────────────────┘
Notice you don't split everything at once. You extract the service with the clearest independent scaling and ownership need — payments — and leave the rest in the monolith until they earn their own split. This is how you migrate: incrementally, driven by real pressure, not up front.
When to choose each
Choose a monolith when:
- You're a small team or a solo developer.
- You're building an MVP or a product still finding its shape.
- Your scaling needs are uniform across features.
- You want to ship fast and keep operations cheap.
Choose microservices when:
- You have multiple teams that need to deploy independently.
- Specific parts of the system have very different scaling profiles.
- The app is large enough that a single deploy pipeline is a bottleneck.
- You have the operational maturity (observability, orchestration, on-call) to run distributed systems.
The honest summary: microservices solve organizational and scaling problems, not code-quality problems. A messy monolith becomes a messy distributed system — and now it's messy and hard to debug. Get the structure right first.
The modular monolith: the best default
The framing of "monolith vs microservices" hides the option that's right for most teams: a modular monolith. It deploys as one unit — so you keep the simplicity, the fast internal calls, and the easy transactions — but internally it's organized into clear modules with enforced boundaries. Each module owns its domain, exposes a defined interface, and doesn't reach into another module's internals.
src/
modules/
catalog/ # owns catalog logic + data access; exposes a clean API
orders/
payments/
notifications/
shared/ # cross-cutting: auth, logging, config
The payoff is that you get the productivity of a monolith today and an easy migration path tomorrow. When one module genuinely needs to become its own service, the boundary is already there — you extract it along a seam that already exists, rather than untangling a knot. Nearly every successful microservices migration I've seen started as a well-structured monolith. The ones that started by splitting everything up front mostly regretted it.
The cost nobody budgets for
When teams estimate microservices, they price the code and forget the operations. A distributed system needs, at minimum: centralized logging (you can't SSH into one box to grep), distributed tracing (a single request now touches many services), service discovery, a strategy for inter-service auth, handling of partial failures and retries, and usually a container orchestrator to run it all. That's a standing cost paid every day by the whole team — not a one-time setup. If you don't have the maturity and headcount to carry it, microservices will slow you down, not speed you up. That's the real reason "start with a monolith" is the right default: you defer that cost until the benefit is real.
Where this fits
Serverless is a related architecture decision that often comes up in the same conversation — and it can be a great way to deploy individual services. See When Should You Use Serverless Architecture?. For the complete picture of taking an app to production and scaling it, start at the hub: Deploying, Scaling & Architecting Full-Stack Apps.