Microservices vs Monolith: Which Should You Build?
Learn the real tradeoffs between microservices and monolithic architecture. When microservices make sense, when they don't, and how to migrate without pain.
Every startup talks about microservices. Netflix does it. Uber does it. You should too, right?
Then you spend 6 months building service infrastructure, and your team of 4 can barely ship features.
The truth: microservices solve a specific problem. Build a monolith first.
What Is a Monolith?
A monolith is one deployable unit. All your code — user auth, payments, notifications, orders — runs in one process, connects to one database.
Monolith:
┌─────────────────────────────────┐
│ App Process │
│ ├── Auth Module │
│ ├── Orders Module │
│ ├── Payments Module │
│ └── Notifications Module │
└─────────────┬───────────────────┘
│
PostgreSQLOne codebase. One deploy. One database. Simple.
What Are Microservices?
Microservices split the application into small, independent services. Each service owns its data, has its own deployment, and communicates over HTTP or messaging.
Microservices:
Auth Service → auth DB
↕
Orders Service → orders DB ← API Gateway ← Client
↕
Payments Service → payments DB
↕
Notifications ServiceEach service is a separate process. Separate team. Separate deployment. Separate database.
Why the Monolith Is Underrated
Simple to develop
One codebase. git clone, npm install, npm start. No Kubernetes, no service mesh, no message queues to run locally. Your team ships features fast.
# In a monolith — just call the function
from payments import charge_card
from inventory import reserve_items
from notifications import send_confirmation
def checkout(order):
charge_card(order.user, order.total)
reserve_items(order.items)
send_confirmation(order.user)No HTTP calls. No serialization. No timeouts. No distributed transactions. It either works or it doesn't.
Easy to debug
One log file. One stack trace. No tracing spans across 5 services. When something breaks at 3am, you find it in minutes, not hours.
ACID transactions are free
Updating orders + inventory + payments in the same database transaction? One BEGIN / COMMIT. In microservices, this becomes a saga with compensating transactions, distributed locks, and eventual consistency.
Scales further than you think
A well-optimized monolith with a good database handles millions of users. Reddit, Stack Overflow, and GitHub ran monoliths at massive scale for years.
Why Microservices Exist
Monoliths have real problems at scale — not scale of users, but scale of teams.
Deployment bottleneck: 50 engineers deploying to one codebase. Every deploy risks breaking everything. Teams block each other.
Independent scaling: Payments needs 10x more compute than notifications. In a monolith, you scale everything. In microservices, you scale only what needs it.
Technology flexibility: The ML team wants Python. The real-time service wants Go. The monolith forces one language.
Fault isolation: A bug in the recommendation service taking down the entire app. Microservices isolate failures.
These are real problems. But they appear at large teams (50+ engineers) or very specific technical needs — not at 5 engineers.
The Real Cost of Microservices
Microservices don't remove complexity. They move it from your codebase to your infrastructure.
Local development: Running 8 services locally with Docker Compose is painful. Debugging across services is painful. Keeping service versions in sync is painful.
Network failures: In a monolith, function calls don't fail with timeouts. In microservices, every service call can fail. You need retries, circuit breakers, and timeouts everywhere.
Distributed transactions: A simple checkout flow now needs a saga, message queue, and compensating transactions to stay consistent.
Operational overhead: 8 services = 8 deployment pipelines, 8 sets of logs, 8 sets of metrics. You need Kubernetes, a service mesh, distributed tracing, centralized logging.
Small teams paying this cost for no benefit is how you end up with a "distributed monolith" — all the complexity of microservices with none of the benefits.
The Migration Path
The right path for most companies:
Start monolith. Build everything in one place. Ship fast. Learn what your system actually needs.
Modularize the monolith. Keep the codebase organized into clear modules with defined boundaries. Don't let everything depend on everything.
# Good monolith structure — modules with clear interfaces
# payments/service.py
def charge(user_id: int, amount: int) -> PaymentResult: ...
# orders/service.py
from payments.service import charge # Clear dependency
def create_order(user_id: int, items: list) -> Order:
result = charge(user_id, calculate_total(items))
...Extract services when you have a specific reason. Not "this feels like a separate service" — but "this module needs to scale 10x independently" or "this team needs to deploy independently."
Concrete reasons to extract:
✓ The ML recommendation engine needs Python + GPU
✓ The notification service has a different SLA and team
✓ The payment processing has strict compliance requirements
✓ The search service needs Elasticsearch, not Postgres
Not concrete:
✗ "It feels like a separate concern"
✗ "Netflix does it"
✗ "It would be cleaner"Who Should Use Microservices
Fit for microservices:
- Multiple teams owning different parts of the system independently
- Parts of the system with very different scaling needs
- Parts of the system requiring different technology (ML in Python, real-time in Go)
- Compliance isolation (payment card data in a separate auditable service)
Stick with monolith:
- Fewer than 20-30 engineers
- Early-stage product (still figuring out what to build)
- Single team
- When operational complexity would overwhelm the team
Amazon famously started as a monolith. So did Netflix, Uber, and Airbnb. They all moved to microservices after reaching hundreds of engineers — not before.
The Modular Monolith: Best of Both
A modular monolith has the simplicity of a monolith with the organization of microservices. It's one deployable unit internally structured into modules with clear APIs.
When you eventually need to extract a service, your module boundaries make it easy — you already have the interface defined.
Key Takeaways
- Monolith = simpler dev, deploy, debug, transactions — right for most teams
- Microservices = independent deployment, scaling, and teams — right for large orgs
- Microservices move complexity from code to infrastructure — that's a bad trade for small teams
- Start monolith, modularize it, extract services only when you have a concrete reason
- Netflix/Uber-scale problems require Netflix/Uber-scale team sizes to justify microservices
- A modular monolith is often the right long-term answer
Build a monolith. Modularize it well. Extract services only when the pain of not doing so is concrete.
Related reading: API Gateway Pattern · Service Discovery Explained · Message Queues Explained
Enjoyed this article?
Get weekly insights on backend architecture, system design, and Go programming.
Related Posts
Continue reading with these related posts
Service Discovery: How Microservices Find Each Other
Learn how service discovery works in microservices. Covers client-side vs server-side discovery, Consul, etcd, and Kubernetes DNS with practical examples.
API Gateway Pattern: The Front Door to Your Services
Learn what an API gateway does, when to use one, and how to set it up. Covers routing, authentication, rate limiting, and tools like Kong, AWS API Gateway, and Traefik.
Database Sharding Explained: Scale to Millions of Users
Learn how database sharding works, when to use it, and common strategies. Covers horizontal partitioning, shard keys, and challenges with real examples.