Microservices vs Monolith
API gateways, rate limiting, circuit breakers, and when microservices are (and aren't) worth the cost.
The monolith
A monolith is a single deployable unit containing all of an application's functionality — one codebase, one build, one deployment.
┌─────────────────────────────────┐
│ Monolith │
│ Orders | Users | Payments | │
│ Inventory | Notifications │
└─────────────────────────────────┘
|
v
[ One database ]
Advantages: simple to develop, test, and deploy early on; no network calls between modules (function calls are fast and reliable); one codebase is easy to reason about end-to-end; transactions across "modules" are trivial (they're just local database transactions).
Disadvantages: the entire application scales as one unit even if only one module is under heavy load; a bug in one module can take down the whole app; large teams working in the same codebase step on each other; the whole thing must be redeployed for any change.
Microservices
Microservices split an application into independently deployable services, each owning its own data and communicating over the network (usually HTTP/REST or gRPC, often combined with events).
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐
│ Orders │ │ Users │ │ Payments │ │ Inventory │
│ Service │ │ Service │ │ Service │ │ Service │
└─────┬────┘ └────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │ │
[DB_orders] [DB_users] [DB_payments] [DB_inventory]
Advantages: each service scales independently (only scale the Orders service if only Orders is under load); teams own their service end-to-end and deploy independently; a failure in one service doesn't necessarily crash others; different services can use different tech stacks where it makes sense.
Disadvantages: distributed systems complexity — network calls can fail, be slow, or arrive out of order, where a function call could not; cross-service transactions are hard (see below); requires real operational maturity — service discovery, distributed tracing, centralized logging, CI/CD per service; total infrastructure cost and operational overhead go up significantly.
The honest trade-off
Microservices don't make a system "better" by default — they trade development-time simplicity for operational flexibility and independent scalability. A small team building an MVP is very often better off with a well-structured monolith; a large organization with many independent teams and genuinely different scaling needs per module benefits more from microservices. Many successful companies (Shopify, GitHub for a long time, Basecamp) deliberately stayed monolithic well past the "unicorn" stage.
API Gateway
When a system is split into many services, clients shouldn't need to know about (or call) each one directly. An API Gateway is a single entry point that routes external requests to the right internal service, and often also handles cross-cutting concerns:
Client --> API Gateway --> routes to --> Orders / Users / Payments / ...
|
+-- authentication
+-- rate limiting
+-- request logging
+-- response aggregation
Rate limiting
Rate limiting protects a service from being overwhelmed — by a buggy client, a traffic spike, or an abusive actor — by capping how many requests a client can make in a given window.
- Token bucket — a bucket refills with tokens at a fixed rate; each request consumes one token; requests are rejected once the bucket is empty. Allows short bursts up to the bucket size.
- Fixed window — count requests in fixed time windows (e.g., per minute); simple, but allows a burst of
2xthe limit right at a window boundary. - Sliding window — smooths out the fixed-window boundary problem by considering a rolling time range instead of discrete buckets.
Circuit breaker
When Service A calls Service B, and B is slow or failing, A retrying repeatedly can make things worse — piling up requests against an already-struggling service, and tying up A's own resources waiting. A circuit breaker wraps that call and, after enough consecutive failures, "trips open" — failing fast (without even attempting the call) for a cooldown period, then cautiously testing ("half-open") whether B has recovered before fully resuming.
CLOSED (normal) --[too many failures]--> OPEN (fail fast, no calls attempted)
^ |
| [cooldown timer expires]
| v
+----------[test call succeeds]------- HALF-OPEN (try one request)
This prevents cascading failures — one struggling service degrading gracefully instead of dragging down every service that depends on it.
Distributed transactions: the Saga pattern
A single database transaction can't span multiple services' independent databases. The Saga pattern breaks a multi-step business transaction into a sequence of local transactions, each in one service, with a defined compensating action to undo previous steps if a later step fails:
1. Orders: create order (pending) -> success
2. Payments: charge customer -> success
3. Inventory: reserve stock -> FAILS (out of stock)
Compensate:
3. (nothing to undo — it failed)
2. Payments: refund the charge
1. Orders: mark order as cancelled
Common mistakes
- Adopting microservices for a small team/product before there's an actual scaling or organizational reason to — the operational tax is paid immediately, the benefits arrive later (if ever).
- Treating a distributed transaction like a local one — forgetting that a multi-service "transaction" needs an explicit compensation strategy (Saga) since there's no single database
ROLLBACKacross services. - Adding retries without a circuit breaker, which can amplify an outage instead of containing it (a "retry storm").
Interview questions
Q: When would you recommend a monolith over microservices? For small teams, early-stage products, or when the domain isn't well understood yet — a monolith is faster to build, easier to reason about, and cheaper to operate, and can always be split into services later once real scaling or organizational boundaries emerge.
Q: What problem does a circuit breaker solve that a simple retry doesn't? A plain retry can pile more load onto an already-struggling downstream service, worsening the outage ("retry storm"). A circuit breaker detects sustained failure and stops sending requests entirely for a cooldown period, giving the downstream service room to recover, then cautiously tests before fully resuming traffic.
Q: How do you handle a transaction that spans multiple microservices? There's no single ACID transaction across independent databases, so you use the Saga pattern: a sequence of local transactions, each with a defined compensating action, so a failure partway through can be "undone" by running compensations for the steps that already succeeded.