Five Signals a Service Boundary Has Stopped Paying Rent

And what each one actually points at, which is rarely a merge.

Picture two services, one team, separate pipelines for three years. Latency is fine. Neither is a hub. They fail independently, mostly.

Then last quarter, eleven of the sixteen releases of one of them could not go out until the other had been deployed first. That is the only thing wrong with this boundary, and on its own it is enough to put the boundary on somebody’s list.

Those numbers are illustrative. The shape behind them is what I want to argue about, and the shape is common enough that you can probably name the pair in your own system before you finish this paragraph.

Nobody on that team thinks the architecture is broken. It ships, the dashboards are green most weeks, and if someone suggested the boundary had outlived its purpose, the conversation would come down to who argues more persuasively. The eleven-of-sixteen number has been sitting in their deploy history the whole time. Nobody has looked.

We have a rich vocabulary for splitting software. Extract a service. Find the bounded context. Strangle the monolith. Every step has a named pattern and a migration guide. The reverse direction is thinner, and what is missing is evidence rather than writing: a set of measurements that tell you a boundary has stopped earning what it costs, before the argument turns into a matter of taste.

Below are the five I keep coming back to, with the query for each where one exists. For each I have tried to say what it is actually evidence for, which is almost never “merge these two services.”

What a Boundary Buys, and What It Charges

A service boundary makes three technical promises and one organizational one. One part of the system can be deployed without waiting on another. Expensive workloads can be scaled independently. Failures can be contained on one side. And two teams with different roadmaps stop negotiating every release, because the interface says where one team’s decisions end.

It also runs a tab that arrives in increments, which is why it gets underestimated. A function call becomes a network call. An internal interface becomes a versioned contract with a deprecation policy. A local transaction becomes a consistency problem you own now. A stack trace becomes a distributed trace, if context propagation survived the last queue hop.

Some of the bill is arithmetic. Two services that each deliver 99.9%, with their failures approximated as independent, give a serial path near 99.8%. That turns 43 minutes of monthly downtime budget into 86. Neither service got less reliable. The path changed.

Latency compounds less tidily. Queueing, retries, and correlated load can push the end-to-end P99 past anything the per-service dashboards suggest, which is why an interaction problem so often looks fine from inside each service.

Two caveats before quoting any of that at anyone. Published service SLOs frequently already account for the dependencies underneath them, so multiplying can double-count. And a timeout with a real fallback breaks the chain outright, which is the point of the third signal below. Isolation is something you build. The arithmetic describes the default, and the default is what you get when nobody builds anything.

None of this makes decomposition a bad idea. It means a boundary has to buy something in exchange for what it charges. The exchange rate moves even when the code does not, and having been built is not a reason on its own.

That is harder to notice than it sounds, because decomposition produces a convincing kind of fake progress. Every repo gets easier to read, every deployment unit shrinks, and local reasoning improves everywhere while global reasoning quietly gets worse. Local reasoning is the only kind most of us do on a Tuesday afternoon.

Read the Graph, Not the Repository

Split a checkout component into Pricing, Inventory, and Promotions and you get three boxes where there was one. The boxes got smaller and the arrows got more important, which is the trade nobody puts on the slide. Each arrow can carry latency, retries, failure propagation, contract evolution, and an ownership handoff. Three small boxes joined by three arrows is not obviously simpler than one medium box.

Service count is therefore a weak measure of architecture quality, and the useful questions are all about the graph. Which dependencies sit on the highest-traffic paths? Which boundaries change together? Where does slowness propagate? Which services have quietly become hubs that a dozen customer journeys pass through? And which pairs can no longer ship without each other?

Service count is genuinely good for one thing, which is planning for the people who operate the services. Fifty services need fifty on-call stories whether or not the graph is healthy. That is a real cost, and it belongs in a different conversation from this one.

Five Signals

Nothing on this list tells you to merge. Each signal tells you a specific benefit you assumed you were buying may have stopped arriving. Any one of them is enough to investigate. Several on the same boundary raise the priority and strengthen the case that the boundary itself, rather than one local implementation detail, deserves review.

Four of the five are computable with what you already have. The hub signal needs an afternoon of hand labeling on top, which I will come back to. Change coupling comes out of git log. Release coupling comes out of your deploy records, with one caveat about what those records can actually prove. Call counts and serial depth come out of traces you are probably already sampling. A signal nobody can compute is a signal nobody will check.

Treat all five as guardrails rather than targets. There is a tension here I have not resolved. The point of measuring is that somebody can lose an argument to a number, and any number that can win arguments eventually gets managed instead of measured. Two mitigations, neither complete. Keep these off the dashboards that feed performance reviews. And let a number open a review but never close one, so that whatever gets gamed is a prompt rather than a verdict.

1. Changes arrive in pairs

Dependencies are fine. Every useful system has them. The revealing measurement is change coupling: how often do commits touching A land in the same window as commits touching B? Pick a window that matches your release cadence, and read the ratio rather than the raw count.

In a monorepo, that is two commands. First, the set of weeks in which each top-level module was touched:

git log --since="6 months ago" --date=format:'%Y-%W' \
--pretty=format:'%ad' --name-only \
| awk '/^[0-9]{4}-[0-9]{2}$/ {w=$0; next}
NF {split($0,p,"/"); print w, p[1]}' \
| sort -u > weeks.txt

Then the overlap for a pair, over the union of weeks either one moved:

both=$(comm -12 <(awk '$2=="pricing" {print $1}' weeks.txt | sort) \
<(awk '$2=="inventory"{print $1}' weeks.txt | sort) | wc -l)
either=$(awk '$2=="pricing" || $2=="inventory" {print $1}' weeks.txt | sort -u | wc -l)
echo "scale=2; $both / $either" | bc

Separate repositories need the same thing joined on the week column instead of on the path prefix.

Several things inflate that ratio with no coupling present at all. Sprint boundaries and release trains synchronize commits on their own. A team small enough that one person touches everything will happily correlate with itself. Run the same query against two modules you know are unrelated and use whatever you get as your floor. Correlated commits are a prompt to go look, not proof of coupling.

The interesting case runs both ways. If a change in either direction routinely creates work in the other, independent evolution is no longer something this boundary provides.

Segment’s 2018 account shows the expensive version of this, in a shape worth naming separately because it is not a pair. They ran one service per data destination, more than a hundred of them, every one depending on the same shared libraries. A library change had to be rolled across a hundred repositories, so it usually was not, and the versions drifted until each destination ran something slightly different. That is change coupling across one-to-many, which is what the pattern looks like when the shared thing is a library rather than a contract. The bill arrives as divergence instead of as coordination meetings.

Whatever the shape, the remedy is worth slowing down over. A boundary in the wrong place has three fixes: move the misplaced responsibility across, merge, or accept the coordination cost. Merging gets reached for because it is the only one with a name. If pricing logic keeps leaking into the inventory service, moving one module may be the entire fix.

2. The boundary taxes the hot path

Some boundaries get chatty. One customer operation crosses the same seam three, four, six times, or a large share of the request budget is spent waiting on the other side. The Chatty Threshold I use has two triggers: more than three synchronous remote calls in one request, or remote wait above 40% of end-to-end latency, measured as wall-clock on the critical path and not summed across concurrent calls. Neither is a law. They are where I stop assuming and start measuring.

Two other measurements sit next to it, and both get conflated with it constantly.

  • Sync Hop is serial depth: the number of synchronous stages on the longest customer-visible path. It sets the latency floor. A parallel fan-out to four services counts as four calls but only one stage, assuming they run concurrently and the caller waits once for the group.
  • Tail Index is P99 over the median. A rising ratio on the same journey corroborates the story, with one reading rule: check both terms before you believe it. The ratio also climbs when the median improves, and a cache that speeds up the common case can look identical to a tail that is falling apart.

Which means a single scatter-gather to four services trips the call-count trigger on its own. That is intended. The trigger flags width, and width is only a problem sometimes, which is exactly why Sync Hop sits beside it. A boundary can be wide, deep, or both, and the fix differs in each case.

A variant where the bill is not latency. In 2023 a team at Prime Video published a case that belongs here even though it trips neither trigger above. Their video quality detectors ran as separate serverless components, and frames moved between them through object storage, so transfer cost tracked the size of the thing being analyzed rather than the work of analyzing it. Putting the components in one process and passing frames in memory cut infrastructure cost by over ninety percent. The new design scales by cloning the whole unit, a constraint they accepted rather than a free win.

The reason to keep it in this section is that the mechanism is the same and only the currency changes. A seam charges per crossing. Usually you pay in milliseconds. On a data-heavy path you pay in transfer, and no latency threshold will ever fire. If a boundary sits between two stages that hand large payloads to each other, measure bytes across the seam per unit of work and put that number next to the latency numbers.

Know the dispute before citing it anywhere. The argument ran for weeks, the original has since vanished from Amazon’s site, and Paul Johnston’s rebuttal is largely right that the headline oversold it. Almost all of it is definitional. Does the result deserve the word monolith, and does one team optimizing a serverless prototype teach anything general about microservices? Nobody disputed the part that matters here. Moving data between separated components cost far more than passing it in the same process, whatever you call the architecture on either side.

The trap is that chattiness is often an interface problem wearing a boundary problem’s clothes. Six crossings per request is usually an N+1 or a missing batch endpoint, so coarsen the interface first. If the pattern survives a proper API, the remaining coupling is structural. The domain may be one thing, or the workflow may be owned on the wrong side. Either way the boundary is the subject, and the endpoint is not.

3. Failure crosses the boundary anyway

Checkout depends synchronously on Pricing for nearly every request. Pricing gets slow, Checkout threads pile up waiting, retries add load, and customers experience an outage of Checkout. Two processes, one failure domain. This is one common symptom of a distributed monolith.

Now the part that usually gets stated backwards. Merging does not fix it. If pricing computation is slow, it is slow in-process too. What merging removes is the illusion of isolation, plus the thread pool exhaustion and retry amplification the network added on top of the original slowness.

So this signal is a fork rather than a conclusion. Either you buy the isolation you thought you had, with bulkheads, timeout hierarchies, retry budgets, and a real fallback for a degraded Pricing, or you stop paying network costs for isolation you never receive. Both are defensible. Paying and not receiving is the only clearly wrong option.

4. One service has become a hub

If this list were ordered by damage instead of by how easily each one is measured, this signal would be first.

Start with Dependency Degree, upstream plus downstream, weighted by traffic, change frequency, and data sensitivity. Raw caller count treats every edge as equal and flatters the wrong services. Two of those weights are measured and two are not: traffic and change frequency come out of systems you already run, while data sensitivity and business criticality have to be classified, which means somebody sits down and labels every service by hand. It costs a spreadsheet rather than a platform, and it is where teams most often stall.

Hub risk goes further. Connectivity turns dangerous once it combines with business criticality, error-budget burn, and how far a failure travels. A service with two dozen callers, six of them on revenue paths, is shared infrastructure being maintained like a feature service, and one bad deploy there can become a cross-journey incident. If you want a single number for the quarterly ledger, multiply the weighted degree by a criticality factor and by the Tail Index; the arithmetic matters less than the fact that the three terms are visible together.

A hub is not automatically a merge candidate or even a design failure. Auth, configuration, and payment gateways earn the position, and three interventions compete for any hub worth worrying about: split it further, buffer it with bulkheads and rate limits and backpressure so shocks stay local, or consolidate it. A thick, well-defended hub is often the cheapest of the three. Eliminating hubs is not the goal.

What separates a safe hub from a dangerous one is governance. A hub can emerge by design or by accident, and either can be fine. What matters is whether it has an owner, an error budget, containment, and a review mechanism before its connectivity becomes propagation risk.

5. Releases arrive in lockstep

Two pipelines prove nothing on their own. Make it concrete as a Release Coupling Ratio:

RCR = releases in the window that required a coordinated deploy with another service or team, divided by total releases in that window.

Lockstep deployment is the strongest form, where A cannot reach production safely unless B goes with it. Of everything on this list, that ratio maps most directly onto a promise the boundary made when somebody created it.

A word about measuring it, because this is harder than it looks. Deploy records show you sequence. They do not show you necessity. A deployed ten minutes after B tells you nothing about whether A could have gone alone. Everybody who reaches for this metric hits that wall.

Two ways through. The forensic one: flag a release of A as coupled when a deploy of B lands within your validation window and A’s changeset touches a shared contract, schema version, or generated client. Expect false positives from release trains, weekly cadence, and one person shipping both sides, and expect false negatives where the coupling lived in a runbook instead of in a diff. Sample twenty flagged releases by hand before you trust the ratio.

The better one, if you can spend a week on it: stop inferring and start recording. Add one line to the release template.

Requires: <service>@<version> # or: none

That turns a forensic problem into a counting problem, and the first month of data is usually more persuasive than any query you could have written against the past.

On thresholds. I do not have a single number for this one, and the asymmetry with the Chatty Threshold is deliberate. Call counts have a natural scale. Release coupling does not. A ratio of 0.2 is alarming for two services owned by different teams on different roadmaps, and unremarkable for a service and the worker it was always going to ship alongside.

The one reference point I use is not mine: an RCR at or above 0.5 is the line where team boundaries and code boundaries have visibly diverged, which is a Conway problem rather than a boundary problem and wants a different conversation. Below that line, the question is the gap between the ratio and what the boundary was sold as buying. A pair sold on independent deployability and running at 0.3 is not in crisis, and it is also not delivering the thing it was bought for.

Not a signal: shared ownership

One team owning both services is not evidence of a bad boundary. Plenty of teams own several services and run all of them well. Ownership belongs in the analysis from the start, because making responsibility clear is one of the things a boundary exists to do. Settling the merge question by itself is not something it will do.

It does change the price. When one team owns both sides, merging avoids a cross-team reorganization, though the data migration, runtime, scaling, and security work is unchanged. When two teams own them, all of that still applies and an organizational negotiation with a headcount conversation sits on top. Same technical evidence, materially different project.

Not on this list: the shared database

Two services reading each other’s tables is the one thing here I would not call a signal, because a signal is something that opens an investigation and this closes one. At the data layer the boundary has already stopped existing, whatever the topology diagram says. There is nothing left to measure.

It stays off the list for a second reason. The five above tell you whether a boundary is earning. A shared database tells you what a change will cost, which is a separate question and the one that decides whether any of this is actionable. Separate schemas with separate migration histories turn consolidation into a data migration project. A shared schema removes that step and should be read as a diagnosis rather than as good news.

Where Each Signal Points

The five do not converge on one answer, and pretending they do is how essays like this go wrong.

Four of the five can fire on a boundary that should stay exactly where it is. A chatty seam usually wants a better API. A hub usually wants governance and containment rather than demolition. Failure that crosses the boundary wants a decision about isolation, because merging removes the illusion of it rather than granting the real thing.

The fifth is different. Independent deployability cannot be added to a boundary from the outside. Either the two things can ship apart or they cannot, and no amount of tooling around the edges changes that. When release coupling is the signal that fires, the boundary itself is the subject.

One thing no single reading can show you is direction. A boundary that looks bad this quarter may be recovering from a migration. A boundary whose release coupling and tail metrics have not improved across two consecutive reviews is the one worth acting on, and that difference is only visible if somebody wrote the first number down.

Evidence First, Verdict Later

The hard case is never the obviously bad boundary. It is the boundary that gave up a few percent of its value every quarter for three years, which nobody ever experienced as a change. Slow declines need a measurement to be visible at all.

So the useful output of this list is a sentence a team can agree on rather than a merge proposal. Something in this shape: we bought this boundary for independent deployability, eleven of the last sixteen releases needed the other side shipped first, and we are still paying network and contract costs for something that stopped arriving a year ago. That sentence beats any threshold in this post, because nobody wins an argument about architectural taste and somebody can lose an argument about a number.

Rent has a due date. These five measurements are how you find out whether the last few payments went through. What to do when they did not is a separate decision, and it has more than one right answer.

The measurements in this post, along with the Hub Risk Score arithmetic and the Modular Monolith experiments that test a boundary before you commit to one, are developed in Chapter 3, “The Illusion of Small Services,” in Beyond Microservices: Building Systems That Align Architecture, Teams, and Flow.

Comments

Leave a Reply