Index

A Module Is Cheaper Than a Network Boundary

The payment was captured. Ordering does not know that.

Its request reached Payments, the capture became durable, and the response disappeared on the way back. Ordering now has a pending order and a timeout. Retrying without a stable identity may charge twice. Treating the timeout as a decline may abandon a real capture.

Inside one process and transaction, that particular ambiguity does not exist. The payment and order commit together or roll back together. Moving Payments across a network has bought a separate release and failure boundary. It has also replaced a return value with a protocol for identity, uncertainty, and repair.

A cohesive boundary should remain a module while its data, transaction, and failure contract are still moving. Extract it when independent operation has a named owner and a concrete benefit which pays for the new protocol.

Compare the same behavior in both forms

The previous article, Decompose by Change, Not by Layer, found Payments as one cohesive boundary in a synthetic commerce application. It did not approve a service. It left one question open: does Payments earn an independent operational lifecycle?

The SVC-02 reproduction carries the same synthetic decision into two forms. Both accept an order, request one capture for capture:ORDER-1042, distinguish a business decline from infrastructure failure, and refuse to capture the same operation twice.

The modular form has:

commerce application
├── Ordering module
├── Payments module
└── commerce database
    ├── Ordering-owned tables
    └── Payments-owned tables

Payments owns its writes and exposes a module API. Ordering cannot reach into its tables. One application artifact and one database transaction still cover the complete operation.

The service form has separate Ordering and Payments processes, artifacts, pipelines, data stores, and on-call ownership. Ordering retains its workflow state and payment reference. Payments alone writes payment state through a versioned remote contract.

That is a fairer control than comparing a service with an undisciplined monolith. Fowler’s Microservice Trade-Offs notes that firm module boundaries are possible in a monolith, although maintaining them requires discipline. A network can enforce a boundary. It cannot tell us whether the boundary was placed well.

Local atomicity removes one class of repair

Consider an Ordering database failure after the payment decision.

In the modular form, the order and capture share one local transaction. Both roll back:

order   = rolled-back
payment = rolled-back

In the service form, Payments has already committed to its own store. The Ordering transaction cannot reach across the network and undo it:

order   = commit-failed
payment = captured-once

That second state is not wrong merely because it is temporarily inconsistent. It is wrong if nobody owns convergence. The fixture assigns a joint Ordering and Payments signal for a captured operation without an accepted order, then requires a decision to reconcile the order or compensate the capture.

Microsoft’s data guidance recommends durable work state for operations which span services and warns that continually chatty services may have the wrong boundary. “Eventual” describes when consistency may arrive. It does not provide the identity, retry, compensation, deadline, or operator which makes arrival likely.

The modular transaction is cheaper here. That does not make the module better for every concern. It identifies one cost which the service’s independence must repay.

Synthetic latency reveals a path, not a benchmark

The fixture declares five deterministic inputs:

ComponentSynthetic input
Ordering work4 ms
Payment work7 ms
Request transit8 ms
Response transit8 ms
Reconciliation work11 ms

An accepted modular capture uses the first two and totals 11 synthetic milliseconds. The service path adds both network transits and totals 27.

Those numbers are not a production percentile, forecast, or universal network penalty. They make the arithmetic inspectable. Change any component and the result follows. The stable observation is the path: a remote request adds transit, serialization, timeout, and dependency capacity to work which an in-process call does not have.

Microsoft’s partial-failure guidance treats slow and unavailable dependencies as ordinary distributed-system conditions. Fowler states the same cost more bluntly: remote calls are slower and can fail. A service decision which measures only its happy handler has excluded the part introduced by distribution.

A timeout changes what the caller can know

The ten-scenario comparison retains the durable state behind each visible result. Three failure cases expose the difference:

ScenarioModular stateService stateRequired service repair
Payment unavailable before workorder not accepted; payment not startedorder not accepted; payment not startedNone; the failure is definite
Response lost after captureorder accepted; payment captured onceorder pending; payment captured onceLook up by operation identity, then complete the order
Ordering commit fails after captureorder and payment rolled backorder commit failed; payment captured onceReconcile or compensate the capture

The first service failure is ordinary refusal at an infrastructure boundary. No remote effect began, so retry policy can work from a known state.

The other two are epistemic failures: the caller’s answer is missing while the callee’s state exists. Stable operation identity prevents a retry from creating another capture. A status lookup or durable reconciliation turns “unknown” into a state transition. Without those pieces, a timeout policy only decides how quickly the caller stops waiting.

The module still needs idempotency at external ingress and careful transaction handling. It does not manufacture uncertainty between its Ordering and Payments modules.

Independent deployment depends on compatibility

A compatible Payments implementation change shows the service’s clearest benefit. The Payments artifact can pass its contract checks and deploy through its own pipeline. The modular version must release the complete commerce artifact even when only Payments changed.

A breaking contract tells a different story. The service fixture requires three stages:

  1. expand Payments to accept the old and new contracts;
  2. migrate Ordering and prove it uses the new contract; and
  3. retire the old contract after that evidence exists.

That release is coordinated. Separate repositories, pipelines, and deployment buttons do not make it independent.

Microsoft’s boundary guidance says services should not have interdependencies which force them to deploy together. It also recommends coarser boundaries when uncertain and allows consistency requirements to keep related behavior together. Independent deployment is an observed property of compatible changes, not a label granted when a process gets its own pipeline.

The service contains a process, not the whole workflow

The Payments process fault produces one genuine isolation benefit. In the modular form, the synthetic fault takes down the shared commerce process. Both payment-dependent acceptance and unrelated Ordering work are unavailable.

In the service form, unrelated Ordering work remains available because its process is healthy. Payment-dependent order acceptance is still unavailable. The service contains the blast radius; it does not make its callers independent of the capability they need.

This distinction changes the operational question. “Does Payments fail independently?” is incomplete. Ask which work survives, which requests stop, which queue or pending state grows, who receives the alert, and what recovery does to already durable operations.

Process isolation earns its machinery when that surviving work matters enough to operate separately. If every important request still needs Payments, the extra process has changed failure shape more than availability.

A moving boundary is cheaper inside the application

The fixture deliberately corrects the first cut. Capture eligibility moves from Payments to Ordering while capture execution stays in Payments.

In the modular form, the correction changes three internal files and ships in one deployment. It changes no public contract and needs no temporary compatibility layer.

In the service form, the same correction changes seven files, two public contract versions, a deployment definition, and three release stages. Payments must temporarily understand both shapes while Ordering migrates.

This is the practical force behind Fowler’s Monolith First argument: stable service boundaries are hard to find at the beginning, and moving responsibility between services costs more than moving it inside one application. Fowler also calls the advice tentative and notes the countercase: a poorly defended monolith can dissolve its modules before learning anything useful.

The answer is not “monolith first” as ritual. It is to keep the cheapest honest boundary while the model is changing. An internal module needs an API, owned data, dependency rules, and tests strong enough to stop casual shortcuts. If those controls cannot survive one process, distribution may enforce separation at a much higher price.

Make the independence pay rent

The synthetic reproduction retains ten scenarios in both forms, 20 outcomes, 36 passing tests, five primary-source responses, and 18 isolated counterfactuals. The counterfactuals try to weaken the modular control, share the service database, erase network latency, call a timeout a failure, roll back a remote capture, duplicate a retry, hide repair or compatibility work, overstate availability, and manufacture an architecture score. All eighteen fail.

The clean export reproduces from commit bf551f84b208b72e880ba3010027a3fdf655a5ed. It proves consequences of the declared synthetic model. It does not measure Brian’s systems, teams, availability, latency, cost, or extraction history.

Keep the boundary modular while independent release is occasional, failures share one owner, and boundary corrections are still common. Reconsider when payment-only releases are frequent, compliance requires separate data and process control, resource isolation protects valuable work, or a separately staffed operational lifecycle already exists.

Before extracting the next module, finish one sentence: “This service needs to operate independently because …” Then attach a release, failure, data, scaling, compliance, or staffing observation. If the sentence has no evidence after “because,” the module is still doing useful work.