The Cost of Microservices: When Are They Unnecessary and When Are They Inevitable?
Microservices are unnecessary for a single-team system without the prerequisites (automated provisioning, monitoring, fast deployment); they become inevitable once teams waiting on each other and parts that scale differently become measurable. The real cost of a network call, measured, plus the fallacies of distributed computing, Fowler's prerequisites, a correct reading of the Prime Video case and a decision checklist for Bookstore.
Version note: The code in this post was built and run with C# 14 and .NET 10.
Guess First
During checkout, Bookstore reads a book's price today with an ordinary method call: prices.Find(isbn). If Catalog becomes a separate service, the same information will arrive through an HTTP (Hypertext Transfer Protocol) request. Even if both run on the same machine, in the same process (so no real network cable is ever involved), how many times slower is the HTTP call than the method call? 2 times, 20 times, 2,000 times?
The second question matters more: in what ways can the method call fail, and in what new ways can the HTTP call fail? What happens to checkout during the 15 seconds you are deploying Catalog? Make your guess; then let's measure.
A Network Call Is Not a Method Call
The program below uses the same PriceList in two ways: directly, and behind a small Minimal API (Application Programming Interface) started inside the same process, over 127.0.0.1. No physical network, no TLS (Transport Layer Security), no load balancer; the most optimistic version of the cost of HTTP and JSON (JavaScript Object Notation):
namespace Bookstore.Catalog;
public sealed record BookPrice(string Isbn, decimal Amount, string Currency);
public sealed class PriceList
{
private readonly Dictionary<string, decimal> _prices = new() { ["978-0441013593"] = 12.50m };
public BookPrice? Find(string isbn) =>
_prices.TryGetValue(isbn, out decimal amount) ? new BookPrice(isbn, amount, "EUR") : null;
}
using System.Diagnostics;
using System.Globalization;
using System.Net.Http.Json;
using Bookstore.Catalog;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
PriceList prices = new();
// We also expose the same PriceList behind HTTP, like a separate "Catalog service"
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.WebHost.UseUrls("http://127.0.0.1:5087");
WebApplication catalog = builder.Build();
catalog.MapGet("/books/{isbn}/price", (string isbn) =>
prices.Find(isbn) is { } price ? Results.Ok(price) : Results.NotFound());
catalog.MapGet("/slow/books/{isbn}/price", async (string isbn) =>
{
await Task.Delay(TimeSpan.FromSeconds(3)); // GC pause, exhausted connection pool, slow disk...
return Results.Ok(prices.Find(isbn));
});
await catalog.StartAsync();
using HttpClient http = new() { BaseAddress = new Uri("http://127.0.0.1:5087"), Timeout = TimeSpan.FromSeconds(1) };
const int calls = 10_000;
for (int i = 0; i < 1_000; i++) await http.GetFromJsonAsync<BookPrice>("/books/978-0441013593/price"); // warm-up
// 1) In-process method call (one round first so the JIT warms up)
for (int i = 0; i < 1_000_000; i++) prices.Find("978-0441013593");
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 1_000_000; i++) prices.Find("978-0441013593");
double inProcess = sw.Elapsed.TotalMicroseconds / 1_000_000;
// 2) HTTP + JSON over loopback on the same machine
sw.Restart();
for (int i = 0; i < calls; i++) await http.GetFromJsonAsync<BookPrice>("/books/978-0441013593/price");
double overHttp = sw.Elapsed.TotalMicroseconds / calls;
Console.WriteLine($"In-process call : {inProcess,10:F3} µs");
Console.WriteLine($"HTTP (loopback) : {overHttp,10:F3} µs (~{overHttp / inProcess:N0}x)");
// 3) A new failure mode of a network call: no answer
try
{
await http.GetFromJsonAsync<BookPrice>("/slow/books/978-0441013593/price");
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
Console.WriteLine($"Slow Catalog : {ex.Message}");
}
// 4) Catalog is being deployed: no process
await catalog.StopAsync();
try
{
await http.GetFromJsonAsync<BookPrice>("/books/978-0441013593/price");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Catalog down : {ex.HttpRequestError} - {ex.Message}");
}
On a Mac with an M5 (Apple Silicon) chip, with a Release build:
In-process call : 0.028 µs
HTTP (loopback) : 51.610 µs (~1,860x)
Slow Catalog : The request was canceled due to the configured HttpClient.Timeout of 1 seconds elapsing.
Catalog down : ConnectionError - Connection refused (127.0.0.1:5087)
The number itself depends on the machine; the order of magnitude doesn't. Reading a price from a dictionary takes 28 nanoseconds, the most optimistic HTTP call around 50 microseconds: roughly two thousand times more. On a real network, with TLS, a service mesh and another machine, this grows to milliseconds. If a page lists 40 books and each price comes through a separate call, the difference is now something the user feels.
The last two lines are what really matter. A method call has no "no answer came back" failure mode; if the process is up, the answer comes. A network call has two new ones: the other side is slow (and you have to decide how long to wait), or the other side isn't there at all. The second isn't even a fault: every time Catalog is deployed, nobody will be at that address for a few seconds. Unless you write these lines, checkout will show every Catalog deployment to customers as an error. Setting up timeouts, retries and circuit breakers properly is the subject of the planned Backend series "External Service Integration and Resilience" posts; what we need to notice here is that this code must be written on the day you move to microservices. For brevity the sample creates HttpClient directly; a real service uses IHttpClientFactory.
The Fallacies of Distributed Computing, Day to Day
In the 1990s, the wrong assumptions that engineers building distributed systems at Sun Microsystems kept seeing in newcomers became a list that L. Peter Deutsch completed to seven items in 1994 and James Gosling extended with an eighth: the fallacies of distributed computing. The list is 30 years old, but every item has a counterpart in Bookstore today:
| Wrong assumption | What it means day to day in Bookstore |
|---|---|
| The network is reliable | A request to Inventory got lost: was the stock reserved or not? If you retry, is it reserved twice? |
| Latency is zero | The cart page makes 40 separate price calls for 40 books; a cost invisible in a method call turns into page time |
| Bandwidth is infinite | Carrying Catalog's full book list to Ordering on every request fills the pipe on campaign day |
| The network is secure | Traffic between services must be encrypted and authenticated too; the "inside is safe" assumption is the subject of this series' tenth post |
| Topology doesn't change | Inventory's address changes from deploy to deploy; hard-coded URLs (Uniform Resource Locators) create the need for service discovery |
| There is one administrator | The payment provider and the carrier are not under your control; they pick maintenance windows without asking you |
| Transport cost is zero | Every call means serialization, CPU (Central Processing Unit) and, in the cloud, a data transfer bill |
| The network is homogeneous | One service is .NET, another uses a different language; date formats, decimal precision and timeout behavior differ |
In a monolith most of these eight items are invisible, because method calls stay in the same process. In microservices each one becomes a design decision: what is the timeout, is a retry safe, what happens if the same request arrives twice, who knows the address. The answers come back as code, tests and operations.
Fowler's Prerequisites and the Microservice Premium
In his August 2014 post MicroservicePrerequisites, Martin Fowler lists three capabilities you should have before starting with microservices. Rapid provisioning: being able to bring up a new server or environment within hours, largely through automation. Basic monitoring: quickly noticing both technical problems (error rates, whether a service is up) and business problems (a sudden drop in orders), and rolling back if needed. Rapid application deployment: being able to take a service to test and production through a pipeline that ideally finishes within a couple of hours. Fowler also sees a cultural shift behind these: developers and operations working closely together, from deployment to incident analysis.
Compare that with Bookstore today: 3 developers, 1 part-time ops person, one deployment a week. None of the three prerequisites is fully in place. That doesn't mean Bookstore will never be split into services; it means these capabilities must be gained on the monolith before splitting it. All three are valuable for a monolith too: a team that can deploy a single application safely within minutes wins whether or not it ever moves to microservices.
In May 2015 Fowler took this one step further in MicroservicePremium: microservices carry a "premium" paid in deployment, monitoring and failure handling, and that premium only pays off for systems too complex to manage as a monolith. In a simple system, the premium lowers productivity. Most systems are better built as a well-modularized monolith.
Operational and Cognitive Cost
Let's make the premium concrete. Imagine you split Bookstore into six services. Things you do once in a monolith will now be done six times, or solved by a platform that covers all six:
- Six build and deployment pipelines, six container images and their version history.
- Six databases (or schemas): separate migrations, backup and restore plans.
- Collecting the logs of six services in one place, and distributed tracing to follow a request across services.
- For each service, a health check, alerts, a dashboard and an answer to "who wakes up at 3 a.m.".
- Contracts between services, versioning and backward compatibility tests.
- Authentication between services, secrets and certificates.
- Local development: starting six services and a message broker to try out one feature.
These aren't one-off setup jobs but things operated continuously. On top comes a cognitive cost: what a developer must hold in their head to find a bug is no longer a call stack but several processes, several databases and messages that can get lost in between. The cognitive load concept we covered in the Introduction to Architecture series post "Conway's Law: How Team Structure Shapes Architecture" applies directly: a 3-person team carrying six services, a broker and a tracing stack at once spends its whole mental capacity on infrastructure instead of the product.
Tools reduce some of this. On the .NET side, the planned Backend series ".NET Aspire for Distributed Applications" posts bring most of local development, service discovery and telemetry ready-made. But tools don't bring the cost to zero; they only lower the one-time setup cost. The operating cost, the number of things kept alive every day, keeps growing with the number of services.
Reading the Prime Video Case Correctly
In March 2023 the Prime Video Tech blog published a post by Marcin Kolny: the team described how moving a monitoring tool that detects video and audio quality issues in streams from a distributed design into a single process had cut infrastructure cost by more than 90%. Within weeks it circulated under headlines like "Amazon moves from microservices back to a monolith". The original post is no longer online and its address redirects; so the summary below is based on DevClass's report from 5 May 2023 and The New Stack's assessment from June 2023.
In the system described, components were orchestrated with AWS (Amazon Web Services) Step Functions, and decoded video frames travelled between components through S3 (Simple Storage Service). Two problems appeared. Step Functions charged per state transition and there were several transitions for every second of a stream; as scale grew they hit account limits. Writing and reading intermediate data to and from S3 was also a significant cost. The team gathered the components into a single process, carried intermediate data in memory and ran the system on EC2 (Elastic Compute Cloud) and ECS (Elastic Container Service).
The conclusion "microservices are dead" doesn't follow from this case; the lesson is narrower and more useful. First, what was consolidated was not all of Prime Video but one subsystem. Adrian Cockcroft, who led cloud architecture at Netflix and later at AWS, read the change in The New Stack not as "microservices to monolith" but as moving "from Step Functions to a single microservice", and a Prime Video spokesperson confirmed that the project began this way and ended as a scalable service. Second, the problem was not the distributed design itself but moving large volumes of data over the network and storage at every step: the "transport cost is zero" item from the fallacies list had turned into an invoice. Third, the team realized it had split the components in the wrong place and merged them. That is not a failure; what both Newman and Fowler say is precisely that service boundaries cannot be drawn right the first time and must be reversible.
When Unnecessary, When Inevitable?
The reason to move to microservices should be a measurable problem, not a trend or another company's story. The checklist below ties each question to a signal; the last column is Bookstore's answer today:
| Question | Signal in favor of services | Bookstore today |
|---|---|---|
| How many teams work in the same codebase? | More than one team, and teams waiting on each other's releases | One team, 3 developers: no |
| Do releases collide? | One team's change waits because of another team's untested code | One weekly release, no collisions |
| Do the parts have different load profiles? | One part scales very differently from the others, and that causes a real cost difference | Catalog search 10x during campaigns: watch it |
| Is fault isolation a business need? | One part going down must not stop the critical flow | Email must not break checkout: a queue is enough |
| Must compliance scope be reduced? | Keeping an audited part, such as card data, behind its own boundary shrinks the audit scope | Card data stays with the payment provider: no |
| Are the prerequisites in place? | Automated provisioning, monitoring and deployment within minutes | Not yet |
Bookstore's answer today is clear: microservices are unnecessary, a well-modularized monolith is enough. But the 18-month goal we set in the Introduction to Architecture series changes the table: four stream-aligned teams, two countries, a mobile app and ten times the traffic during campaigns. At that point the first two rows turn into "yes", and scaling Catalog search separately becomes a real cost issue. The inevitable moment is when teams waiting on each other shows up measurably in lead time. Even then the first step should not be splitting the whole system but taking out the most independent part (in Bookstore, most likely Catalog search or Notification); the techniques for that migration are in the last post of the series.
Common Pitfalls
CV (curriculum vitae) driven architecture. Choosing an architecture not because the problem calls for it but because it will look good on the team's résumés. The symptom is easy to spot: the reasoning behind the decision names a technology or a company, not a Bookstore problem.
One team, fifteen services. In a system where three developers look after fifteen services, independent deployability has no customer: with no other team waiting on a deployment, the benefit of independence is zero and the cost is fifteenfold. As Newman stresses, the biggest benefit of microservices is that several teams can work without waiting on each other.
Splitting into services without changing the team structure. The code is split into six services, but the same team still builds every feature end to end, or, the other way round, teams are still organized by layer (frontend, backend, database). Conway's Law is merciless here: if the communication structure doesn't change, service boundaries drift back to team boundaries over time and a distributed monolith emerges. We demonstrated this mechanism with measurements in the Introduction to Architecture series post "Conway's Law: How Team Structure Shapes Architecture".
Using Prime Video in reverse. "Even Amazon went back to a monolith" is as weak a reason as "Netflix uses microservices". Both cases are another system's, another scale's and another team's decisions.
Leaving the prerequisites for later. A plan of "split into services first, set up pipelines and monitoring later" means losing hours in the first production incident without knowing who broke what.
A Small Experiment
★☆☆ Run the program from this post on your machine and note the ratio. Then add a 2 KB description property to BookPrice and measure again. How did the ratio change? What does that tell you about "bandwidth is infinite"?
★★☆ Make the /slow endpoint wait 900 milliseconds instead of 3 seconds and leave the timeout at 1 second. Does the call succeed? Now imagine this call is one of five calls in a checkout chain: set a budget for the user's total wait and work out each call's share.
★★★ Fill in the checklist table for your own team. For each row, find data that supports your answer: the release calendar of the last three months, the number of days one team waited for another, the CPU and memory usage of the parts. The rows you can't find data for are the things to measure before you decide.
We've seen the cost; if it's worth paying, where do we draw the boundaries? In the next post we go from business capability and bounded context to service boundaries: Newman's types of coupling, the "entity service" anti-pattern, why "Customer" is three separate models in Ordering, Payment and Notification, and the team that is each service's single owner.