skip to content

A codebase built its entire Service Layer in the operation-script style - each service method contains its own procedural business logic rather than delegating to domain objects. Over two years, the same 'is this customer eligible for expedited shipping' check has been copy-pasted into four different service methods and now disagrees in one of them. What does this indicate about how the Service Layer has scaled, and what are the practical remedies?

level: seniorimportance: should knowfreq 50%

answer

  1. no natural home for shared rules in operation script
  2. same rule copy-pasted across methods -> silent drift
  3. fix: extract policy/helper, or move to domain object
  4. must also refactor + delete old copies, not just add helper
  5. god-service class bloat is the sibling symptom

basics

~20 s

It means the same business rule now lives in several places instead of one, so they can quietly drift apart and disagree. The usual fixes are pulling the shared rule out into a single helper or domain method that every service method calls, then deleting the old copies.

solid answer

~40 s

This is the classic operation-script duplication failure: because the rule was written procedurally inside each service method rather than owned by one object, there was no single place that different call paths were forced to reuse. Practical remedies without fully rewriting to rich domain facades include extracting the shared rule into a private helper method or a small policy/rules class that all four service methods call, or moving it onto the Customer/Shipment domain object as a single method (a partial shift toward domain facade for just that rule). Either way the point is establishing one authoritative implementation and refactoring all four call sites to depend on it, then adding a regression test that pins the now-unified behavior so it can't silently diverge again.

go deeper

for a junior

Should recognize that copy-pasting the same check into multiple methods risks the copies disagreeing later.

for a middle

Should be able to name at least one concrete remedy (extracting a shared helper/policy) and explain why operation script doesn't prevent this duplication by default.

for a senior

Should distinguish the lightweight fix (shared helper/policy class) from the heavier one (moving logic onto a domain object) and know both require actually refactoring existing call sites, not just adding a new shared method.

for a principal

Should recognize this as a symptom of a broader architectural maturity question, know how to set team norms/tooling to catch duplication early, and decide when a targeted domain-facade migration for just the recurring pain points is worth the cost versus a broader rewrite.

## How the same rule ends up in four places When a Service Layer is built entirely in the operation-script style, every use case's logic is written procedurally inside its own service method, with no structural mechanism forcing related use cases to share a rule even when they conceptually should. 'Is this customer eligible for expedited shipping' isn't owned by any one place — it's just a snippet of conditional logic that: 1. got typed into `placeOrder()`, 2. then copy-pasted (with small, well-intentioned tweaks) into `reorderPreviousCart()`, 3. then again into `adminOverrideOrder()`, 4. and again into a batch job's `bulkReship()` method. Each copy starts identical, but because there's no single implementation both/all call sites depend on, later bug fixes or rule changes only get applied wherever someone remembers to make them, and the copies silently diverge. This is not a hypothetical risk — in any codebase where 'copy the existing method and tweak it' is the easiest way to add a new use case, this kind of drift is close to inevitable given enough time and enough people. ## Why this is a scaling problem, not a one-off bug The reason this specifically indicates a scaling problem with the operation-script style (rather than being a one-off bug) is that operation script has no natural place for shared rules to live. In a domain-facade design, 'expedited shipping eligibility' would be a method on a `Customer` or `ShippingPolicy` domain object, and every service method that needs the answer calls that one method — there's structurally only one implementation to diverge from. In pure operation script, the equivalent shared logic has to be deliberately factored out into something (a private helper, a static utility, a small rules class) or it defaults to being duplicated, because the natural unit of code organization in that style is 'one service method equals one self-contained procedure.' ## The practical remedies The practical remedies fall into two categories, and teams don't have to fully commit to rewriting the whole Service Layer as rich domain facade to apply them. - **The lighter-weight fix** is extraction without object-orientation: pull the shared conditional logic into a private helper method within the service class, or — if it's needed by multiple service classes — into a small stateless policy or rules class (e.g., a `ShippingEligibilityPolicy` with a single `isEligibleForExpedited(customer, order)` method) that every relevant service method calls. This preserves the operation-script style everywhere else but establishes exactly one authoritative implementation for this particular rule, which is really a targeted, minimal step toward domain-facade for just the piece of logic that turned out to need reuse. - **The heavier fix** is to actually move the logic onto the relevant domain object as a proper method, which additionally gets you the domain-facade benefits of being unit-testable in isolation and being harder to accidentally duplicate again, since it now naturally belongs to the object it concerns rather than floating as a free-standing rule multiple services could each reimplement. ## Finishing the job: delete the old copies Either remedy needs to be paired with actually refactoring the existing divergent call sites to depend on the new single implementation — simply adding a shared helper without deleting the old copies doesn't fix anything, and is a common half-measure that leaves five implementations instead of four. Once unified, adding a focused regression test (or a set of parameterized tests covering the known edge cases that had caused the four copies to diverge in the first place) pins the now-single behavior, so a future change to eligibility rules can't silently regress in one call path while being correctly applied everywhere else, which is exactly the failure mode that got the team into this situation. ## How teams usually notice it A concrete real-world pattern this maps to: teams building on frameworks like Spring often notice this exact issue by running full-text search across the codebase for a magic number or string threshold (say, a '$500' free-shipping cutoff) and finding it hardcoded in three or four different `@Service` classes with slightly different comparison operators (>, >=) — a very common and detectable symptom of operation-script duplication that a simple grep for a business constant can surface, which is itself often how teams first discover the problem rather than through a subtler code review. ## The sibling symptom Beyond duplication, a second, related symptom of operation-script strain at scale is **service-class bloat**: because there's no natural home for shared logic, teams often respond by cramming more and more marginally-related use cases into the same service class just because it already has some convenient private helper methods nearby, producing a wide 'god service' class handling dozens of unrelated concerns, which slows down code review, increases merge conflicts, and makes the class hard to reason about or split later, since its internal helpers are entangled across use cases that don't actually belong together.

  • Is extracting a shared helper method the same thing as switching to domain facade?
    Not fully — it's a lighter partial step. A shared helper or policy class centralizes one rule without necessarily giving the domain objects themselves any new behavior, whereas domain facade specifically means the rule lives as a method on the domain object it concerns. Either removes the duplication risk, but only the domain-object version gets you isolated unit-testability of that object's own invariants.
  • How would you detect this kind of duplication before it causes a production bug, rather than after?
    Code review discipline that asks 'does an equivalent check already exist elsewhere' when a new service method is added, periodic duplication-detection tooling (many static analyzers flag near-identical code blocks), and grepping for known business constants/thresholds across the codebase are all practical, low-effort ways to catch it before it silently diverges.
  • Does this duplication risk go away entirely once you move fully to domain facade?
    It's greatly reduced but not eliminated — teams can still accidentally implement a similar rule on two different domain objects (say, both Customer and Order independently deciding shipping eligibility) if ownership of the concept isn't clearly assigned, so domain facade lowers the risk by giving logic a natural home, it doesn't guarantee only one home is ever chosen.

It's like four different employees each keeping their own handwritten copy of the same company policy instead of everyone checking one shared policy binder — each copy starts the same, but small edits to one copy never make it into the others.

saying these in an interview costs you the question

  • Thinks adding a shared helper without removing the old duplicated copies fixes the problem
  • Doesn't recognize service-class bloat ('god service') as a related symptom of the same root cause
  • Assumes this kind of drift can only be caught by a production incident, not by review/tooling
  • Can't name a concrete refactor (extract policy, move to domain object) as a remedy
  • Believes rewriting the entire Service Layer to rich domain model is the only fix

context