DRY in C#: Share Business Rules Without Over-Coupling Unrelated Code
Share business rules without merging unrelated workflows.
Two callers, one price rule
Checkout and draft invoicing both price a subtotal of 150 under a rule that discounts amounts above 100 by 10%. Predict their totals. Then raise the threshold to 200: should an already-issued invoice change too? Identify the rule to share and the historical value to preserve before reading the code.
Share knowledge with the same owner
DRY is often misunderstood as “never repeat any code.” That interpretation is too literal. Two code fragments can look similar and still deserve to stay separate because they change for different reasons. The real problem is when the same business rule is encoded in multiple places and can drift out of sync.
A useful C# example is a pricing rule used by checkout and invoices. Suppose both paths must apply the same discount threshold, tax category, or rounding policy. That rule should live in one place, because it is the same business knowledge. If checkout calculates “10% off orders over 100” and invoices independently re-implement the same rule, a future change to 120 or 15% becomes a bug magnet.
But now imagine checkout also needs to reserve inventory, and invoices also need to format legal text. Those responsibilities are not the same business knowledge. Extracting all of that into one shared abstraction would create coupling between unrelated workflows. The result is often worse than duplication: a small change in one area forces edits in a shared component that was never truly a common domain concept.
Deep dive
A good DRY refactor starts by asking: What is the duplicated knowledge? If the answer is “the discount threshold and rate,” then share that rule explicitly. If the answer is “two methods both use if and return,” that is only structural similarity, not a reason to merge them.
For a pricing-rule example, imagine checkout and invoices both need to determine whether an order qualifies for a loyalty discount. That rule may be expressed as a dedicated domain service, a value object, or a policy function. The key is that the abstraction represents a real concept in the business domain, not just a convenience for code reuse.
A healthy extraction usually has these traits:
- It names a business concept, such as
DiscountPolicyorPricingRules. - It contains logic that truly belongs together because it changes together.
- It is depended on by multiple callers that need the same answer.
- It does not pull unrelated workflow steps along with it.
A common failure is overgeneralization. For example, you might notice that both checkout and invoices need order totals. One calculates totals before payment, the other prints totals after fulfillment. Those totals may share arithmetic, but the surrounding context is different: checkout may apply promotions, invoices may include display formatting or accounting-specific fields. Forcing both into one giant “OrderProcessor” creates an object that knows too much and becomes a bottleneck.
A more nuanced rule is: extract stable business rules, not incidental implementation shapes. If the same tax or discount calculation appears in two places, that is a candidate for sharing. If two methods simply have the same control flow but one works on an HTTP request and the other on a database entity, the duplication may be harmless or even preferable.
A practical interview-ready example:
Checkout: determine final price for the cart Invoices: determine final price for the line items on the bill Shared rule: orders above the threshold get the same loyalty discount Not shared: checkout also handles payment authorization, invoice also handles document formatting
This is where coupling matters. Extraction gives you a single rule, but it also creates a dependency. If the shared abstraction is poorly chosen, every caller becomes tied to its shape. That coupling is acceptable when the concept is truly shared; it is harmful when the abstraction only exists to eliminate similar lines of code. In other words, DRY is not just about removing repetition; it is about selecting the right level of ownership for a rule.
A good question to ask before extracting is: Will these call sites really change for the same reason? If yes, share the rule. If no, keep the code separate and accept the duplication.
For issued invoices, preserve the prices, discounts and effective rule version captured when the transaction was agreed. Sharing a rule does not mean recalculating a historical invoice using today's discount policy. The order snapshot and the reusable pricing policy serve different purposes.
A small C# example
In this illustration, checkout and draft-invoice creation price the same order before agreement. Both use the same policy object; payment and document formatting stay outside it. The policy parameters and subtotals below are fixed valid inputs. This is a discount-only example, not a full pricing engine: currency, tax, rounding, persistence and input validation are omitted.
using System;
using System.Globalization;
var policy = new DiscountPolicy("loyalty-v1", 100m, 0.10m);
Console.WriteLine(Money(policy.Total(100m)));
// Two callers quote the same order under the same effective policy.
decimal checkoutQuote = policy.Total(150m);
decimal draftInvoiceQuote = policy.Total(150m);
Console.WriteLine(checkoutQuote == draftInvoiceQuote);
// Issuing the invoice captures the agreed result and policy identity.
var issued = new InvoiceSnapshot(policy.Version, draftInvoiceQuote);
var newPolicy = new DiscountPolicy("loyalty-v2", 200m, 0.10m);
Console.WriteLine(Money(newPolicy.Total(150m)));
Console.WriteLine($"{issued.PolicyVersion}: {Money(issued.AgreedTotal)}");
static string Money(decimal value) =>
value.ToString("0.00", CultureInfo.InvariantCulture);
public sealed record DiscountPolicy(string Version, decimal Threshold, decimal Rate)
{
public decimal Total(decimal subtotal) =>
subtotal > Threshold ? subtotal * (1m - Rate) : subtotal;
}
public sealed record InvoiceSnapshot(string PolicyVersion, decimal AgreedTotal);Expected output:
100.00 True 150.00 loyalty-v1: 135.00
Exactly 100 does not qualify because the rule says above the threshold. Under v1, both callers quote 135 for a subtotal of 150. Raising the threshold to 200 changes a new quote to 150; it does not rewrite the issued invoice's captured 135. The snapshot here is only an in-memory illustration of the values a real application would persist.
Failure modes
- Mechanical deduplication: removing every repeated pattern, even when the code is coincidentally similar.
- God abstraction: creating one shared class or helper that bundles unrelated checkout and invoice behavior.
- Leaky reuse: sharing a rule but forcing callers to know too much about internal details.
- Unowned duplicated policy: the same discount threshold is copied into checkout and invoice creation, but nobody owns keeping those copies consistent when the rule changes.
- Overcoupling through helper obsession: placing every shared operation into a common utility class until the code becomes hard to test and harder to evolve.
Interview drill
Use these prompts to practice a strong answer:
- What does DRY mean in terms of knowledge rather than code text?
- When is it better to keep duplicated code instead of extracting it?
- How would you share a pricing rule between checkout and invoices without sharing unrelated workflow?
- What signals tell you an abstraction is too broad?
- How does extraction introduce coupling, and when is that coupling acceptable?
A concise interview answer could be: “I use DRY to centralize business rules like pricing thresholds, because duplicated knowledge is risky. But I do not merge unrelated code paths just because they look similar. If checkout and invoices both need the same discount rule, I share that rule; I do not force payment, formatting, and accounting into the same abstraction.”
Answer checkpoints
- Knowledge: the threshold and rate represent one rule. A shared
DiscountPolicyowns that calculation; matching punctuation or control flow does not establish shared meaning. - Intentional duplication: a checkout input check and an invoice layout check may look alike but have different owners and reasons to change. Keep them separate when they are different rules.
- Pricing boundary: share the applicable policy while quoting the same transaction. Keep payment and formatting in their workflows; render an issued invoice from the agreed snapshot.
- Too broad: a proposed
OrderProcessorneeds flags such asisInvoiceto skip payment andisCheckoutto skip document formatting. Those caller-specific branches are a reason to reconsider the boundary. - Coupling: changing
DiscountPolicy.Totalaffects every caller. That is useful when each caller must follow the same effective discount rule; it is harmful if they only happen to return the same number today.
Sources
- Hunt and Thomas: DRY is about knowledge, the authors' published chapter excerpt.
- Sandi Metz: The Wrong Abstraction, on shared code that accumulates caller-specific branches.
TechInterviewNotes Editorial Team · Updated 28 Oct 2026 ·