system design

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.

By Akash Sharma·6 min read
#microservices
#monolith
#architecture
#system design
#backend
#scalability
#distributed systems

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.

plaintext
Monolith:
┌─────────────────────────────────┐
│  App Process                    │
│  ├── Auth Module                │
│  ├── Orders Module              │
│  ├── Payments Module            │
│  └── Notifications Module       │
└─────────────┬───────────────────┘

         PostgreSQL

One 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.

plaintext
Microservices:
Auth Service → auth DB

Orders Service → orders DB  ← API Gateway ← Client

Payments Service → payments DB

Notifications Service

Each 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.

python
# 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.

python
# 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."

plaintext
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.