skip to content

How does applying DRY across service, module, or team boundaries differ from applying it inside a single codebase — and when is deliberate duplication the better architectural choice?

level: seniorimportance: should knowfreq 48%

answer

  1. DRY's scope: "within a system"
  2. shared domain lib → distributed monolith, lockstep release
  3. single-source contracts, duplicate context-local rules
  4. same word ≠ same concept (bounded contexts)
  5. must duplicate → generate or contract-test the copies

basics

~20 s

Inside one codebase, sharing costs you a function call. Across services it costs coordination: a shared library of business rules means teams must upgrade and release together. So teams often duplicate small logic on purpose to stay independently deployable.

solid answer

~50 s

DRY's scope clause — "within a system" — matters. Inside one deployable unit, deduplication couples code that already ships together, so the cost is low. Across independently deployed services, a shared library containing domain logic re-creates the coupling that separate services were meant to remove: a rule change forces a coordinated version bump, lockstep releases, and an implicit shared release train, while a runtime shared service adds a network hop, a new failure mode, and a scaling dependency. The usual guidance is therefore asymmetric: aggressively single-source *contracts and data* — schemas/IDL with generated clients, event schemas in a registry, shared infrastructure and cross-cutting concerns — but tolerate duplicating small amounts of *business logic* that belongs to different bounded contexts. Two services that both compute "is this customer premium?" often are not duplicating: each context's definition can legitimately diverge. Where duplication really is one rule and cannot be centralised, make drift detectable — contract tests, consumer-driven tests, or a generated artifact — rather than trusting convention.

go deeper

for a junior

Say that copying code between two services isn't automatically wrong, because a shared library makes both teams release together.

for a middle

Contrast build-time coupling (shared library) with runtime coupling (shared service) and give the distributed-monolith symptoms.

for a senior

Give the asymmetric policy — single-source contracts, data ownership, and cross-cutting technical concerns; duplicate context-local business rules — and mention bounded contexts and homonyms.

for a principal

Set it as org policy: who owns shared artifacts, deprecation and versioning rules, contract testing / schema registry as the enforcement mechanism, and the explicit trade of duplication for team autonomy and independent deployability.

## Why the boundary changes the maths Deduplication converts duplication into **coupling**. The price of coupling depends on what the two parties share: | Scope | Cost of sharing code | Cost of duplication | |---|---|---| | Same module | Trivial (a call) | Low — both copies are visible, one commit fixes both | | Same deployable, different modules | Low, but adds a dependency edge (architecture rules may forbid it) | Moderate | | Different services, same team | Version bump + release | Moderate | | Different services, different teams | Coordination: agree, bump, test, release in order; possible lockstep | Higher — drift is invisible across repos | | Different organisations | Very high — versioning, deprecation windows, support | Highest — you cannot fix their copy | Inside one deployable everything already ships together, so "they must change together" is free. Across deployables you have deliberately bought *independent evolution*; a shared library of business logic silently sells it back. ## The shared-library trap Symptoms of the anti-pattern (often called the distributed monolith): - A `common`/`core`/`shared-domain` library that every service depends on. - Any change to it requires bumping N services; teams block on each other's release windows. - Services cannot upgrade independently because the library also pins framework versions (diamond dependency conflicts). - Nobody owns the library; it becomes a junk drawer, and its API is shaped by whoever needed something last. - Deploy order matters — the classic tell that you have one system pretending to be many. The alternative — a *runtime* shared service instead of a shared library — trades build-time coupling for runtime coupling: a network hop, latency, a new availability dependency (your service is now down when theirs is), and cascading-failure risk. Sometimes worth it (a genuine authority such as pricing or authorisation), often not. ## What you SHOULD single-source across boundaries The guidance is not "duplicate everything". Some things must have one authority: 1. **Contracts.** API schemas (OpenAPI/gRPC/GraphQL) and event schemas in a registry. Generate server stubs, clients, and docs from them so producer and consumer cannot describe the message differently. This is DRY by *derivation*: no runtime coupling, no flag creep. 2. **Data ownership.** One service owns each entity; others hold read models or caches derived from its events. Two writable copies of the same fact is the worst duplication of all. 3. **Cross-cutting technical concerns.** Auth token verification, tracing/logging conventions, crypto, HTTP client defaults, retry/backoff. These are technical, stable, and dangerous to get wrong — the classic case where a shared library (or a sidecar/service mesh, moving it out of the process entirely) earns its keep. 4. **Security decisions.** Authorisation logic duplicated per service drifts into holes; centralise the decision (policy service, gateway, or a hardened library) even at some coupling cost. 5. **Infrastructure definitions.** One IaC module rather than copy-pasted environment scripts, where drift causes "works in staging" incidents. ## What you should be willing to duplicate 1. **Business rules in different bounded contexts.** "Customer" in Billing (a payer with a tax ID) and "Customer" in Support (a person with a ticket history) are different concepts with the same word. Unifying them creates a god-model that satisfies neither — the reason Domain-Driven Design puts a context boundary there instead. What looks like duplication is often *homonyms*. 2. **Small helpers.** A 5-line date formatter, a slug function. The library's versioning overhead exceeds the rule's value. 3. **DTOs at the edge.** Each service mapping the wire contract into its own internal model is healthy: it keeps the internal model free to evolve and prevents one service's refactor from breaking another. Sharing entity classes across service boundaries couples internals. 4. **Anything whose divergence is *allowed*.** If Billing may one day round differently from Reporting, they were never one rule. ## When duplication is real but centralisation is impossible Sometimes one rule genuinely must exist in several runtimes (a validation in a browser and on a server; a limit in the app and in a database constraint; a calculation in a batch job and an online path). Preference order: 1. **Generate** all copies from one source (schema → validators; a rules file → code in each language). 2. **Contract-test** them: consumer-driven contract tests, or a shared test-vector suite (a JSON file of inputs/expected outputs) that every implementation runs. Then drift fails a build, not a customer. 3. **Cross-reference and monitor**: document the copies, and add an assertion/alert on disagreement in production (e.g. shadow-compare results). The principle: *if you must duplicate, never let it drift silently.* ## How to answer the "do we extract a shared library?" question Ask: - Is this **knowledge of one domain concept** with one owner, or the same word in two contexts? - Would a change force a **coordinated release**? Who is blocked? - Is it **technical and stable** (good candidate) or **business and volatile** (bad candidate)? - Can it be **derived** instead of shared (codegen, schema) — the best of both? - Does the library also drag **framework/transitive dependencies** across the boundary? - Who **owns** it, and what is the deprecation policy for its versions? A defensible senior answer names the trade explicitly: "I'll single-source the contract and the auth check, and let the two teams each keep their own eligibility rule, because those are different definitions in different contexts and I'd rather have two rules than one shared release train."

  • Two services both need to know whether an order is eligible for free shipping. Do you extract a shared library?
    First check whether it is really one rule: if fulfilment's definition can diverge from marketing's, they are two rules and should stay separate. If it is genuinely one authoritative policy, prefer making one service the authority and exposing it via API or publishing eligibility as an event, rather than a shared library that forces coordinated releases — and if latency forbids that, share it as a generated artifact with contract tests.
  • What's the risk of sharing entity/DTO classes across two services?
    It couples internal models to the wire and to each other: one team's refactor breaks the other's compile, additive changes become breaking, and independent deployability is lost. Prefer a schema as the contract with each side mapping into its own internal model.
  • When is a shared library across services clearly the right answer?
    Technical, stable, security-sensitive cross-cutting concerns — token validation, crypto, tracing conventions, HTTP/retry defaults — where divergence is a defect and the API changes rarely. Even then, prefer moving it out-of-process (gateway, sidecar) if versioning becomes a coordination burden.

Two restaurants in a chain can share a supplier contract and a food-safety standard without sharing one kitchen. Force them to cook every dish in a single central kitchen and one kitchen fire closes both — that's the shared business-logic library.

context