Microservices: When They Help and When They Hurt
Microservices split an app into independently deployable services. Costs are real: ten services at 99.9% each give 99.0% together, and retries at three layers multiply load 64 times.
Microservices split an application into small services that each own one business capability and its data, deployed independently and talking over the network. They trade a monolith's simplicity for independent teams, releases and scaling. Interviewers expect the costs: availability drops with each required service, retries multiply across layers, and cross-service transactions need sagas.
Microservices trade the simplicity of one deployable for independent teams, deployments and scaling. The costs show up as network failure modes: ten services at 99.9% availability each give about 99.0% combined, and three layers each retrying 3 times can multiply load by 64. Start with a well-structured monolith unless you have the problems microservices solve.
- A request that depends on ten services in a chain, each 99.9% available, is about 99.0% available overall, and thirty such services give about 97.0%.
- If three calling layers each make 4 attempts, 1 try plus 3 retries, one user request becomes 4 x 4 x 4 = 64 attempts against the failing service.
- A database transaction cannot span several microservices' separate databases, so multi-step operations use a saga of local transactions with compensating actions.
- Sharing one database among microservices couples them through the schema and brings back the coupling the split was meant to remove.
- A common microservices recommendation is monolith first: build a modular monolith and extract a service only when a concrete scaling or team need appears.
01Monolith and microservices
A monolith is one application deployed as one unit: all features in one codebase, one database, one release. A microservices architecture splits the application into small services, each owning one business capability (orders, payments, catalogue), with its own data, deployed independently and communicating over the network (HTTP, gRPC or messages).
Neither is better in general. A monolith is easier to build, test, debug and deploy while the team and product are small. Microservices address problems that arrive with scale: many teams stepping on each other, one release cycle for everything, and parts of the system with very different scaling needs.
02What you gain
Teams can work on and release their own service without coordinating a whole-company release. A service can scale on its own: the search service may need 20 instances while the settings service needs 2. A failure can be contained to one service if the others handle it gracefully. Different services can use different technology where it fits, though too much variety becomes a burden.
These gains are conditional on clear boundaries. Boundaries drawn around business capabilities (domain-driven design calls them bounded contexts) work better than boundaries drawn around technical layers.
03Cost 1: failures multiply
In a monolith, calling another module is a function call that essentially cannot fail on its own. In microservices, it is a network request that can time out, fail or be slow. If a request needs ten services in a chain and each is available 99.9% of the time, the whole request is available about:
print(round(0.999 ** 10, 4)) # 0.99 (about 3.7 days of downtime a year)
print(round(0.999 ** 30, 4)) # 0.9704Ten dependent services at 99.9% give 99.0% overall, and thirty give about 97.0%. Each additional required service lowers the combined figure. The remedies are to reduce hard dependencies in the request path, use timeouts, fall back to cached or default data when a service is down, and make non-essential calls asynchronous.
04Cost 2: retry storms
Suppose service A calls B, B calls C, and C calls D, and each caller retries 3 times on failure. If D is down, each call to D is attempted 1 + 3 retries = 4 times by C. C itself is called 4 times by B, and B 4 times by A. One user request becomes 4 x 4 x 4 = 64 attempts against D, which is exactly the moment it is struggling, so the retries keep it from recovering.
Defences: retry only at one layer, limit retries with a budget (for example, at most 10% extra traffic), use exponential backoff with random jitter, and add circuit breakers that stop calling a service that is failing and probe it occasionally. Make retried operations idempotent. Set deadlines that propagate down the chain so work for an abandoned request is not continued.
05Cost 3: data and consistency
A database transaction cannot span several services' separate databases. A checkout that must reserve stock, charge a card and create an order cannot be one ACID transaction. The usual approach is a saga: a sequence of local transactions, each with a compensating action that undoes it if a later step fails (refund the card if the stock reservation fails). Data is eventually consistent between services. Queries that join data from several services need an aggregator or a read model built from events (see the message queues topic).
Sharing one database among services to avoid this brings back the coupling you were trying to remove.
06Cost 4: operations
You need automated deployment pipelines per service, service discovery, centralised logging, metrics and distributed tracing (to follow one request across services), configuration management, versioned APIs with backward compatibility, and contract tests so changes do not silently break callers. Local development becomes harder: running thirty services on a laptop is rarely practical.
This operational overhead is why small teams often regret early microservices. The cost is paid immediately, and the benefits only appear when the organisation is big enough to need them.
07A practical path
A common advice is monolith first. Build a modular monolith with clear internal boundaries and separate data ownership between modules. Extract a service when a concrete need appears: a team that is blocked by shared releases, a component with very different scaling or reliability needs, or a part that needs a different runtime. Extract one piece at a time, keep the interface small, and measure.
Size is not the point. "Micro" is a misleading word. A service should be as large as one team can own and understand, with a boundary that rarely changes.
08Common interview questions
Q: Microservices versus monolith?
A: Microservices give independent deployment and scaling for large organisations, at the cost of distributed-system complexity. A monolith is simpler for small teams.
Q: How does availability change with more services in a request path?
A: Combined availability is roughly the product of each one, so it drops: ten services at 99.9% give about 99.0%.
Q: What is a retry storm and how do you prevent it?
A: Retries at several layers multiply load on a failing service. Use retry budgets, backoff with jitter, circuit breakers and retries at one layer.
Q: How do you handle a transaction across services?
A: A saga of local transactions with compensating actions, accepting eventual consistency.
Q: Should services share a database?
A: Generally no. It couples them through the schema.
Q: When should you move to microservices?
A: When team size, release coordination or scaling needs make the monolith the bottleneck.