PTEN

Invisible Coupling: When Your Microservices Become a Distributed Monolith

Synchronous call chains turn microservices into a distributed monolith. How temporal coupling costs you p99 latency, money and resilience.

Author
Will Peixoto
Published
27 Oct 2026
Reading
13 min
Language
EN / PT

🇧🇷 Leia em português: Acoplamento invisível: quando seus microsserviços viram um monolito distribuído

Black Friday peak. The checkout has microservices, a CI/CD pipeline, containers, serverless functions, every piece with its own deploy and a green dashboard. Then the settlement service, way at the end of the chain, starts responding slowly, and within minutes the whole payment authorization flow is timing out. On paper, every service was independent. The customer still gave up on the purchase while waiting for the approval.

That scene has a name, invisible coupling, and so does the system it produces: a distributed monolith. If this sounds familiar, stick with me, this post is for you. It kicks off a short run of posts on the topic: next I'll show how to break these dependencies by separating the hot path from the side-effects, walk through the patterns that hold that split together, and wrap up hands-on with a prototype on AWS.

Invisible coupling: what a distributed monolith really is

Coupling is a measure of how much one service depends on another to work. My provocation, right up front: if your microservice only responds because another one responded first, that independence is an illusion. In practice, it's part of a distributed monolith.

Sam Newman explores that term, distributed monolith, in Monolith to Microservices, and he describes exactly the trap: you paid the full price of running separate services (network, deploys, observability, tracing) and didn't collect the prize, which was independence. The danger is the false sense of modernity. All the microservices machinery is there, but when one point fails, the system fails with it. Distributing the system didn't erase the coupling, it just made it harder to see.

The comment that made me write this

A few days ago, in a discussion about serverless, I read an argument that went roughly like this (translated from Portuguese):

“In complex environments, when microservices depend on others, and those depend on third parties, the latency of those requests drives cost way up and can make the model unviable. Also, in compute-heavy scenarios, serverless may not be the best option either because of cost.”

This argument shows up all the time, and it touches on something real: dependency chains between microservices are expensive, fragile and slow. So far, I agree.

Except this isn't a serverless problem. It's an architecture problem. Running on AWS Lambda, ECS, EKS or a VM in your server closet, if the integration is poorly designed the outcome is the same: latency, cost and fragility. A synchronous chain doesn't get faster because you swapped the runtime underneath it.

That's why the “serverless vs. containers” debate feels to me like fighting over the wrong dish. Each one has its place (stateful workloads and long-running jobs call for containers; event-driven and spiky traffic is where serverless shines), but that catalog is for another day.

The central point of this post is this:

A microservice's independence is decided by how you design its integration, and the runtime doesn't get a vote.

Temporal coupling and accumulated latency

Temporal coupling happens when a service can only respond if another one is up at that exact moment. The availability and latency of one service start contaminating every other service in the chain.

Then comes the cascade. Each synchronous hop adds a few milliseconds, which looks like nothing at first. Then you add up five calls in series and latency blows up right in the tail percentiles, those slowest requests that make up 5% or 1% of the total. That's where the customer either completes the purchase or abandons the cart.

Let's do some back-of-the-napkin math, with illustrative, hypothetical numbers, just to picture it. Five services in series, each responding in 40ms at the median: the whole chain sits around 200ms. Fine so far. But real requests don't live at the median. If each hop has a 1% chance of hiccuping and taking 2 seconds, in a chain of five the chance that at least one of them hiccups gets close to 5%. One in every twenty transactions will feel it. And the customer who felt it doesn't care that YOUR average looks great. It's the same series-component arithmetic that drags availability down in the post on why multi-cloud can leave you less resilient: every extra link multiplies the odds of something going wrong.

The distributed monolith in action: payments

Back to the payment from the opening, because that's where every millisecond counts. The customer is standing at the checkout waiting for approval, or the e-commerce site needs to confirm before they give up. A delay of a few seconds costs revenue and trust.

And the bill doesn't stop at the merchant: the card network charges too. Mastercard's Transaction Processing Rules (sections 2.3 and 2.4) require the issuing bank to answer the authorization within a maximum time, and if the answer doesn't arrive in time the transaction times out and falls to Stand-In Processing. For Maestro and ATM transactions, an issuer with more than 2% of failures from unavailability, malfunction, or timeout for two consecutive months is already below standard, and anyone who misses the performance standards is subject to a USD 15,000 assessment on the first occurrence, rising to USD 20,000 from the third within 12 months. In payments, latency is part of the contract.

In the traditional synchronous model, an authorization walks down this staircase: the Authorization service calls Fraud, which calls Limits, which calls Settlement. The response only climbs back to the customer once the last one answers. Look at the linear shape of the diagram below: nobody moves until the one below responds.

Synchronous payment chain: Authorization, Fraud, Limits and Settlement in series; the response only returns once every service has answered
Fig. 01

That's the distributed monolith in practice: several services tied into a single linear flow. Each hop adds latency, multiplies logs, traces and retries, and adds one more point that, if it goes down, takes the whole transaction with it.

At low load, it works. The problem shows up under high concurrency: the Black Friday from the opening, a promotion that goes viral, the daily volume of a digital bank. Average latency even looks acceptable, but p95 and p99 shoot up (remember the napkin math?). Cost grows with it, and retries make the bill worse: the Amazon Builders' Library article on timeouts and retries describes a five-deep stack of service calls where each layer retries three times on its own, and the load on the database at the bottom gets multiplied by 243. In serverless, the cost of waiting is literal: in a synchronous chain of Lambda functions, the functions at the top keep running (and keep being billed for duration) while they wait on the ones below, the anti-pattern AWS itself documents. And a small percentage of slow transactions is enough for the frustration to show.

How we got here

Nobody woke up one day wanting to build a distributed monolith, so it's worth understanding why this pattern spread so widely. During the microservices adoption wave, one situation became routine: “I need data that lives in another microservice”. Nothing wrong there, that's normal and acceptable in any distributed system. The problem was the answer, which became something close to a mantra: “then build an API, the other service exposes it and I consume it”. That was the by-the-book approach at a lot of companies, and I've watched the pattern repeat itself in projects I worked on and in conversations with people in the field.

And it was attractive. It leaned on good practice: well-documented REST APIs, OpenAPI/Swagger contracts, fast integration. For a while, it worked. Once the domain starts demanding low latency, high availability and resilience, the model shows its seams. Every new synchronous dependency is more temporal coupling. What was born to decouple turned into another kind of coupling, distributed and far more expensive to maintain.

Try this at home

Want to know if this story is yours? Run a thought experiment, no need to break anything in production: pick a service in your system and imagine it going down right now. How many others stop with it? If the answer is “the whole transaction”, invisible coupling already lives there.

A useful variation to bring to your next standup: can you deploy this service at peak time without warning the neighboring teams? If the answer is no, that independence only exists on the org chart. And that's exactly Sam Newman's criterion: when a set of services has to go to production together, you have a distributed monolith.

Where independence is actually designed

So the deciding factor is how the services talk to each other. A synchronous chain creates temporal coupling and makes everything fragile. The opposite path has a name and tooling too: Event-Driven Architecture (EDA) to reduce direct dependencies, clear domain boundaries so each service owns its own data, and the split between the hot path (the fast path the user is waiting on) and the side-effects (analytics, notifications, the receipt email, which can run in parallel without holding anyone in line). Werner Vogels summed it up in his re:Invent 2022 keynote with “the world is event-driven”: the real world is asynchronous, and your architecture thanks you when you stop pretending it isn't.

The same payment, now event-driven

Go back to the payments staircase. In the redesign, the hot path keeps only what decides the approval: Authorization checks Fraud and Limits, responds to the customer and publishes a “payment authorized” event. Settlement, Notification and Analytics become consumers of that event, running in parallel, each at its own pace. If Settlement is down, Authorization keeps responding, and the event waits in the queue until it comes back. That event can travel over a queue, a bus or a stream, and each choice has its own trade-offs around ordering, replay and cost (I compared the streaming options in the post on Kinesis, Firehose, Flink or MSK).

Event-driven flow: Authorization responds to the client and publishes an event; Settlement, Notification and Analytics consume it in parallel
Fig. 02

In the card world, settlement is already asynchronous by nature: authorization happens on the spot, and clearing and settlement come later, usually in batches. The synchronous design we saw earlier was fighting the domain itself.

There's a whole toolbox behind this split (Circuit Breaker, Bulkhead, Outbox, CQRS, Saga, idempotency keys) and, above all, the decision of when to use each one. And none of them comes for free: in the post on the price of high availability I argued that resilience is a decision with a cost, and every one of these patterns goes into that bill. The rest is material for the next posts, where I'll go pattern by pattern. For now, hold on to the core idea: what separates an independent microservice from a hostage is the integration.

This ties straight into the Reliability pillar of the Well-Architected Framework. One of its best practices, implement loosely coupled dependencies, recommends making interactions asynchronous where possible, usually with a durable layer in between (an Amazon SQS queue, a Kinesis stream, AWS Step Functions), precisely so that one component's failure stays isolated instead of taking down the entire chain.

Remember the scene from the beginning, the whole checkout timing out because settlement, at the end of the chain, choked? In the event-driven design, that same slowness becomes a growing queue, and the customer gets the approval. What took the checkout down was the design of a chain built to fall together, and a design can be redone. No rewriting the system from scratch and no runtime migration: the work is to go through the integrations one by one and ask which ones really need an answer right now.

What about that comment from the beginning?

I'm coming back to it because this whole post is an answer. In its first part, the comment got the symptom right: a microservice that depends on another, which depends on a third party, gets slow and expensive. It just blamed the wrong culprit. The latency comes from the synchronous chain, and that chain is just as slow on containers, on a VM or on Lambda. And a good part of the cost it put on serverless's tab is the cost of waiting: Lambda calling Lambda pays for duration while it waits for the answer. Take the waiting out of the path, with a lean hot path and everything else turned into events, and that bill drops along with the latency.

In the second part, about heavy processing, it has a point. A workload with high, constant CPU can come out cheaper on containers, and that runtime choice is worth making case by case. But that's a different conversation. Swapping Lambda for containers in a synchronous chain only changes where the waiting happens.

What you take away from this post

If I could leave you with just a few ideas, these would be the ones:

  • If your service only responds after another one has responded, it's part of a distributed monolith.

  • Temporal coupling is the villain: one service's latency and availability contaminate the whole chain, and the bill shows up in p95/p99, far from the average.

  • Swapping the runtime doesn't fix a synchronous chain. Serverless or containers, what matters is how the integration is designed.

  • Independence is designed: EDA, clear boundaries, the hot path separated from the side-effects, and the named resilience patterns (Circuit Breaker, Bulkhead, Outbox).

So, to finally answer that comment: the question was never whether to use serverless. What decides whether your system survives Black Friday is how you design the integration between services, and that holds for any runtime.

In the next post I'll show how to break these dependencies in practice, separating the hot path from the side-effects to cut latency and make your microservices truly independent.

Before that, I want to hear from you: have you ever heard (or said) that serverless gets expensive when one service depends on another? After this post, does that argument still hold up? Tell me in the comments. If this post helped, drop a like and share it around. Thanks a lot!

BUILD. SCALE. REPEAT. =D

Will Peixoto

Will Peixoto is a Staff Engineer at Serverless Guru and writes about serverless, AWS architecture and AI agents: what he'd pick, why, and what it costs.

More from the series

New posts, straight to your inbox

One serverless, architecture or AI agents topic on AWS per issue: what I'd pick, why, and what it costs. No spam.

Opens the subscription page in a new tab.