Distribution Boundaries
Purpose
Make distribution a decision with a stated driver and a stated price, rather than a default
architecture. A process boundary is not a stronger version of a module boundary; it is a
different kind of thing, with different failure semantics. Remote calls do not inherit a
local transaction or a shared release lifecycle; transport failure can leave their outcome
unknown. Local calls can also block or fail after a mutation.
The first law here is old and still correct: do not distribute your objects. Distribute
when a concrete driver justifies it, and design the boundary to be worth its cost. The law
warns against transparent remote objects, not against every service boundary.
What crossing a process boundary actually costs
| Property |
In-process |
Across a process boundary |
| Call cost |
dispatch plus actual work |
transport, serialisation, queueing and work; measure tails |
| Failure modes |
exception, blocking, partial mutation |
also transport ambiguity, independent failures and retry duplicates |
| Atomicity |
only within an actual transaction |
requires explicit transaction participation or application recovery |
| Refactoring |
often one release |
compatibility across independently deployed versions |
| Debugging |
local evidence, possibly asynchronous |
correlated evidence across systems, if instrumented |
| Types |
shared |
a wire contract with independent lifecycles |
| Coupling |
static and runtime |
contract, data, temporal and operational dependencies |
| Potential gain |
— |
independent deploy/scaling, isolation, autonomy, technology or regulatory fit |
The gains require design and operational evidence; a separate process alone guarantees none.
Workflow
- Name the driver. Independent deployment, independent scaling, fault isolation, team
ownership, or a technology/regulatory constraint. "Microservices" is not a driver, and
neither is anticipated scale nobody has measured.
- Prefer an in-process rehearsal when feasible. Inspect dependency and change history;
moving tangled code over HTTP preserves its coupling. A regulatory or technology
constraint can justify direct extraction with explicit migration risks
(
layering-and-boundaries).
- Draw the data ownership line. Name the authority for each invariant and write path,
including replicas and migration writers. Shared storage is not automatically shared
ownership; direct access to private tables creates schema and deployment coupling.
- Coarsen the interface. A remote operation should be a complete business request, not
a getter. Design it as a Remote Facade over the local model
(
remote-facade-and-dto).
- Decide the consistency story explicitly. What is atomic, what is eventual, what is
the visible intermediate state, and what compensates a partial failure.
- Bound call duration and capacity. Define a deadline, retry policy (including no retry),
and safe failure behaviour. Some operations must fail closed. Repeated writes need
idempotency or reconciliation of an unknown outcome (
timeouts-and-deadlines,
retries-and-backoff, idempotency).
- Verify the intended gain. Persistent lockstep releases undermine independent
deployment; they do not disprove scaling or isolation benefits. Test mixed versions,
dependency outages and the proposed migration/rollback path.
With missing workload, dependency or ownership evidence, keep extraction conditional and
identify the next measurement. Return a short decision: driver and evidence, local alternative,
chosen interaction and consistency contract, failure behaviour, and validation/rollback criteria.
Decision rules
The two sides change together in most commits
→ inspect why. Accidental coupling favours keeping local or
redesigning; a cross-cutting feature alone does not invalidate a boundary.
The driver is independent deployability, and the module has a stable,
narrow, business-shaped interface
→ a candidate. Extract as a module first, run it that way, then
separate the process when the interface has stopped churning.
The driver is scaling one part independently
→ measure CPU, memory, I/O, bottlenecks and load curves. Low CPU
alone does not reject extraction; a GPU or large heap requirement
is a candidate driver, not proof of a net benefit.
The driver is fault isolation
→ valid, and often the strongest one — but only if the caller has
a defined behaviour when the callee is down. Isolation without a
capacity boundary and safe failure behaviour adds another failure mode.
The driver is team autonomy
→ measure release coordination cost and ownership friction
against the new operational and contract maintenance costs.
Two candidate services would share a database table
→ inspect write authority and schema coupling. Shared private
tables need an ownership/migration plan; shared infrastructure
or a supported read contract is a different trade-off.
The operation requires atomicity across both sides
→ prefer a single transaction boundary. If distribution is necessary,
verify supported atomic-commit protocols and their recovery costs,
or obtain an explicit business contract allowing saga/outbox recovery.
A synchronous chain would be three or more hops deep
→ sequential stage latencies add; required dependencies can reduce
availability. Measure the critical path before collapsing hops
or changing the completion contract to events.
Rules
- Design remote granularity around caller use cases. If measured latency is dominated
by round trips, coarsening or batching is a candidate; measure payload/processing costs
before attributing every slow remote call to chattiness (
architecture-and-performance).
- If all four services are required and their success events are independent, each at
99.9%, combined availability is
0.999^4 = 99.6006%: about 2.876 hours unavailable
in a 30-day month. Correlations, retries and the success definition change that model;
it is not a measured outage forecast. Caches and messaging change dependencies and
freshness/completion guarantees rather than making dependencies disappear.
- A local transaction does not automatically include a remote effect. Design the
failure gap between a local commit and a remote operation. Sagas and outboxes do not
provide cross-service ACID atomicity; explicit distributed transaction protocols can,
with participant and availability constraints (
distributed-transactions-and-sagas).
- Access to another service's private tables couples schema changes and operations.
Evaluate supported views, replication or APIs against consistency and ownership needs.
- Chattiness and coupling trade off. A coarse operation that returns everything the caller
might need transfers data nobody uses; a fine one requires many round trips. Resolve it
from the caller's actual use cases, not by symmetry with the domain model.
- A boundary intended for independent deployment needs compatible evolution. If an
optional field requires lockstep releases, inspect tolerant readers and contract tests;
a change to required business semantics may need a staged migration
(
rpc-and-api-contracts).
- Prefer asynchronous messaging where the caller does not need the answer to proceed. It
decouples producer progress from immediate consumer availability, subject to durable
acceptance, queue capacity, retention and recovery. Publication still needs time bounds;
define completion deadlines, backlog limits and visible intermediate states
(
delivery-semantics).
- Do not extract a service to fix a code quality problem. A tangled module becomes a
tangled module you cannot refactor with an IDE.
- Distribution is close to irreversible in practice. Merging two services back is a
migration, not a refactor, so this decision deserves the analysis that one-way decisions
get (
architecture-decision-making).
References
- Local versus remote boundaries — the concrete arithmetic
of a chatty interface, the failure modes a local call does not have, why an in-process
module is the right rehearsal for a service, the distributed monolith's detectable
symptoms, and how to run an extraction so it can be abandoned halfway. Read before
proposing or reviewing an extraction.
- Distribution strategies — synchronous request,
asynchronous messaging, event-carried state transfer and replication compared on
coupling, consistency and failure; sagas and compensation; the outbox; fan-out and its
latency; and choosing per interaction rather than per system. Read when designing the
interaction between two services.
1---2name: distribution-boundaries3description: Deciding whether a boundary should be a process boundary, and designing it when it must be: what distribution actually costs (latency, serialisation, partial failure, lost atomicity, independent deployment), why a remote interface must be coarser than a local one, and choosing between synchronous call, messaging and replication. Use when a module is proposed for extraction into a service, when microservices are being adopted without a named driver, when a service call sits inside a transaction, when one request fans out to a dozen downstream calls, when two services share a database, when a "service" cannot be deployed without another being deployed too, when a synchronous chain has three or more hops, or when a distributed transaction is being designed. Does not cover the remote API's shape and payload types (remote-facade-and-dto), contract compatibility and versioning (rpc-and-api-contracts), transaction mechanics on one database (enterprise-transactions), or in-process layering (layering-and-boundaries).4---56# Distribution Boundaries78## Purpose910Make distribution a decision with a stated driver and a stated price, rather than a default11architecture. A process boundary is not a stronger version of a module boundary; it is a12different kind of thing, with different failure semantics. Remote calls do not inherit a13local transaction or a shared release lifecycle; transport failure can leave their outcome14unknown. Local calls can also block or fail after a mutation.1516The first law here is old and still correct: **do not distribute your objects.** Distribute17when a concrete driver justifies it, and design the boundary to be worth its cost. The law18warns against transparent remote objects, not against every service boundary.1920## What crossing a process boundary actually costs2122| Property | In-process | Across a process boundary |23| -------------- | ------------------------------------- | ----------------------------------------------------------------------------- |24| Call cost | dispatch plus actual work | transport, serialisation, queueing and work; measure tails |25| Failure modes | exception, blocking, partial mutation | also transport ambiguity, independent failures and retry duplicates |26| Atomicity | only within an actual transaction | requires explicit transaction participation or application recovery |27| Refactoring | often one release | compatibility across independently deployed versions |28| Debugging | local evidence, possibly asynchronous | correlated evidence across systems, if instrumented |29| Types | shared | a wire contract with independent lifecycles |30| Coupling | static and runtime | contract, data, temporal and operational dependencies |31| Potential gain | — | independent deploy/scaling, isolation, autonomy, technology or regulatory fit |3233The gains require design and operational evidence; a separate process alone guarantees none.3435## Workflow36371. **Name the driver.** Independent deployment, independent scaling, fault isolation, team38 ownership, or a technology/regulatory constraint. "Microservices" is not a driver, and39 neither is anticipated scale nobody has measured.402. **Prefer an in-process rehearsal** when feasible. Inspect dependency and change history;41 moving tangled code over HTTP preserves its coupling. A regulatory or technology42 constraint can justify direct extraction with explicit migration risks43 (`layering-and-boundaries`).443. **Draw the data ownership line.** Name the authority for each invariant and write path,45 including replicas and migration writers. Shared storage is not automatically shared46 ownership; direct access to private tables creates schema and deployment coupling.474. **Coarsen the interface.** A remote operation should be a complete business request, not48 a getter. Design it as a Remote Facade over the local model49 (`remote-facade-and-dto`).505. **Decide the consistency story explicitly.** What is atomic, what is eventual, what is51 the visible intermediate state, and what compensates a partial failure.526. **Bound call duration and capacity.** Define a deadline, retry policy (including no retry),53 and safe failure behaviour. Some operations must fail closed. Repeated writes need54 idempotency or reconciliation of an unknown outcome (`timeouts-and-deadlines`,55 `retries-and-backoff`, `idempotency`).567. **Verify the intended gain.** Persistent lockstep releases undermine independent57 deployment; they do not disprove scaling or isolation benefits. Test mixed versions,58 dependency outages and the proposed migration/rollback path.5960With missing workload, dependency or ownership evidence, keep extraction conditional and61identify the next measurement. Return a short decision: driver and evidence, local alternative,62chosen interaction and consistency contract, failure behaviour, and validation/rollback criteria.6364## Decision rules6566```text67The two sides change together in most commits68 → inspect why. Accidental coupling favours keeping local or69 redesigning; a cross-cutting feature alone does not invalidate a boundary.7071The driver is independent deployability, and the module has a stable,72narrow, business-shaped interface73 → a candidate. Extract as a module first, run it that way, then74 separate the process when the interface has stopped churning.7576The driver is scaling one part independently77 → measure CPU, memory, I/O, bottlenecks and load curves. Low CPU78 alone does not reject extraction; a GPU or large heap requirement79 is a candidate driver, not proof of a net benefit.8081The driver is fault isolation82 → valid, and often the strongest one — but only if the caller has83 a defined behaviour when the callee is down. Isolation without a84 capacity boundary and safe failure behaviour adds another failure mode.8586The driver is team autonomy87 → measure release coordination cost and ownership friction88 against the new operational and contract maintenance costs.8990Two candidate services would share a database table91 → inspect write authority and schema coupling. Shared private92 tables need an ownership/migration plan; shared infrastructure93 or a supported read contract is a different trade-off.9495The operation requires atomicity across both sides96 → prefer a single transaction boundary. If distribution is necessary,97 verify supported atomic-commit protocols and their recovery costs,98 or obtain an explicit business contract allowing saga/outbox recovery.99100A synchronous chain would be three or more hops deep101 → sequential stage latencies add; required dependencies can reduce102 availability. Measure the critical path before collapsing hops103 or changing the completion contract to events.104```105106## Rules107108- **Design remote granularity around caller use cases.** If measured latency is dominated109 by round trips, coarsening or batching is a candidate; measure payload/processing costs110 before attributing every slow remote call to chattiness (`architecture-and-performance`).111- If all four services are required and their success events are independent, each at112 99.9%, combined availability is `0.999^4 = 99.6006%`: about 2.876 hours unavailable113 in a 30-day month. Correlations, retries and the success definition change that model;114 it is not a measured outage forecast. Caches and messaging change dependencies and115 freshness/completion guarantees rather than making dependencies disappear.116- **A local transaction does not automatically include a remote effect.** Design the117 failure gap between a local commit and a remote operation. Sagas and outboxes do not118 provide cross-service ACID atomicity; explicit distributed transaction protocols can,119 with participant and availability constraints (`distributed-transactions-and-sagas`).120- Access to another service's private tables couples schema changes and operations.121 Evaluate supported views, replication or APIs against consistency and ownership needs.122- Chattiness and coupling trade off. A coarse operation that returns everything the caller123 might need transfers data nobody uses; a fine one requires many round trips. Resolve it124 from the caller's actual use cases, not by symmetry with the domain model.125- A boundary intended for independent deployment needs compatible evolution. If an126 optional field requires lockstep releases, inspect tolerant readers and contract tests;127 a change to required business semantics may need a staged migration128 (`rpc-and-api-contracts`).129- Prefer asynchronous messaging where the caller does not need the answer to proceed. It130 decouples producer progress from immediate consumer availability, subject to durable131 acceptance, queue capacity, retention and recovery. Publication still needs time bounds;132 define completion deadlines, backlog limits and visible intermediate states133 (`delivery-semantics`).134- Do not extract a service to fix a code quality problem. A tangled module becomes a135 tangled module you cannot refactor with an IDE.136- Distribution is close to irreversible in practice. Merging two services back is a137 migration, not a refactor, so this decision deserves the analysis that one-way decisions138 get (`architecture-decision-making`).139140## References141142- [Local versus remote boundaries](references/local-vs-remote.md) — the concrete arithmetic143 of a chatty interface, the failure modes a local call does not have, why an in-process144 module is the right rehearsal for a service, the distributed monolith's detectable145 symptoms, and how to run an extraction so it can be abandoned halfway. Read before146 proposing or reviewing an extraction.147- [Distribution strategies](references/distribution-strategies.md) — synchronous request,148 asynchronous messaging, event-carried state transfer and replication compared on149 coupling, consistency and failure; sagas and compensation; the outbox; fan-out and its150 latency; and choosing per interaction rather than per system. Read when designing the151 interaction between two services.