← All writing
articleJul 30, 202514 min read

Practical DRY Beyond Code Reuse

Using DRY to keep business knowledge authoritative across rules, contracts, data, operations, and documentation without forcing unrelated code into one abstraction.

DesignArchitectureEngineering
Practical DRY Beyond Code Reuse cover illustration

One checkout rule existed in five places: the web form disabled the button, the API validator rejected the request, the pricing service applied a limit, a background import used a slightly older threshold, and the support runbook described the original rule. Four implementations agreed. The fifth accepted orders the product no longer allowed.

The defect was not duplicated syntax. None of those components used the same language or even ran in the same process. The duplicated thing was knowledge: what makes an order eligible.

That is the useful meaning of DRY. Every important fact should have one authoritative representation. Copies may exist for performance, presentation, autonomy, or resilience, but their relationship to the authority must be deliberate and testable.

Similar lines are weak evidence

Consider two calculations:

decimal promotionalDiscount = subtotal * 0.05m;
decimal regulatoryLevy = subtotal * 0.05m;

The lines match today. The knowledge does not. Marketing changes the discount after an experiment. A regulator changes the levy on an effective date. Combining them into CalculateFivePercent() gives unrelated owners one code dependency and gives the method a name that explains neither rule.

Now consider eligibility in an API and a batch importer. The code may look completely different, but both must apply the same minimum age and jurisdiction policy. That is duplicated knowledge even when no clone detector finds it.

Before extracting anything, I ask four questions:

  1. Do these implementations express the same business decision?
  2. Must they change for the same reason?
  3. Do they share one owner and release policy?
  4. Can one authority serve every execution boundary without creating a worse runtime dependency?

The first three identify shared knowledge. The fourth decides how to represent it.

Choose the authority, not automatically a shared library

“Put it in a common package” is only one way to remove duplicated knowledge. The correct authority depends on the kind of fact.

Knowledge Possible authority How consumers stay aligned
State invariant Database constraint or domain model transaction fails or model rejects transition
API shape OpenAPI/schema source generated clients and compatibility tests
Authorization rule Policy/authorization service policy evaluation at protected boundary
Retry/timeout policy Owned client component shared configured client plus telemetry
Deployment topology Infrastructure module reviewed parameters and plan output
Operational recovery Versioned runbook/automation rehearsal and executable checks
UI presentation Design token/component build-time dependency and visual tests

A shared runtime service gives one live decision point, but it adds latency and availability coupling. A shared package avoids a network hop, but versions can drift. Generated code creates copies, but the copies come from one schema. A database constraint is authoritative for stored state, but it may not provide the early user feedback a UI needs.

The goal is not zero copies. The goal is zero independent definitions of the same fact.

Business rules need one executable owner

Suppose cancellation is allowed only before fulfillment starts. The dangerous implementation is one condition in the controller, another in the mobile app, and a third in the worker.

Put the transition with the state it protects:

public sealed class Order
{
    public OrderStatus Status { get; private set; }

    public CancelOrderResult Cancel(CancellationReason reason, Instant now)
    {
        if (Status is OrderStatus.Fulfilling or OrderStatus.Shipped)
            return CancelOrderResult.Rejected("order.cancellation_closed");

        Status = OrderStatus.Cancelled;
        AddEvent(new OrderCancelled(Id, reason, now));
        return CancelOrderResult.Accepted();
    }
}

The API, worker, and admin tool call the same domain operation inside their own application flows. The UI may still show or hide a cancel button using projected state, but that is guidance, not enforcement. The authoritative transition remains server-side.

If services cannot share a process or package, publish the decision as a capability instead of copying the rule. That does not mean turning every domain method into a synchronous network service. Sometimes the consuming service should own a local projection of the facts and a versioned policy artifact. The tradeoff—consistency versus runtime autonomy—must be explicit.

Contracts are knowledge too

An event producer, JSON example in documentation, hand-written TypeScript interface, C# DTO, and integration test can all define the same payload independently. They will eventually disagree.

Choose a contract authority and generate or validate the rest:

OpenAPI / AsyncAPI / protobuf / JSON Schema
                  |
        +---------+----------+
        |                    |
 generated client      compatibility tests
        |                    |
 application code       retained examples

Generation is not automatically good. Generated artifacts need deterministic tooling, reviewed diffs, version pinning, and a rule for what is safe to regenerate. Do not edit generated files manually; make the source or generator capable of expressing the required contract.

Not every internal model should become the public schema. A persistence entity, domain object, and API representation have different reasons to change. Mapping between them is not wasteful duplication when it protects those boundaries.

Data duplication can be designed

A read model repeats facts from the write model. A cache repeats database values. A search index repeats product attributes. Calling all of that a DRY violation misses why distributed systems duplicate data.

The real questions are:

  • Which system is authoritative for each field?
  • How does a copy receive changes?
  • What staleness is acceptable?
  • How is deletion propagated?
  • Can the copy be rebuilt?
  • How are divergence and lag detected?

For a search index, the product database may own name and availability while the index owns tokenization and ranking structures. A change-data or outbox pipeline updates the copy. A reconciliation job compares versions or counts, and a full rebuild restores it.

That is controlled duplication. Two services both accepting writes to “the same” customer email without conflict semantics is duplicated authority.

Avoid dual writes that assume two stores commit together:

update database -> update search index

A crash between the calls makes the copies disagree. Commit the authoritative change plus an outbox record, project asynchronously, and measure propagation lag. DRY at the knowledge level often requires accepting physical copies while refusing multiple write authorities.

Infrastructure policy has an owner

Retries are a common form of hidden duplication. The HTTP client retries three times, the repository retries twice, the message consumer retries five times, and the job scheduler reruns the whole operation. One failing dependency can receive dozens of attempts even though every local policy looked reasonable.

The fact that must be authoritative is the end-to-end attempt budget. Give one layer ownership of each retryable boundary, carry attempt context through telemetry, and make outer layers aware of the total deadline.

The same applies to:

  • timeouts;
  • circuit-breaker thresholds;
  • authentication configuration;
  • tenant resolution;
  • correlation and trace propagation;
  • redaction policy;
  • resource naming and tags.

Centralization can go too far. One “standard resilience policy” applied to payment capture, image retrieval, and idempotent metadata reads ignores different consequences. Share the mechanism and observability vocabulary; keep operation-specific policy near the owner.

Documentation can contradict the running system

Copying a command into six wiki pages makes six future edits. Prefer one versioned runbook linked from service documentation. Better, turn safe diagnostic and recovery steps into tested automation and let the runbook explain when and why to execute it.

Examples:

  • derive configuration reference from the validated schema;
  • generate API examples from contract fixtures;
  • execute documentation code samples in CI where practical;
  • link alert annotations to the owned runbook;
  • rehearse restoration steps and record the last verified date;
  • keep ownership metadata in one catalog and render it elsewhere.

Documentation still contains narrative that cannot be generated. DRY does not mean replacing explanation with source code. It means facts such as ports, retention, flags, and ownership should not be maintained independently in prose when a reliable authority exists.

Premature abstraction creates the wrong knowledge

Two similar implementations can be evidence of an emerging concept or a coincidence. Extracting after the first resemblance freezes a guess before the reasons for change are visible.

The warning signs appear in the shared API:

Calculate(
    amount,
    isLegacy: true,
    skipValidation: false,
    useNewRounding: tenant.IsPilot,
    regulationMode: country.Code);

Each flag says some callers do not share the same rule. Eventually the helper becomes a switchboard where changing one path risks all others.

I prefer honest duplication while the concept is uncertain. After two or three changes, the evidence is better: which pieces move together, who owns them, and which variation is intentional. Then extract the stable policy and leave divergent workflows separate.

This is not “always wait for three copies.” A security invariant or financial rule may require authority immediately. The point is to extract knowledge, not react mechanically to line count.

Shared libraries have a distribution problem

A common package does not create one running version. Ten services can reference ten releases. If the package owns a critical rule, the organization needs to know which versions remain valid, how upgrades are enforced, and whether old callers can coexist.

Use:

  • semantic or otherwise explicit compatibility rules;
  • automated dependency inventory;
  • deprecation dates;
  • contract tests for supported versions;
  • migration notes for behavioral changes;
  • a release owner.

Avoid a “common” library that depends on web frameworks, database providers, messaging clients, and every domain. It lowers duplication by increasing coupling. Small packages around a stable contract travel better than an internal platform monolith.

Sometimes a generated source artifact or centrally evaluated service is a better distribution model. That choice belongs in the same decision as the abstraction itself.

Detect duplicated knowledge through change history

Text similarity tools find clones, not authority. The strongest signal is a change that repeatedly requires coordinated edits.

During review and incidents, ask:

  • Which other components must change if this fact changes?
  • Has this pull request edited the same constant in several repositories?
  • Did an outage come from one copy lagging behind another?
  • Are support, application, and analytics using different definitions of the same metric?
  • Does one feature flag select implementations that claim to be one abstraction?

A change-coupling report from repository history can reveal files that move together without sharing code. Incident retrospectives reveal duplicated operational assumptions. Data reconciliation reveals duplicated ownership. DRY is discovered through system behavior as often as through source.

A safe consolidation sequence

When several definitions already exist:

  1. Name the knowledge precisely—do not begin with a generic helper name.
  2. Identify its business and operational owner.
  3. Compare every implementation and document intentional differences.
  4. Add characterization tests around current consumers.
  5. Choose the authority and distribution mechanism.
  6. Migrate one consumer at a time with comparison telemetry where risk is high.
  7. Remove the old definition, not merely stop calling it.
  8. Add a guard that prevents independent reintroduction.

For a pricing rule, shadow-calculate both versions and compare outcomes before switching. For a schema, run compatibility checks. For a database invariant, locate and repair rows that violate it before adding the constraint. For a runbook, rehearse the new automation before deleting the manual recovery notes.

Rollback also matters. If a new authority writes a format old consumers cannot read, the migration has the same compatibility problem as a database release.

The change test

An abstraction earns its place when a future requirement has one obvious home and consumers inherit the correct behavior without unrelated changes.

I use this test:

If this business fact changes tomorrow, can one owner change one authoritative representation, can every copy be traced back to it, and can we detect a consumer that disagrees?

If yes, the design is DRY even when bytes and models are duplicated. If no, replacing two similar methods with one helper may only have hidden the disagreement.

Removing repeated lines was one refactor. Making it impossible for the system to hold five independent answers to one business question was the architecture.

Further reading

Keep reading
Browse everything