skip to content

Two years after a DDD-based microservice decomposition, a Pricing service and a Promotions service constantly deploy together, share a hidden 'effective price' concept neither fully owns, and any change to one breaks the other's tests. As the architect asked to fix this, what diagnostic would you run, and how would you decide whether to merge them, redraw their boundary, or introduce a formal translation layer?

level: principalimportance: should knowfreq 40%

answer

  1. deploy-together = distributed monolith symptom
  2. trace the shared concept before choosing a fix
  3. atomic invariant -> merge/consolidate ownership
  4. tolerable staleness -> keep split + events/ACL
  5. org chart can re-erode a fixed boundary

basics

~20 s

When two services always break together, it usually means they were never really separate business areas to begin with, or the line between them was drawn in the wrong place. You'd look at what concept they secretly share, then either merge them back into one service, move the shared idea fully into one of them, or add a clear translation step between them.

solid answer

~50 s

This is a classic symptom of a mis-drawn or eroded bounded-context boundary — a 'distributed monolith,' where deployment coupling reveals that the two services never had independent ubiquitous languages in the first place. The diagnostic is to trace the shared 'effective price' concept: find every place it's read or computed in both services, and determine whether it's genuinely one aggregate's invariant (in which case the fix is merging the two services or moving full ownership of that concept into one of them, with the other consuming it via API) or whether it's legitimately two different concepts that have been conflated (in which case the fix is separating the models and adding an explicit translation/ACL at the boundary so each service's internal 'price' stays coherent). The deciding factor is whether the invariant genuinely needs atomic, single-transaction enforcement (merge) or whether the two teams can tolerate eventual consistency once the concepts are properly separated and connected by events (keep separate, add translation).

go deeper

for a junior

Should recognize that two services always breaking together is a bad sign, without necessarily being able to diagnose the root cause.

for a middle

Should be able to suggest tracing the shared concept and propose either merging or adding a clearer interface as candidate fixes.

for a senior

Should be able to distinguish an atomic-invariant case from a leaked-concept case and design the corresponding fix (consolidation vs. explicit translation/composition layer).

for a principal

Should factor in organizational ownership and Conway's Law alongside the technical diagnosis, choose and justify a remediation path (merge, re-draw, or ACL) at the level of a whole domain, and plan a migration that doesn't break existing consumers of either service.

## The symptom Two services that always deploy together and whose tests break in lockstep are exhibiting the textbook symptom of a **distributed monolith** — a system that has the operational overhead of microservices (network calls, separate deployments, separate failure domains) without the actual benefit (independent evolution). In DDD terms, this almost always means the bounded-context boundary between the two services was either wrong from the start or has eroded over time as the two services accreted overlapping responsibility around a concept — here, *effective price* — that neither one fully owns. ## The diagnostic: trace the shared concept The diagnostic starts with tracing the shared concept end to end, not with guessing at an architectural fix first. Concretely, list every field, computation, and business rule involving *effective price* in both services: - where is a promotion discount applied; - where is a base price set; - where is tax or currency conversion folded in; - and which of those steps has to happen atomically with which other step for the number shown to a customer to be correct. This tracing exercise (essentially a mini event-storming pass focused on just this concept) usually reveals one of two underlying situations, and the fix differs sharply between them. ## Situation one: one invariant, prematurely split The first situation is that *effective price* is genuinely one aggregate's invariant that got split across two services — for instance, if a price can never be legally displayed without its currently-applicable promotions already applied, and the two computations must always be consistent with each other at the moment of display or checkout, then this was never really two bounded contexts; it's one pricing concept that got prematurely split, likely along an org-chart line (a Pricing team and a Promotions team) rather than a domain seam. The fix here is genuinely to merge the two services back into one, or at minimum to move full ownership of the *effective price* computation into one of them and make the other a thin, non-authoritative consumer via its API — accepting the loss of two independently-deployable services in exchange for restoring a real, enforceable invariant and eliminating the deployment coupling that was really just distributed-transaction pain in disguise. ## Situation two: two concepts that leaked into each other The second situation is that Pricing and Promotions are legitimately separate concepts — a base price genuinely belongs to Pricing, a discount rule genuinely belongs to Promotions — but the code has let them leak into each other's internals without an explicit boundary: - Promotions reaches into Pricing's price fields directly, or - Pricing has grown ad hoc discount-adjustment logic that duplicates what Promotions already does. Here the fix is not merging but re-establishing the boundary properly: give each service its own clean model (Pricing owns `BasePrice`, Promotions owns `DiscountRule`), and introduce an explicit translation/composition point — often a thin *pricing composition* step, either a small dedicated service or a well-defined API contract — that reads from both and computes the customer-facing effective price without either service reaching into the other's internals. This preserves independent deployability for the two underlying concepts while acknowledging that displaying a price is a composition of two contexts' outputs, not a shared piece of either one. ## Choosing between the two paths Choosing between these two paths hinges on one question: does the invariant require atomic, single-transaction enforcement, or can the two pieces tolerate eventual consistency once properly separated? | Which case you are in | Where it points | |---|---| | A promotion must never be able to apply to a stale base price — a hard business rule with legal/financial consequences, e.g. regulated pricing display requirements | That argues for tighter coupling — merge, or make one service authoritative | | A brief window of staleness between a price update and a promotion recalculation is tolerable (most retail use cases) | The boundary can stay split, connected by domain events (`PriceChanged` triggering a promotion re-evaluation) rather than a shared transaction | ## The organizational dimension The organizational dimension matters too, which is why this is a principal-level call rather than a purely technical one: if Pricing and Promotions are owned by two different teams for headcount or reporting reasons unrelated to the domain, merging the services may require merging the teams (or accepting one team as authoritative owner with the other as a contributor), which is a harder conversation than the code change itself — Conway's Law cuts both ways, and a boundary that's wrong in the org chart will keep re-eroding the service boundary no matter how many times it's technically fixed. A concrete real-world parallel is e-commerce systems that eventually consolidate *pricing engine* and *promotions engine* into one owned pricing-and-offers domain specifically because their invariants (what price a customer is legally shown) turned out to require atomic consistency that two independently-evolving services couldn't reliably provide, while still keeping catalog and inventory — genuinely independent concepts — as separate services connected by events.

  • Before deciding merge-vs-redraw, how would you distinguish 'these are really one concept' from 'these are two concepts that leaked into each other from bad discipline'?
    Check whether the two pieces must be consistent with each other at the exact same instant to satisfy a real business rule — if a promotion could ever legally apply against a base price that's a split-second stale, and nobody would notice or care, that argues for two concepts connected by eventual consistency. If any staleness between the two would produce an incorrect, non-compliant, or financially wrong result shown to a customer, that argues they were always one invariant and should be owned atomically by one service.
  • If the organizational fix (merging two teams) isn't politically feasible even though the technical diagnosis says the services should merge, what's a reasonable interim architecture?
    Make one of the two services the single source of truth/authoritative owner for the shared invariant (say, Pricing owns and computes the final effective price, incorporating promotion rules it reads from Promotions via API at computation time), so there's still exactly one place enforcing the invariant even though two teams contribute to it. This doesn't fully restore team independence, but it stops the invariant from being split across two independently-committing transactions, which was the actual source of the breakage.
  • What's a concrete code-level signal, short of watching deployments, that tells you two services have drifted into sharing an unowned concept like this?
    Look for duplicated or near-duplicated business logic in both services around the same concept (e.g. both computing some version of 'final price'), or for one service's client code reaching past the other's public API to read internal fields/tables directly. Another strong signal is integration tests in one service that have to stub out or replicate the other service's internal logic just to get a realistic result, which means the two are really testing one shared concept from two angles.

Like two neighboring shops that keep needing to renovate together because the wall between them was never load-bearing where they put it — sometimes the right fix is knocking the wall down and running it as one shop, and sometimes the right fix is rebuilding the wall in the correct place and adding a proper doorway (a defined interface) between two shops that really are separate businesses.

saying these in an interview costs you the question

  • Jumps straight to 'just merge the services' without tracing what the shared concept actually is
  • Doesn't consider the organizational/team-ownership dimension of the fix
  • Treats 'always deploy together' as acceptable/normal for microservices
  • Proposes a technical fix (e.g. adding a cache) that papers over the coupling instead of addressing the boundary
  • Can't distinguish an invariant that needs atomicity from one that can tolerate eventual consistency

context