When we build a backend, we have a fundamental choice: put everything in one application (monolith) or split it into many small, independent services (microservices). Both are valid. The right choice depends on where we are, not where we want to be.
What Is a Monolith?
A monolith is a single application where all the code lives together. User auth, payments, notifications, search — everything is in one codebase, one deployment, one process.
Think of it like a restaurant where one chef does everything: takes orders, cooks, plates, and serves. Simple when it’s a small restaurant.
Advantages:
- Easy to develop and debug — everything is in one place
- Simple deployment — one build, one deploy
- No network calls between components — just function calls
- Easy to test end-to-end
- Great for small teams (2-10 devs)
Disadvantages:
- Gets messy as the codebase grows (the “big ball of mud”)
- One bug can bring down the entire system
- Scaling means scaling everything, even the parts that don’t need it
- Slow CI/CD — small change requires full rebuild and redeploy
- Hard for multiple teams to work on without stepping on each other
What Are Microservices?
Microservices split the application into small, independent services. Each service owns one piece of functionality, has its own database, and can be deployed independently.
Think of it like a food court — each stall specializes in one thing. The pizza stall doesn’t need to know how the sushi stall works.
Advantages:
- Independent deployment — update one service without touching others
- Scale individual services based on demand
- Teams can own services end-to-end
- Tech diversity — each service can use the best tool for its job
- Fault isolation — one service crashing doesn’t kill everything
Disadvantages:
- Distributed system complexity (network failures, latency)
- Data consistency across services is genuinely hard
- Debugging a request that spans 5 services is painful
- More infrastructure to manage (service discovery, monitoring, logging)
- Operational overhead — we need DevOps maturity
The Architecture Difference
own DB
own DB
own DB
own DB
Communication Between Services
When services are split, they need to talk to each other. Two main approaches:
Synchronous (HTTP/gRPC)
Service A calls Service B and waits for a response. Like a phone call.
Order Service --HTTP POST--> Payment Service
<--200 OK---
- Simple to understand and implement
- But creates tight coupling — if Payment Service is down, Order Service is stuck
- Cascading failures are a real risk
Asynchronous (Message Queues)
Service A puts a message on a queue and moves on. Service B picks it up whenever it’s ready. Like sending a text message.
Order Service --publish--> [Message Queue] --consume--> Payment Service
- Services are decoupled — they don’t need to be online at the same time
- Better fault tolerance — messages wait in the queue
- But adds complexity (message ordering, duplicate handling, eventual consistency)
Most production systems use a mix of both. Synchronous for things that need an immediate response (login, checkout), asynchronous for things that can happen later (send email, generate report).
Service Discovery
In a monolith, calling another module is just a function call. In microservices, we need to know where the other service lives. That’s service discovery.
Two approaches:
- Client-side discovery — the caller asks a registry (like Consul or Eureka) for the service address
- Server-side discovery — a load balancer or API gateway handles routing (Kubernetes does this with its built-in DNS)
Kubernetes makes this almost free. Each service gets a DNS name like payment-service.default.svc.cluster.local, and K8s handles the rest.
The “Monolith First” Approach
Martin Fowler’s advice (and most experienced engineers agree): start with a monolith.
Why? Because:
- We don’t know our domain boundaries well enough at the start
- Splitting too early creates the wrong service boundaries
- Monoliths are faster to iterate on when we’re finding product-market fit
- We can always split later once we understand the pain points
The pattern looks like this:
- Start: Monolith with clean module boundaries
- Grow: Identify which modules cause the most scaling/deployment pain
- Extract: Pull those modules out into separate services one at a time
- Repeat: Keep extracting as needed
Amazon, Netflix, and Uber all started as monoliths before moving to microservices.
When to Go Microservices
Microservices make sense when:
- Multiple teams need to deploy independently
- Different parts of the system have wildly different scaling needs
- We need fault isolation (one feature crashing can’t kill the whole app)
- The codebase is so large that build times are painful
- We need technology diversity (ML in Python, API in Go, etc.)
Key Takeaway
In simple language, a monolith is one big app, microservices are many small apps that talk to each other. Monoliths aren’t bad — they’re the right starting point. Microservices aren’t magic — they trade code complexity for operational complexity. Pick based on team size, scale needs, and how well we understand our domain.