📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What does DRY really mean, and when does removing duplication make a .NET codebase worse?

Don't Repeat Yourself as single source of knowledge, not identical text; accidental versus true duplication; the wrong abstraction; where DRY matters most.

Mid-Level Theory Intermediate design-principles dry refactoring

🎯 Key points

  • DRY is about a single authoritative source for each piece of knowledge, not identical lines
  • True duplication changes together for the same reason; accidental duplication only looks alike
  • Merging accidental duplication couples unrelated features
  • The wrong abstraction is costlier than duplication
  • DRY matters most for business rules, constants, schemas and contracts
  • Rule of three: wait for a pattern before abstracting

⏱ 30-second answer

DRY (from The Pragmatic Programmer) says every piece of knowledge should have one authoritative representation in the system. It is about rules, not text: the GST rate, the definition of an overdue invoice or a validation rule should live in one place. Two blocks of code that look the same but change for different reasons are accidental duplication, and merging them couples unrelated features. I remove true duplication of business knowledge quickly, and I tolerate similar-looking code until a real shared concept appears, usually around the third repetition.

🗣 Full answer (senior)

How I would answer this in an interview

"DRY comes from The Pragmatic Programmer: 'every piece of knowledge must have a single, unambiguous, authoritative representation within a system'. The key word is knowledge, not text. The GST rate, what counts as an overdue invoice, a validation rule, a database schema, an API contract: each should live in one place. When it's copied, the copies drift, and you get bugs like reminder emails sent for disputed invoices because only three of four copies were updated. But two blocks of code that merely look alike and change for different reasons are accidental duplication. Merging them couples unrelated features, and the shared helper slowly grows isSupplier / skipValidation flags until it's what Sandi Metz calls the wrong abstraction, which costs more than the duplication did. So I remove true duplication of business knowledge quickly, I tolerate similar-looking incidental code until a real shared concept appears (often around the third occurrence), and across service boundaries I share contracts, not domain code."


1. What DRY is about

DRY targets Single source
Business rules (overdue, eligibility, pricing) One named policy/domain method
Constants with meaning (tax rate, grace days) Configuration or named constants
Schemas EF Core model → migrations; or DB-first scaffolding, not both by hand
API contracts OpenAPI → generated clients; protobuf → generated code
Validation shared by layers One validator (or generated from the contract)
Status/state values Enums or constants, not scattered strings
Documentation that restates code Generate it, or keep it at the level of why

2. True vs accidental duplication

True duplication Accidental duplication
Encodes The same knowledge Different knowledge that happens to look alike
Changes Together, for the same reason Independently, for different reasons
Owned by Same actor Different actors/features
If merged One fix applies everywhere ✅ Shared code grows flags and conditionals ❌
Example Overdue rule in 4 places Customer vs supplier address validation

3. When removing duplication makes things worse

Situation Why merging hurts
Similar code serving different features/teams Couples change cycles; one team's change breaks the other
Shared helper needs a flag per caller The wrong abstraction: harder to change than the copies
Domain code shared across microservices Couples deployments (distributed monolith)
Tests made DRY with deep base classes/helpers Each test becomes unreadable on its own (prefer DAMP)
Premature extraction after two occurrences You guess the shape of the abstraction before you know it
Shared "Common" project for unrelated utilities Becomes a dependency magnet everyone must upgrade

4. Decision checklist

Question If yes
Must all copies change together? Merge
Would a change request mention only one of them? Keep separate
Would the shared version need per-caller flags? Keep separate (or split)
Is this a business rule? Give it one named home now
Third occurrence of the same concept? Extract with confidence

Production example

In our [invoicing platform], the overdue rule (due date + [14] days, excluding disputed and paid invoices) was coded inline in [four] places: the dashboard, the reminder job, a report and the public API. A policy change to exclude disputed invoices updated three of them, and the reminder job started emailing customers about disputed invoices, which generated complaints. Searching for the grace-day constant found all four copies. I created InvoicePolicy.IsOverdue(invoice, today) with focused unit tests and replaced every copy. The next policy change was a one-line edit in one place, and we added a review checklist item: business rules live in named policies, not inline conditions.

🔍 What the interviewer is checking

Checks whether the candidate understands DRY as a principle about knowledge rather than a mechanical rule.

Green flags

  • Defines DRY in terms of knowledge and a single source of truth.
  • Distinguishes true from accidental duplication.
  • Mentions the wrong abstraction or the rule of three.
  • Gives a concrete bug caused by duplicated rules.

Red flags

  • "Never write the same line twice."
  • Shares domain code between microservices without mentioning coupling.

📚 Learn in depth

1. Origin and scope

Andy Hunt and Dave Thomas defined DRY in The Pragmatic Programmer (1999) and explicitly applied it beyond code: database schemas, build scripts, tests and documentation. They later stressed that DRY is about knowledge and intent, not textual sameness.

2. Types of duplication (from the book)

Type Example Remedy
Imposed Schema in SQL and in C# by hand Generate one from the other (EF migrations, scaffolding, source generators)
Inadvertent Two developers implement the same rule unknowingly Discoverable policies, code search, shared vocabulary
Impatient Copy-paste to save time Extract when it's the same knowledge
Inter-developer Teams duplicate utilities Ownership and internal packages for genuinely shared, stable code

3. The wrong abstraction

Sandi Metz's sequence:

  1. Duplication exists; someone extracts a shared method.
  2. A new caller needs something slightly different; a parameter is added.
  3. Repeat until the method is a maze of flags and conditionals.

The fix is counter-intuitive: inline the shared code back into each caller, delete what each caller doesn't use, and then re-extract only what's genuinely common. "Duplication is far cheaper than the wrong abstraction."

4. Rule of three

First time, write it. Second time, notice the duplication (maybe accept it). Third time, you now see the real shape of the shared concept and can extract it with the right parameters. This balances DRY against premature abstraction.

5. DRY across service boundaries

Shared Effect
Domain library (entities, rules) across services Coupled releases; changes need coordinated deploys
Contracts (OpenAPI, protobuf, AsyncAPI) Services stay independent; clients generated from the contract
Small, stable technical packages (logging setup, auth helpers) Reasonable, versioned like any library

Within a service, DRY strongly; between services, prefer duplication of DTOs over shared domain code.

6. DRY vs DAMP in tests

Tests are read far more than they're changed together. "Descriptive And Meaningful Phrases" means each test should be understandable on its own. Use test data builders for genuinely repeated setup, but avoid deep base classes and helper chains that hide what a test does.

7. Try it yourself

  1. Search your codebase for a meaningful literal (0.1m, 14, "Overdue") and list each use.
  2. For each, decide: same knowledge, or coincidence?
  3. Move true duplicates into one named policy with tests; leave coincidences.
  4. Find a shared helper with boolean parameters; try inlining it into callers and re-extracting.
  5. Check whether your schema is defined twice (SQL scripts and EF model) and pick one source.

💻 Code

var invoices = new[]
{
    new Invoice(1, new DateOnly(2026, 9, 1), Disputed: false, Paid: false),
    new Invoice(2, new DateOnly(2026, 9, 1), Disputed: true, Paid: false),
    new Invoice(3, new DateOnly(2026, 10, 20), Disputed: false, Paid: false)
};

var today = new DateOnly(2026, 10, 23);

// Dashboard, reminder job and report all ask the same policy.
foreach (var inv in invoices.Where(i => InvoicePolicy.IsOverdue(i, today)))
    Console.WriteLine($"Invoice {inv.Id} is overdue");

public sealed record Invoice(int Id, DateOnly DueDate, bool Disputed, bool Paid);

// One authoritative representation of the 'overdue' rule.
public static class InvoicePolicy
{
    public const int GraceDays = 14;

    public static bool IsOverdue(Invoice invoice, DateOnly today) =>
        !invoice.Paid &&
        !invoice.Disputed &&
        today > invoice.DueDate.AddDays(GraceDays);
}

Accidental duplication that should stay separate:

// Similar today, but customer and supplier rules are owned by different teams
// and will diverge (suppliers need an ABN check, customers do not).
public static class CustomerAddressRules
{
    public static bool IsValid(string postcode) => postcode.Length == 4 && postcode.All(char.IsDigit);
}

public static class SupplierAddressRules
{
    public static bool IsValid(string postcode) => postcode.Length == 4 && postcode.All(char.IsDigit);
}

⚠ Common mistakes

1. Treating similar code as a DRY violation

What happens: any two similar blocks are merged. Why it is wrong: similar text isn't the same knowledge; merging couples independent features. Do instead: ask whether the copies must always change together.

2. Shared helpers growing flags

What happens: Validate(address, isSupplier: true, skipPostcode: false). Why it is wrong: the wrong abstraction: harder to change than the original copies. Do instead: inline back into callers, then re-extract only what's truly common.

3. Sharing domain libraries across microservices

What happens: a Company.Domain package used by every service. Why it is wrong: coupled releases; a distributed monolith. Do instead: share contracts; accept duplicated DTOs.

4. Leaving short business rules duplicated

What happens: "it's just one line, I'll copy it". Why it is wrong: short rules drift silently and cause the worst bugs. Do instead: give every business rule one named home.

5. Extracting after the second occurrence by reflex

What happens: abstractions designed from two examples. Why it is wrong: the shape is a guess and often wrong. Do instead: wait for the third, unless it's clearly the same business rule.

6. Over-DRY tests

What happens: tests built on deep base classes and shared setup helpers. Why it is wrong: individual tests become unreadable; failures are hard to diagnose. Do instead: DAMP tests with builders for genuinely repeated setup.

7. Duplicating schema by hand

What happens: SQL scripts and EF model both edited manually. Why it is wrong: they drift; production schema doesn't match the model. Do instead: one source of truth (migrations or scaffolding).

8. Magic strings for states

What happens: "Overdue", "overdue", "OVERDUE" across code. Why it is wrong: typos and inconsistent comparisons. Do instead: enums or constants.

9. A giant Common project

What happens: every utility goes into one shared project. Why it is wrong: low cohesion; every project depends on everything. Do instead: put shared code with the concept it belongs to; keep shared packages small and focused.

10. Copying documentation of behaviour into many places

What happens: the same rule explained in wiki, README and comments. Why it is wrong: the explanations drift from the code. Do instead: document why once; let tests and code document what.

🏛 Senior insight: trade-offs

Seniors apply DRY to knowledge and tolerate duplication of incidental code. The cost of duplication grows with the number of copies and the importance of the rule; the cost of a premature abstraction grows with every caller.

Situation Choice
Business rule in several places Merge now
Two similar handlers for different features Leave, revisit at the third
DTOs across service boundaries Duplicate; share the contract
Test arrange code Builders for real repetition; prefer readable tests
Shared helper with many flags Inline and re-extract

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions waiting before abstracting Why: Probes YAGNI and premature abstraction
    How to answer

    YAGNI (You Aren't Gonna Need It, from Extreme Programming) says do not build a capability until you actually need it. Speculative features cost time to build, delay what is needed now, add code to maintain, and are often wrong when the real requirement arrives. YAGNI does not mean skipping tests or refactoring; it is those practices that make adding the feature later cheap. I make an exception for decisions that are expensive to reverse, such as public API contracts, persisted data formats and security, where some upfront design pays off.

  2. When: Candidate warns about the wrong abstraction Why: Checks preference for simple designs
    How to answer

    KISS (Keep It Simple) means choosing the simplest design that meets the actual requirements, so the code is easy to read, test and change. The useful distinction is between essential complexity (the problem really is hard, such as tax rules) and accidental complexity (layers, patterns and frameworks we added). I also separate simple (few interleaved concepts) from easy (familiar or quick to write): copying a framework template is easy but may not be simple. In practice it means a plain class before an abstraction, a direct EF Core query before a generic repository, and readable code over clever LINQ.

  3. When: Candidate talks about sharing code across services Why: Probes coupling costs of shared libraries
    How to answer

    Coupling is the degree to which one module depends on another: how much it must know about it, and how likely it is to change when the other changes. Some coupling is unavoidable; the goal is coupling that is explicit, narrow and stable. In code the harmful kinds are reaching into another type's internals, shared mutable global state and passing control flags. In distributed .NET systems the expensive kinds are temporal coupling (synchronous call chains), shared databases and lock-step deployments. I reduce them with narrow interfaces, published contracts, asynchronous messaging where it fits, and each service owning its data.

🛠 Real-world problems: how you handle them

Overdue rule duplicated in four places
Situation
An [invoicing platform] had the overdue rule coded in the dashboard, reminder job, report and API.
Signals
Customer complaints about reminder emails for [disputed] invoices after a policy change.
Investigation
Searching for the grace-day constant found [four] implementations; only three had been updated.
Resolution
Created `InvoicePolicy.IsOverdue` and replaced all copies; added unit tests for the policy.
Prevention
Code review checklist item: business rules must live in a named policy, not inline.

How to tell it (STAR)

After a policy change, our reminder job started chasing customers with disputed invoices. I found the overdue rule written in [four] places and only three had been updated. I moved it into one policy class with tests, and every consumer now calls it, so the next rule change was a single edit.

↩ Usually asked after

More in Additional Principles

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me