Five Services, One Release Train: The Worst of Both Worlds
The retrospective opens with a slide: "Order API v2.14 deploys require Billing 3.9, Customers 2.6, and Inventory 4.1 to be upgraded first; the coordinated release window is Thursday 02:00 UTC." Six processes, three Kubernetes clusters, one deploy train. Everyone waits for the same window, one failure rolls back all five, and the on-call channel has a name for the window: "release roulette." This is a distributed monolith — separate processes that still change, release, and fail as one unit — and it is the worst possible outcome: you pay for distribution and keep all the coordination.
The definition has teeth: a distributed monolith requires coordinated releases despite running as separate processes. Coordination takes four forms, and you can audit for each:
| Form | Concrete symptom | Root coupling |
|---|---|---|
| Release coordination | Shared deploy windows; one service's rollout blocks others' | Contracts negotiated by humans, not versioned schemas |
| Library lockstep | Every service must bump common-lib to the same version together |
Shared code with a single evolving model |
| Temporal coupling | Callers cannot deploy before callees; chain must be up for every request | Synchronous calls with non-versioned payloads |
| Data coupling | Foreign tables/FKs (from the previous topic) | Shared storage |
Notice which forms the org chart hides. Each team believes it owns its service end to end; the release calendar tells another story. The cheapest audit is data you already have: pull the last quarter of deployments and measure release correlation — of the times service A deployed within an hour of service B, what fraction were the same change (linked PRs, same release ticket)? Below 10%: independent ships. Above 40%: you built a distributed monolith with extra YAML. The second metric is simpler: how many services appear in a typical rollback? One is the point of the architecture; five means a failure's blast radius was never actually isolated. A third check costs nothing: ask two teams who owns the Thursday window. If the answer is "we all do," the calendar already knows what the metrics will confirm.
Why this matters beyond embarrassment: the resilience promise is inverted. In a monolith, a slow dependency is a stack frame you can degrade locally; in a properly split system, one service's failure stays in its blast radius. In a distributed monolith, a slow member still pins the synchronous chain (as in Inter-service Communication), and its fix requires the Thursday window — so the incident is both wider and slower to repair. Teams in this state often respond by adding more services, which deepens the train. The correct reading is that the boundaries were never real: the split drew deployment boxes around code that still shares one model, one library, and one release reality.
Interview framing: this is a favorite advanced question — "what is a distributed monolith and how would you know if you had built one?" Answer with the definition, two of the four forms, and a measurement (release correlation, rollback blast radius). Then signal the next move: the two most common causes — shared libraries and synchronous temporal coupling — each have concrete escapes, which is where the next two chapters go.
The Shared Library That Ate Your Autonomy: Lockstep Versions and Their Escapes
common-model is 40,000 lines and every service depends on it. It holds Customer, OrderStatus, the money type, the event base classes, and — inevitably — CustomerView, which gained a loyaltyTier field eighteen months ago that exactly two services use. Adding a field means bumping the major version, which means every service must adopt the new version in the same release cycle or compile against a version with a different Customer definition. The library, built to reduce duplication, has become the mechanism that forces joint upgrades: shared libraries versioned in lockstep across services force joint releases, and the jar is where your autonomy went to die.
The deep problem is not the tooling, it is that a shared library distributes code where the system needed contracts. Two services linking the same class file still evolved at different paces, so the class became a negotiation table. Escapes, ranked by how much model they let you share:
| Escape | Mechanism | Trade-off |
|---|---|---|
| Share the contract, not the code | Publish schemas (Protobuf/Avro/JSON Schema) or IDL; each service generates its own types | Types are per-service copies — small drift handled by compatibility rules, no lockstep |
| Sanctioned duplication | Copy the 30-line value object with a comment citing origin | Two implementations to fix; explicit, cheap, greppable |
| Versioned internal package | Semver with a compatibility window: v1 supported for N months, consumers upgrade independently | Requires a real deprecation policy and someone to enforce it |
| Shared kernel (DDD rules) | Jointly owned, tiny subset with change protocol — one team, dual sign-off | Dies the moment a second team joins — you built it already |
The first escape is the default answer for events and APIs: the wire contract is the boundary, and code generation means each service's OrderPlaced type is its own, compiled against its version of the schema. Backward-compatible evolution (add optional fields, never change tags) means old and new coexist — which is precisely what a shared jar cannot do, because one class definition cannot be two versions at once in one JVM dependency graph.
The second escape — duplication — makes purists flinch and is often right. Thirty lines of money arithmetic duplicated across four services costs 120 lines and four places to fix a rounding bug. The alternative (the shared library) costs a coordinated release every time anything in it changes. Duplication is a local cost; lockstep is a systemic one. The discipline is making the duplication explicit: a header comment with origin and a link to the owning module's source, so future readers know it was a decision, not an accident. When the copy changes three times, that is your evidence-based trigger to promote it to a versioned package — not before.
How to dig out of an existing mess: inventory who imports what in common-model, split it by change frequency (the money type is stable; the CustomerView DTO is not), publish the volatile parts as schemas, freeze the stable parts, and put a deprecation clock on the rest. Measure progress the same way as the previous chapter: count joint upgrades per quarter — it should be falling. A library is allowed to be shared; what it is not allowed to do is make service A's deploy contingent on service B's calendar, because that is the lockstep failure wearing a pom.xml.
Proving the Split Worked: Removing Temporal Coupling and Measuring Independence
Diagnosis without cure is just a better incident report. Escaping the distributed monolith means attacking the two couplings that survive reorganizations — temporal and release — and then proving with data that independence is real.
Temporal coupling first, because it is what users feel. A caller that needs loyalty-service up right now for every request has synchronized their fates whether or not anyone chose to. The redesign follows the answer question:
Does the caller need the answer in this request?
yes, and the member is critical → sync with tight deadline + honest failure;
accept the joint fate explicitly, in writing
yes, but degraded is fine → sync behind a circuit breaker with a
pre-agreed degraded answer
no → replace the call with a published event;
caller ships without callee's deploy
Each "no" converts a temporal edge into a contract edge: the producer publishes OrderPlaced and deploys whenever it likes; the consumer catches up on its own schedule. The price — eventual consistency and a delivery contract — was analyzed in the communication topic; the gain is that release ordering constraints disappear from that edge entirely.
Release coupling second. Joint releases persist after async conversion when contracts are negotiated by humans ("we changed the payload, so coordinate the deploy"). Kill that with mechanical compatibility: additive-only schemas, a registry, and consumer-driven contract tests in CI — the producer runs its consumers' expectations on every PR, so "will their code break?" is answered by a red build, not by a Thursday negotiation. When contract tests pass, deploys need no ordering at all, and the release window becomes an ordinary time of day.
Then measure, because autonomy claimed is not autonomy delivered:
| Metric | Distributed monolith | Healthy split |
|---|---|---|
| Deploy correlation (same-window deploys) | >40% of pairs | <10% |
| Services touched per rollback | 3–5 | 1 (plus downstream consumers degrading) |
| Median joint upgrades of shared libs per quarter | many | zero or policy-bounded |
| Lead time for a one-team change | days (window waits) | hours (merge → prod) |
Any one metric can mislead (a quiet quarter looks independent), so read them together — and be honest about the metric that may not move: if the underlying work is genuinely co-changing, no amount of tooling will make the teams independent, because they built one cohesive unit. At that point the mature answer is merging services back — the same data-driven reversal from When Microservices Make Sense. Admitting that a split should be undone is a stronger architectural position than defending boxes on a diagram.
The interview-ready summary: distributed monolith = coordinated releases across separate processes, detected by release correlation and rollback blast radius; the two usual causes are lockstep shared libraries (fix: share contracts, not code) and synchronous temporal coupling (fix: event edges where no answer is needed, contract tests where there is); and the proof is a dashboard showing lead time falling while deploy correlation falls with it. That answer demonstrates you have run the loop — measure, cut, re-measure — instead of merely naming the anti-pattern.
Quiz
1. Which combination is the definition of a distributed monolith?
2. `common-model` v2 forces every service to upgrade in the same release cycle because the `Customer` class changed. What is the primary fix?
3. A caller synchronously needs `loyalty-service` on every checkout but can show orders without discounts if it is down. What does the decoupling decision table say?
4. How do you prove — rather than claim — that service independence was achieved?
Flashcards
Question
What is a distributed monolith, in one sentence, and name its four coordination forms.
Click to reveal answer
Answer
Separate processes that still change, release, and fail as one unit. Forms: release coordination (shared deploy windows), library lockstep (common-lib bumped together), temporal coupling (sync chains requiring callee up and pre-deployed), and data coupling (shared tables/FKs).
Question
How do you measure whether your system is a distributed monolith?
Click to reveal answer
Answer
Release correlation: fraction of same-window/same-change deploys between service pairs (>40% = monolith; <10% = independent). Rollback blast radius: services in a typical rollback (1 = healthy, 3–5 = coupled). Plus quarterly joint-upgrade count for shared libs and median lead time for a single-team change.
Question
Why does a shared library force joint releases, and what is the default escape?
Click to reveal answer
Answer
One class definition cannot exist as two versions in one dependency graph, so any change to shared types makes every consumer upgrade in the same cycle — code distribution where a contract was needed. Default escape: share the wire contract (Protobuf/Avro/JSON Schema) and let each service generate its own types, evolving additively so versions coexist.
Question
Shared contract vs sanctioned duplication — when is copying 30 lines the right call?
Click to reveal answer
Answer
When the shared alternative costs lockstep: duplicating a stable value object is a local cost (N copies to fix) while a shared jar is a systemic cost (coordinated release every change). Make it explicit — origin comment linking the owner — and promote to a versioned package only when the copy has changed three times (evidence, not anticipation).
Question
What mechanically removes release-ordering constraints between two services?
Click to reveal answer
Answer
Consumer-driven contract tests in CI: the producer runs its consumers' expectations on every PR, so compatibility is answered by a red build instead of a deploy negotiation — combined with additive-only versioned schemas. When the contract tests pass, neither side needs to know when the other deploys.
Revision Notes
Key Takeaways
- 1. A distributed monolith is measured, not felt: release correlation, rollback blast radius, and joint-upgrade counts prove whether the split survived contact with reality
- 2. Shared libraries distribute code where the system needed contracts — one class cannot be two versions, so lockstep is guaranteed; schemas with codegen break the cycle
- 3. Duplication is a local cost and lockstep is a systemic one: sanctioned copies with origin comments beat joint releases until change frequency justifies a versioned package
- 4. Temporal coupling is removed edge by edge: synchronous where an answer is critical, breaker + degradation where it is optional, events where no answer is needed
- 5. If independence metrics will not move because the work is truly co-changing, merging services back is the honest, data-driven answer
Interview Tips
- • 'What is a distributed monolith?' — coordinated releases across separate processes; name two forms (lockstep libraries, sync chains) and one measurement (release correlation) — most candidates only define it verbally
- • 'How would you detect one in our system?' — pull deploy history: same-window pairs, services per rollback, joint-upgrade cadence; the data ends the argument about whether the split worked
- • 'Our common-lib forces everyone to upgrade together — fix it?' — share the contract not the code (schema + per-service codegen), freeze stable parts, sanctioned duplication for tiny types, deprecation clock on the rest
- • 'Should everything become async?' — no: decision table by whether the answer is needed in-request and whether degradation is acceptable; then contract tests so the remaining sync edges do not reintroduce release ordering
Cheat Sheet
Distributed Monolith Pitfalls
Definition: separate processes that still change, release, and fail as one. Worst of both worlds — distribution's cost, monolith's coordination.
Four forms + audit:
| Form | Symptom | Fix direction |
|---|---|---|
| Release coordination | shared windows, blocking rollouts | versioned contracts + contract tests |
| Library lockstep | common-lib bumped jointly | share contracts (schemas), not code |
| Temporal coupling | chain must be up & pre-deployed | events where no answer needed; breaker + degraded answer where yes |
| Data coupling | foreign tables/FKs | database-per-service contract |
Measurements: deploy correlation (same-window pairs: >40% bad, <10% good) · rollback blast radius (1 healthy) · quarterly joint-upgrade count · lead time per team.
Shared-lib escape ladder: contract (schema + codegen) → sanctioned duplication (origin comment, promote after 3rd change) → versioned internal package with deprecation window → shared kernel (one team only).
Edge-removal decision:
answer needed now? critical → sync + tight deadline, accept joint fate in writing
degraded ok → sync + circuit breaker + agreed fallback
no → publish event, consumer catches up (ordering constraint gone)
Maturity check: if metrics cannot improve because work is genuinely co-changing, merge the services back — reversal is a data-driven option, not a defeat.