Repair it, merge it, or write down why you are doing neither. A service boundary should keep paying rent.
The release runbook says deploy A, wait for validation, deploy B, verify together. It has said that for two years. Nobody wrote it as policy. It accumulated, one compatibility shortcut at a time.
The two services still have separate repositories, separate pipelines, separate runtimes, and every network cost that implies is still being charged: the serialization, the timeouts, the versioned contract, the distributed trace.
It would overstate things to say the boundary now buys nothing. Independent scaling still works. Runtime resource isolation still works. What stopped is independent deployability, one of the promises a service boundary makes when somebody creates it.
That distinction decides how much of this applies to you. If the split was bought for scaling or blast-radius containment, coupled releases may be a price worth paying. They remain a price, since sequenced deploys slow every incident and complicate every rollback, and the section on when merging is wrong is written mostly for you.
If the split was bought for independent deployability, you are paying a full distributed-systems bill for a benefit that stopped arriving. The bill is not abstract: the versioned contract, the distributed trace, the retry policy, the second on-call runbook, and a serial path that turns 43 minutes of monthly downtime budget into 86. Read that last figure as the default rather than as a law, since a timeout with a real fallback breaks the chain and a published SLO may already price its dependencies in.
This state is not a compromise architecture to settle into. It is a decision point with two exits, and most teams take neither, because there is no meeting where either proposal would belong. There is also a legitimate way to take neither for now, and it looks nothing like what most teams actually do.
You can see the state in deploy records without instrumenting anything new. Count the releases that could not go out alone, divide by total releases, and you have a Release Coupling Ratio.
Two things to hold onto while you count. Deploy records show sequence rather than necessity, so either hand-check a sample of whatever your query flags or add a Requires: <service>@<version> line to the release template and start counting forward. And two features shipping on the same day do not count, because the ratio is about what it takes to reach production rather than about when a customer sees the result.
Teams running that count for the first time tend to be surprised, which is the point. Nobody decided this.
The Choice Is Not Two Services or One
Most of this conversation gets stuck because people frame it as a binary. On the release and runtime axis there are three operating states worth distinguishing, plus a fourth move that is a separate decision entirely.

Position 1 is what healthy release independence looks like. Two services, two runtimes, and either one can ship without the other. That is one axis rather than a clean bill of health, since a pair can release independently and still share a database or propagate every failure.
Position 2 is where this post started, and it arrives by drift. Nobody proposes coupling their releases. A compatibility shortcut here, a shared migration there, a rollout that has to be sequenced once and then always. Two years later the runbook is the architecture.
So position 2 is a diagnosis, and it forks two ways.
Exit one: restore release independence
Maybe runtime isolation or independent scaling still earn their keep. Maybe a team’s autonomy was supposed to rest on deploying without waiting. Then the work is to repair whatever made the releases dependent: backward-compatible contracts, expand-and-contract schema changes, consumer-driven contract tests in CI, and a feature flag that separates the release decision from the deploy. That last one has a name worth using, because Release ≠ Deploy is the property doing the work here. Once exposure is a separate decision from shipping bits, most of what looks like release coupling turns out to be rollout sequencing that nobody unbundled.
None of it is research. Merging instead would throw away a boundary that is still earning.
This is the more common right answer, and it is unglamorous enough that it rarely gets written up as a migration story. Most coupled boundaries got there through ordinary contract rot rather than because a domain turned out to be one thing, and rot is repairable.
One diagnostic sits underneath this exit. A boundary with no consumer-driven contract tests and no schema gates in CI has never had its independence tested, whatever the topology says. Read that as a boundary nobody has been maintaining rather than as a merge signal. Adding the gates is the cheapest thing on this page if what you want is to keep the split.
Exit two: consolidate
If those benefits no longer justify a distributed boundary, and consolidation is acceptable on cost and reversibility, one consolidation path is position 3. Consolidation is a family rather than a single move, with tighter module boundaries at the reversible end and structural merges at the other.
Position 3 is the one people usually mean by merge, and the details are the whole point. The network boundary disappears. The domain boundary does not. The modules stay separate. The relevant schemas and contracts stay explicit. The tests stay. The observability stays. What you stop paying is the distributed-systems tax on a boundary that stopped justifying distribution. If the result sounds like a modular monolith, that is because it is one, arrived at from the other direction than usual.
Two kinds of enforcement do different jobs here. Module visibility and architecture tests keep the structural coupling honest. Semantic contracts keep the behavioral agreement honest: ordering guarantees, staleness tolerance, idempotency scope, the invariants a type signature never captured. Removing the network does nothing to the second category.
Position 4, fusing the code and the data completely, is where most people’s mental image of “merge” lives. It is a separate domain-modeling decision. It arises only once the module boundary has also stopped carrying useful responsibility, which is a different finding, and acting on it is far less reversible. Jumping straight there from a coupled pair is how a merge becomes the six-month project someone eventually writes a cautionary blog post about.
Taking neither, on purpose
There is a third thing teams do, and it needs separating from the two exits because it is not one. It is the decision to take neither exit yet. A boundary can be one you would never draw today and still one you keep for now, because the migration costs more than another year of coupled releases.
That counts as a decision only if somebody wrote it down and attached an expiry. We are accepting a Release Coupling Ratio of 0.6 on this pair, which is above the line where team and code boundaries have visibly diverged, and we know it. We are not fixing it this year. We look again in March, and if the ratio has climbed or the tail has worsened, we take one of the exits.
Drift and a deliberate hold produce identical code. They produce completely different postmortems. And the expiry is not special to holding. Every outcome here gets one, which is the subject of the last section.
When Merging Is the Wrong Answer
I have argued for a direction the industry underuses. Here is where the argument breaks.
Module boundaries are weaker than network boundaries. This is the strongest objection to everything above, and it deserves a direct answer instead of a footnote. A network call is a crude enforcement mechanism, but a developer three weeks from a deadline cannot casually bypass it. An import rule they can. Plenty of teams split precisely because their module boundaries had already rotted. Consolidating puts those modules back within reach of the same pressure that rotted them the first time.
The answer is that enforcement has to be mechanical rather than cultural, and the tooling exists in most ecosystems. Shopify built Packwerk for Ruby, a static check in CI that fails the build when one package reaches past another’s public surface. Java has ArchUnit and the module system, TypeScript has boundary linting, Go has internal packages, Rust has crate visibility, .NET has assembly-level access rules. Pick whichever one your build already runs and make the violation a failing check rather than a review comment.
Then read Shopify’s own retrospective before treating it as solved. Adoption inside their own monolith stayed partial, and driving a single package to zero violations required enough indirection that the code got harder to read. Enforcement is available and it is not free. If you are not going to fund it, this objection stands and you should keep the network.
Data is often where consolidation gets expensive. Separate schemas with separate migration histories and different consistency requirements make this a data migration project, and the code merge is the easy half. A shared database removes one migration step, and that should be read as a diagnosis rather than as good news, because it means the data boundary eroded some time ago. The post that tells you to merge without mentioning the database is selling you something.
Scaling profiles do not average. A CPU-bound service and a memory-bound one in a single process scale on the worse dimension. If one has a much steeper scaling curve, merging forces both to scale as a unit, and every replica added for the hot component carries the footprint of the quiet one. That alone can justify keeping a boundary that fails every other test.
Compliance and blast radius. Data residency, PCI or PHI scope, tenant isolation, or credential separation can make a boundary mandatory regardless of how chatty it is. A merged service drops the edge between A and B but inherits the union of their external dependencies, so check that the result is not simply a new hub.
Availability requirements can differ legitimately. Merging fuses the fate of a component that must survive a partial outage with one that can be down for an hour unnoticed.
And the regressions in build time, test duration, cold start, and rollback size are never dealbreakers and always surprises. Measure all four before the merge so you have a baseline to be annoyed at later.
When Segment consolidated more than a hundred per-destination services in 2018, they named what it cost them, which is why their account is worth more than most write-ups. Consolidating cost them fault isolation, made in-memory caching less effective, and turned dependency upgrades into a change with a much wider blast radius. None of that surfaced later as a surprise. It was the price of the trade, and they said so up front.
Make the Question Routine
Neither exit gets taken and the hold never gets written down if nobody is asked, and that is the actual failure here. The failure is organizational. A team can know its Release Coupling Ratio is 0.7 and agree that the boundary is not earning. Two years later they have still done nothing, because doing something requires a forum that does not exist.
You have seen this before under another name, and I have no argument that a Consolidation Review is structurally different from an architecture review board. What I can say is where those usually fail. They produce opinions instead of numbers. They own decisions instead of surfacing them for the teams who will do the work. And they have no defined behavior for the quarter when nobody shows up. Everything below is aimed at those three failures, and if your version does not address them it will die the same way.
What puts a boundary on the agenda. Two paths, and you want both. Anything breaching a defined trigger is auto-referred: Chatty Threshold exceeded, Release Coupling Ratio above your line, a cross-context database read, a change that required approval from two other teams. Whatever the triggers do not catch, ranking does. Sort the remaining boundaries by weighted edges, traffic and change frequency and data sensitivity, and take the top three. Triggers keep the review honest. Ranking keeps it finite.
Thirty minutes, once a quarter. Five minutes on the ledger and on which triggers fired. Fifteen on three boundaries, five questions each. Five to attach sunsets and defaults. Five to look at what expired since last time.
The five questions.
- What is this boundary buying right now? Independent deployability, failure isolation, independent scaling, clearer responsibility. If the answer takes more than a sentence, that is data.
- Does the telemetry agree? Release Coupling Ratio, Sync Hop, Dependency Degree, incidents that cross. Claimed and observed benefits diverge more than anyone expects.
- Would we draw this boundary today?
- What does keeping it cost for another quarter?
- What would it cost to undo?
The output is one row per boundary. Something like this, which is short enough that nobody has to prepare a deck for it:
| Boundary | Sold as | RCR | Sync Hop | Draw it today? | Decision | Sunset | Default if nobody meets |
|---|---|---|---|---|---|---|---|
| Checkout ↔ Pricing | Independent deployability | 0.58 | 4 | No | Hold | March | Escalate to exit one |
| Catalog ↔ Search | Independent scaling | 0.42 | 3 | Yes, narrower | Exit one: add CDC tests + schema gates | 6 months | Re-open at next review |
| Orders ↔ Fulfillment | Failure isolation | 0.10 | 2 | Yes | Keep | 12 months | Keep |
Creating a boundary is expensive and removing one is expensive too, and the asymmetry between them is as much psychological as technical. We underprice the ongoing tax of a new boundary because it arrives in installments nobody signs for, then overprice the reversal once undoing it feels like a loss. The last three questions exist to counter that.
Splitting them matters. “Would we draw it today” removes sunk cost, which is correct, since the six months spent on the split is gone regardless of what you decide next. The other two are future costs, and neither is sunk. Keeping the boundary costs the operational tax plus the cost of delay on everything the coordination slows down.
Question 5 is the one teams hand-wave, so price it in five parts and accept rough numbers. Data migration, usually the largest and the one people forget. Contract and client removal, including whatever else consumes those endpoints. CI and runtime regression, which is the build time, test duration, cold start, and rollback size listed earlier. On-call consolidation, including the runbooks that get merged or deleted. And a risk premium for what the migration breaks, which for a reversible position-3 move is small and for a position-4 move is not. A five-line estimate that everyone agrees is approximate beats a confident number nobody produced.
Which is how a hold gets chosen honestly rather than backed into. The migration can genuinely cost more than another quarter of friction. That conclusion is defensible when it comes with the numbers and a date attached, and indefensible when it arrives by nobody bringing it up.
The output does not need to be an architecture project. Keep it, measure it, or schedule the work. Most boundaries land in the first bucket, which is fine, because now they are there on purpose.
Whatever the outcome, attach a sunset to it. Three months, six, or twelve, depending on how quickly the metrics could plausibly change. Release Coupling Ratio, P95 and P99, change failure rate, and time to recover should improve before that date, or the decision comes back to the same table. A review that only produces decisions is half a loop.
That applies to consolidation as well, and it is the part people skip because a merge feels like an ending. The merged pair is also a boundary now, an in-process one, and it owes rent in the same way. Give it the same expiry and the same four metrics, plus the module violation count. If the modules have fused into one thing by the time the sunset arrives, you have learned something about the domain that the split never told you.
Then write down what happens if the review does not happen, because eventually it will not. Somebody leaves, a quarter gets eaten by an incident, the calendar invite quietly dies. A decision with a defined default expires into that default instead of into permanence, and the difference between those two is the whole mechanism.
Without something like this, every boundary becomes permanent by default, and default permanence is not a decision anyone made.
Split When the Boundary Buys Something
Microservices work because boundaries work. A service boundary should keep paying its rent, and independent deployability, failure isolation, scaling flexibility, and clearer responsibility are how it pays.
When those benefits stop arriving, the bill does not stop with them. The network hops remain. The distributed failure modes remain. The coordination overhead very much remains. The semantic contracts remain too, and that one is deliberate. Consolidation removes topology that stopped earning while leaving a domain boundary that still means something.
We have gotten very good at taking systems apart. The other skill starts with something much smaller than a merge: a recurring meeting where “should this still be two things” is an ordinary question rather than a career risk.
The Consolidation Review, the Decision Sunset that keeps it from expiring into permanence, and the economics of hub risk behind the ranking step are developed in Chapter 3, “The Illusion of Small Services,” in Beyond Microservices: Building Systems That Align Architecture, Teams, and Flow.

Leave a Reply