- microservices
- architecture
- system-design
- backend
Microservices Are Not the Default: Split Only When the Use Case Demands It
Every service you add buys you one thing and costs you several. Here is how to tell when the trade is worth it, using what Amazon, Uber, Shopify, Segment and others have said in public.
Muhammad Ahsan Farooq
· 14 min read

Every service you add buys you one thing and costs you several. Here is how to tell when the trade is worth it, using what Amazon, Uber, Shopify, Segment and others have said in public.
A function call inside one process is cheap and almost never fails. A call to another service crosses a network. It can be slow, drop, time out, or arrive twice.
Microservices turn many function calls into network calls. That is the whole trade. You get independent deploys and independent scaling. You pay with latency, new failure modes, and a lot more to operate.
My view is simple: pay that price only when you have a problem that services actually solve. Below are the costs, what well-known companies did, when services are the right call, and a middle path you can start on Monday. Every claim links to its source right where it appears.
The cost nobody puts on the slide
First, a plain definition. A microservice is a small application that runs on its own and is called over the network. Uber's engineers describe it as a library with a performance hit: you pay for network I/O and for turning data into bytes and back again.
1. Network calls fail
Across a network you get timeouts, retries, and the question "did it run or not?"
Here is simple math, which is my own illustration and not from any source. Say each service is up 99.9% of the time and a request touches five of them in a row. If failures are independent, the chain is up about 99.5% of the time (0.999 to the power of 5). You lost reliability just by splitting the code.
Speed suffers too. A team of Google researchers wrote in a 2023 paper that serializing data and sending it over the network is "increasingly becoming a bottleneck", and that these overheads add up when developers split applications into too many pieces.
2. Transactions and data consistency
In one PostgreSQL database, a single transaction can update orders and payments together. Both succeed or both roll back. Split them into services with separate databases and that guarantee is gone. You need patterns like sagas: a chain of local steps, with "undo" steps if one fails. It works, but it is more code and more ways to be wrong.
You are not alone in this. Martin Fowler lists eventual consistency among the factors you must deal with once you build a distributed system. Sam Newman's book Monolith to Microservices has a section titled "Distributed Transactions—Just Say No", followed by a section on sagas.
Even simple reads get harder. Newman's example is a "top 10 albums sold this week" report. In a monolith it is one SQL join. After a split it becomes calls to two services, and the join moves into application code. In his QCon talk he says this can become "horrific" for things like latency. And the database can no longer enforce foreign keys across the two data stores for you.
3. Seeing what happened (observability)
When one request crosses many services, one log file is no longer enough. You need tracing and shared request IDs.
Uber reports that once it reached around 2,200 critical microservices, engineers had to work through around 50 services across 12 teams to find the root cause of a single problem. Fowler makes the same point from the other side: past a handful of services, you must be able to trace business transactions through multiple services.
4. Deploy and ops overhead
Each service needs a build, a pipeline, config, alerts, and an on-call owner. Fowler's baseline list is fast server provisioning, basic monitoring, fast deployment, and close collaboration between developers and operations. His advice is to start with a handful of services and learn from them before growing the number.
5. The "distributed monolith"
Newman names the worst case. Your code runs as many services, but you must still release them together in lockstep. You pay for distribution and keep the coordination of a monolith. His warning sign: if a company has a full-time release-coordination manager, it probably has a distributed monolith.
Fowler calls the total bill the "microservice premium," and says it often gets projects "into serious trouble".
What big companies actually did
None of these stories say "microservices are bad." Each one says "match the tool to the problem."
Amazon Prime Video: one service moved back into one process (2023)
A Prime Video team built a tool to check the quality of live streams. It used AWS Step Functions and Lambda, with each detector as a separate component.
Two things hurt. Step Functions charges per state transition, and the service made several of them for every second of a stream, so it quickly hit its account limits. Separately, video frames moved between components through an S3 bucket, and the large number of S3 calls was expensive. The service hit a hard scaling limit at only 5% of the expected load.
The team moved the components into a single process and passed frames in memory. The result is in the title of their own post: reducing costs by 90%. (The original URL now redirects to a generic Amazon page, so this link goes to the archived copy.)
Two details keep this honest:
- It was one service, not all of Prime Video.
- They still scale it out. When one instance ran out of room, they cloned the service and gave each copy a subset of the detectors, per the archived post.
The author's own conclusion was that microservices and serverless are tools that work at high scale, but the choice has to be made "on a case-by-case basis". Former Netflix architect Adrian Cockcroft added that the team had followed a "Serverless First" path and then found its state transitions too frequent and its calls to S3 too chatty. Read that way, the lesson is less "monoliths beat microservices" and more "a first design needs to be measured and reshaped."
Segment: from 140+ services to one (2018)
Segment's first split made sense. One shared queue had head-of-line blocking: when one partner API slowed down, retries flooded the queue and delayed every other destination. A service and a queue per destination fixed that.
Then it grew past 140 services. Shared libraries drifted into different versions. Each service had its own load pattern, and tuning autoscaling was "more art than science". Three full-time engineers were spending most of their time just keeping the system alive.
So they merged everything into one service and one repo. A change to a shared library no longer meant deploying 140+ services. In the following year they made 46 improvements to shared libraries, versus 32 the year before.
Segment was also open about what the monolith costs: fault isolation gets harder (one bad destination can crash the service for all), in-memory caching is less effective, and a dependency upgrade can break several destinations at once. Every choice has a bill.
Istio: merged its own control plane (2020)
Istio is a service mesh. It first shipped its control plane as separate microservices. In Istio 1.5 the team merged them into one binary called istiod.
Their reasoning is a ready-made checklist. They went through the usual reasons to split (languages, teams, versions, scaling, security) and found that none applied. For example, the components were always released together at the same version, and most installs were run by a single team. Their summary: microservices work best when they are "reflected in your organizational reality".
Google researchers: split the code, not the deploy
A 2023 paper from Google researchers argues that microservices often "backfire" because they mix up two things: how code is organized (logical boundaries) and how it is deployed (physical boundaries). They propose writing a "logical monolith" and letting a runtime decide what runs where. On an eleven-component benchmark app, their prototype reduced latency by up to 15× and cost by up to 9× in the most aggressive setup, where all components ran together.
That is research on a small test, not proof for your system. The useful idea is that how you organize code and how you deploy it are two separate choices.
Shopify: a very large monolith, made modular
When Shopify's engineers wrote about it in 2020, their core Rails monolith had over 2.8 million lines of Ruby and 500,000 commits. Their answer was to make it modular, not to shatter it.
It was hard. Their dependency graph was so dense that every component depended on over half of the others. So they built Packwerk, a tool that runs in the pull request workflow and rejects changes that break component boundaries.
They split out services only for specific reasons. Storefront rendering is read-only with very high throughput, so it scales on its own. Credit card vaulting handles sensitive data that should not flow through other parts of the system. Their own verdict: splitting a monolith into services increases the overall complexity considerably.
Stack Overflow: a few big pieces, heavy traffic
In 2016, Nick Craver reported that Stack Overflow served about 209 million HTTP requests and 505 million SQL queries in one day. The main Q&A application is multi-tenant: one application serves all the Q&A sites, and the team could run the whole network off a single application pool on a single server. Craver adds a caveat that he is saying it works, "not saying it's a good idea".
To be fair, the same post also lists separate apps, such as Careers and the APIs. And this is a 2016 snapshot, not today's setup.
Uber: microservices at huge scale, and still needed restructuring
Uber is the counter-case. Around 2012 to 2013 it had primarily two monolithic services, and hit real problems. A single regression could bring down all of Uber. Deployments were risky and slow. And as engineering grew from tens to hundreds of people, teams could not move independently. Uber says it would not have reached its scale and quality of execution without microservices.
Then growth brought a new problem. At about 2,200 critical microservices, even simple features crossed many teams. Uber did not go back. It classified the 2,200 services into about 70 domains with a single gateway into each, a design it calls DOMA. The same post is also frank that in small organizations the operational benefit likely does not offset the added architectural complexity.
When microservices ARE the right call
Use services when you can name at least one of these, ideally with evidence:
- Many teams block each other. This is Uber's case. Even DHH, who argues for monoliths, says that for companies like Amazon or Google, with thousands of developers, services are "a wonderful way to parallelize opportunities for improvement". Fowler also lists large teams as one source of the complexity that can justify the premium.
- You need independent deploys. Newman calls the ability to deploy a change to one service without changing anything else "the golden rule of microservices". It matters most when waiting on other teams is your real bottleneck.
- One part has a very different load. Shopify's storefront rendering is the example. Istio's test is a fair one: the value of independent scaling must be larger than the cost of running it separately.
- You need fault isolation. Segment's separate queues stopped one slow partner from delaying the rest.
- You need a security boundary. Shopify's card vaulting is the example. It only counts if the permissions really differ, and Istio found they did not in its control plane.
- A part needs a different technology. Istio's data plane is written in C++ while its control plane is in Go, a clean natural split.
Then check the prerequisites. Fowler's list is fast provisioning, monitoring, fast deployment, and developers and operations working together. If you lack them, build those first. Newman's book also has a section on when microservices may be a bad idea, and its list includes an unclear domain, startups, and "not having a good reason."
Conway's law: teams drive the design
In 1968 Melvin Conway wrote that organizations that design systems are constrained to produce designs that are "copies of the communication structures" of those organizations. Fowler points out that some companies have used this on purpose: they deliberately split themselves into small, loosely coupled groups to push the software toward the same shape. He calls this the "Inverse Conway Maneuver."
So team shape matters more than fashion. Many independent teams may justify services. One small team that owns everything rarely does.
The middle ground: a modular monolith
A modular monolith is one deployable app with clear walls inside it. Newman calls it a "highly underrated option". He says many organizations he works with would be better off with a modular monolith than with microservices, and that he has told about half of his clients over the last three years that microservices are not for them.
Fowler's "Monolith First" view is related. He notes that almost all the successful microservice stories he knows began with a monolith that grew too big and was broken up. He also says the evidence is thin and that his advice is tentative. I take the habit from that: start simple, split with evidence. His other reason is practical: moving functionality between services is much harder than moving it inside a monolith, so you want your boundaries right before you pay that price.
The catch is discipline. Without it, a modular monolith can decay into a "ball of mud," as Newman warns in the same talk. And Fowler notes that the gradual route to services worked only where the modular design was relatively good to start with. So you need walls that tools enforce, not walls in a wiki.
Make the walls real
In NestJS, a module keeps its providers private unless it exports them. The docs say a module's exported providers "form its public interface, or API". Export a small interface and keep everything else hidden:
// billing/billing.api.ts: the only file other modules may import
export interface BillingApi {
charge(input: {
orderId: string;
amountCents: number;
idempotencyKey: string; // safe to retry later, when this becomes a network call
}): Promise<{ paymentId: string }>;
}
export const BILLING_API = Symbol('BILLING_API');
// billing/billing.module.ts
@Module({
providers: [
BillingService,
{ provide: BILLING_API, useExisting: BillingService },
],
exports: [BILLING_API], // BillingService itself stays private
})
export class BillingModule {}
// orders/orders.service.ts: depends on the interface, never on billing internals
@Injectable()
export class OrdersService {
constructor(@Inject(BILLING_API) private readonly billing: BillingApi) {}
checkout(orderId: string, amountCents: number) {
return this.billing.charge({
orderId,
amountCents,
idempotencyKey: `order-${orderId}`,
});
}
}
Then add a CI check that fails the build on imports across module walls. Shopify did exactly this with Packwerk in Ruby, and in TypeScript, tools such as dependency-cruiser or eslint-plugin-boundaries can play the same role.
Do the same for data. In PostgreSQL, give each module its own schema and its own database role. Schemas group tables, but the PostgreSQL docs are clear that schemas are not rigidly separated: a user can reach objects in any schema if they have the privileges. So the role setup is what makes the wall real.
CREATE SCHEMA billing;
CREATE ROLE billing_app LOGIN;
GRANT USAGE, CREATE ON SCHEMA billing TO billing_app;
ALTER ROLE billing_app SET search_path = billing;
-- The orders role gets no grants on the billing schema at all.
Then ban joins across schemas, so each module owns its data. This pays off later, because the data is the hardest part of any split. If billing's tables are already walled off, extracting billing means moving a schema, not untangling a shared database.
Extract only when a boundary proves itself

A boundary is proven when changes inside it rarely touch other modules, one team owns it, and it has a real reason to run apart (load, security, or release pace). Shopify is even building automation to count how many components each pull request touches, and wants that number to fall over time.
When you do extract, go slowly. Newman recommends incremental patterns such as the strangler fig and branch by abstraction, both covered in his talk: put the new service behind the same interface, keep the old code in place, and flip a toggle back if production misbehaves. With the interface above, the swap is one provider line (useClass: HttpBillingClient). The idempotencyKey is already there because the call can now be retried.
A practical decision checklist
Answer these in order. Stop at the first "no."
- What problem are we solving? Name it. Newman's own chapter on planning a migration opens with the line "Microservices are not the goal". If you can't name the problem, don't split.
- Have we tried a cheaper fix? Module walls, a faster pipeline, a read replica, a background job queue.
- Is the domain stable? If boundaries still move, refactoring across services is much harder than inside a monolith.
- Is a team behind the boundary? No owner, no service.
- Does it need its own scale, security, or release pace? Prove it with load data, not a guess. Segment and Istio both did the math and merged.
- Can we operate it? Tracing, alerts, automated deploys, and an on-call plan, as Fowler's prerequisites say.
- Can the data be split cleanly? If two modules must join tables constantly, they are probably one module.
- Can we undo it? Keep the old path until the new one has run in production.
If you passed all eight, extract one service and measure. If not, keep the modules and keep shipping.
Conclusion
Microservices solve organizational and operational problems: many teams, independent deploys, uneven load, isolation. They also create network, data, and ops problems. Uber says it could not have scaled without them. Segment, Istio, and one Prime Video service got simpler by merging. Shopify kept a big modular monolith and extracted only a few pieces.
The common thread is that each matched the architecture to a specific problem. Do the same. Start with clear modules and a clean data boundary. Extract when a boundary proves itself. Treat every new service as a cost you must justify.