Developers often learn the DRY principle as “do not repeat yourself.” That shorthand can lead to the wrong refactoring: two blocks look similar, so they are merged into one abstraction. Later, the two cases evolve for different reasons, and the shared abstraction fills with flags and exceptions.
The more useful idea is narrower: avoid having the same piece of knowledge represented independently in multiple places.
That distinction changes how you refactor. Similar code is only a clue. The real question is whether the copies encode one decision that must stay consistent, or different decisions that merely happen to look alike today.
This article develops that mental model and shows how to remove duplicated knowledge without coupling unrelated behavior.
Think in terms of decisions that can drift
Suppose a service decides that standard orders qualify for free shipping at $50. The threshold appears in two places:
checkout:
if order.total >= 50:
shipping = 0
shipping_quote:
if order.total >= 50:
return "free"The problem is not that the number 50 appears twice. The problem is that both places claim authority over the same business rule.
If the threshold changes to $60 and only one copy is updated, the system can tell a customer that shipping is free and then charge for it at checkout. One decision has two independent representations, so the representations can drift.
A useful test for duplication is therefore:
If one copy changes, must the other change for the system to remain conceptually correct?
When the answer is yes, you probably have duplicated knowledge.
Give one rule one authoritative representation
The smallest improvement is to name the rule once and make both callers depend on it:
function qualifies_for_free_shipping(order):
return order.total >= 50
checkout:
if qualifies_for_free_shipping(order):
shipping = 0
shipping_quote:
if qualifies_for_free_shipping(order):
return "free"Now a change to the threshold has one authoritative implementation. The two callers still have different responsibilities: checkout calculates a charge, while the quote produces a message. Only the shared decision has been centralized.
This is an important boundary. DRY does not require combining the entire checkout and quoting workflows. It asks you to identify the knowledge they genuinely share and represent that knowledge once.
In production code, the rule might belong in a domain object, policy object, module, or configuration-backed component. The exact mechanism matters less than ownership: there should be a clear place that answers the question “does this order qualify?”
Similar syntax is not enough
Now consider two unrelated rules:
if failed_login_count >= 3:
require_extra_verification()
if failed_delivery_count >= 3:
pause_subscription()The conditions have the same shape. Both compare a count with 3. Extracting a generic helper such as this is easy:
function reached_limit(count):
return count >= 3But the abstraction hides two different policies. The login threshold may change because of an account-protection decision. The delivery threshold may change because of a customer-service decision. Their current values are coincidental.
If one policy changes to 5, the shared helper either becomes wrong or gains parameters that merely reconstruct the original decisions:
reached_limit(failed_login_count, 5)
reached_limit(failed_delivery_count, 3)Nothing meaningful has been deduplicated. The code is less explicit, while the two pieces of knowledge remain separate.
This gives us a second test:
Do these pieces of code change for the same reason?
If they change for different reasons, keeping them separate is often the clearer design even when their syntax is nearly identical.
Distinguish knowledge duplication from representation duplication
Not every repeated value is duplicated knowledge, and duplicated knowledge does not always look identical.
Imagine a tax rate stored as 0.2 in a calculation and displayed as 20% in explanatory text. The strings differ, but they may represent the same policy. If the rate changes, both representations must change together.
Conversely, two independent timeouts might both be 30 seconds. One protects an interactive request; the other controls a background job. Equal values do not make them one rule.
The important relationship is semantic, not textual:
- Textual duplication means code or data looks the same.
- Knowledge duplication means multiple places independently encode the same fact, rule, mapping, or decision.
Textual duplication can make knowledge duplication easier to notice, but it is neither necessary nor sufficient evidence.
Look for duplicated mappings and derived facts
Business rules are only one form of duplicated knowledge. Mappings are another common source.
Suppose an application defines supported document states in one module:
states = ["draft", "review", "published"]A separate validation module independently lists the accepted values, and a user-interface module independently builds the same options. Three lists now encode one fact: which states exist.
Adding archived requires coordinated edits. Missing one produces inconsistent behavior.
A better design chooses one authoritative representation and derives the other forms from it where practical. The authoritative representation might be a type, a domain definition, or metadata from which validation and presentation options are produced.
The same reasoning applies to derived facts. If a value can be calculated reliably from authoritative data, storing another independently editable copy creates an opportunity for disagreement. For example, storing both quantity, unit_price, and an editable subtotal requires a rule for keeping the subtotal synchronized. Computing the subtotal when needed avoids that synchronization problem when the performance and historical requirements allow it.
Do not turn this into a rule that derived values must never be stored. Caches, materialized views, audit records, and performance-sensitive systems often store derived data deliberately. In those cases, the design must define which representation is authoritative and how stale or inconsistent copies are detected or repaired.
Use change history as evidence
When you are unsure whether two pieces of code represent the same knowledge, their change patterns can help.
Consider two validation functions that both check a maximum length of 100 characters. Ask why each limit exists. Then inspect how the code has evolved:
- Are the limits defined by the same requirement?
- Do changes to one routinely require changes to the other?
- Would a product or engineering decision naturally mention both at once?
- Could one change while the other remains correct?
If they move together because they express one policy, centralization can reduce maintenance risk. If they merely started with the same value, sharing may create a dependency that the domain does not actually have.
Change history is evidence, not proof. Requirements can diverge later. The goal is to understand the underlying reason for change rather than mechanically count matching commits.
Refactor toward ownership, not utility functions
A common response to duplication is to create a generic utils module. That can remove repeated lines while making ownership less clear.
Suppose several modules calculate whether a subscription is within its cancellation window. A helper named is_within_days(date, 14) removes repeated date arithmetic, but every caller still owns the 14-day policy. A future change can still miss one caller.
Prefer an operation that names the domain decision:
cancellation_policy.can_cancel(subscription, now)The policy object or module owns the duration and the interpretation of dates. Callers ask a meaningful question instead of reconstructing the rule from generic pieces.
This is the deeper benefit of removing duplicated knowledge: it establishes ownership. A developer changing a rule can find the place where that rule lives and see the callers that depend on it.
Be careful with configuration
Moving a repeated value into configuration can create one source of data without creating one source of meaning.
For example, this configuration looks centralized:
free_shipping_threshold = 50But if several callers still interpret the threshold differently, the rule remains fragmented. One caller might use total >= threshold, another total > threshold, and another compare a pre-tax amount while checkout compares a post-tax amount.
The shared number is not the whole policy.
When consistency matters, centralize the decision at the level where its semantics are clear. A function such as qualifies_for_free_shipping(order) can define which total is used, whether equality qualifies, and which order types are excluded. Configuration can still supply the threshold, but configuration does not replace the policy that interprets it.
Know when duplication is cheaper than abstraction
Removing genuine knowledge duplication is valuable because it prevents drift. Removing mere code similarity has a different trade-off.
Two small implementations may be easier to understand than a shared abstraction with callbacks, mode flags, optional arguments, or a complicated type hierarchy. This is especially true when the cases are expected to evolve independently.
A practical approach is to tolerate some code duplication until the shared concept is clear. Repetition gives you evidence about what is actually stable across the cases. Once you can name the shared knowledge precisely, extract that knowledge rather than every common line around it.
This does not mean waiting indefinitely. If duplicated code already represents one rule and a change must be synchronized across several places, the maintenance risk is present now. Centralizing that rule is useful even if the surrounding workflows remain separate.
Watch for abstractions that reunpack themselves
A poor deduplication often reveals itself through parameters that select unrelated behavior:
process(record, mode="invoice")
process(record, mode="shipment")Inside, the function branches repeatedly on mode. The abstraction merged two workflows, then recreated their differences with conditionals.
Other warning signs include:
- boolean flags that change large parts of a shared function;
- parameters used by only one caller;
- branches named after callers rather than domain concepts;
- changes for one use case repeatedly touching code used by another;
- a shared abstraction whose name is vague because the shared idea is hard to state.
These signs do not automatically make an abstraction wrong. They are prompts to ask whether the code shares knowledge or only shape.
Sometimes the right refactoring is to split the abstraction again and retain only a smaller shared policy or calculation.
A practical decision process
When you notice repetition, do not start by extracting it. First identify what each copy means.
If both copies encode the same rule, ask where that rule should be owned. Move the decision there, then make callers depend on that representation. If the copies express different rules, keep them independent even if their implementations currently match.
For uncertain cases, ask what future change would separate them. If you can describe a realistic change that should affect only one copy, that is strong evidence that the copies do not belong behind one shared policy.
After refactoring, check the opposite scenario: if the shared rule changes, is there now one obvious place to make that change? If not, you may have removed repeated syntax without removing duplicated knowledge.
Conclusion
DRY is most useful as a rule about knowledge, not appearances.
When one business rule, mapping, constraint, or derived fact is represented independently in several places, those representations can drift. Give that knowledge a clear owner and let other code depend on it.
When two pieces of code merely look alike, first ask why each exists and why each would change. If their reasons differ, duplication may be simpler and more maintainable than an abstraction that couples them.
The practical goal is not the fewest lines of code. It is a system in which each important decision has an obvious, authoritative home.