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

📚 Interview question

When is code duplication acceptable or even preferable, and how do you explain that decision in a code review?

Rule of three, the wrong abstraction, duplication across bounded contexts and services, test code, and how to justify keeping duplication to reviewers.

Senior Architecture Advanced code-review design-judgment dry duplication

🎯 Key points

  • Duplication is acceptable when the copies represent different knowledge that may diverge
  • Prefer duplication over the wrong abstraction (Sandi Metz)
  • Rule of three: wait for a third occurrence before extracting
  • Across bounded contexts and microservices, duplicating models avoids coupling
  • Tests can repeat setup for readability (DAMP over DRY)
  • Explain the decision in review with ownership, change reasons and coupling cost

⏱ 30-second answer

Duplication is acceptable when the copies are coincidentally similar but represent different knowledge, so they may change independently. Merging them creates the wrong abstraction: a shared helper that gains a flag for each caller. I also accept duplication across bounded contexts and microservices, where a shared model would couple teams and deployments, and in tests, where readable, self-contained cases beat clever shared setup. In review I justify it with three facts: who owns each copy, whether they change for the same reason, and what coupling a shared version would add.

🗣 Full answer (senior)

How I would answer this in an interview

"Duplication is acceptable, and sometimes better, when the copies are coincidentally similar but represent different knowledge, so they'll change for different reasons, often for different owners. Merging those creates what Sandi Metz calls the wrong abstraction: a shared helper that gains a boolean flag for every caller, until a change for one feature breaks another. 'Duplication is far cheaper than the wrong abstraction.' I also accept duplication across bounded contexts and microservices, where a shared domain model or library would couple teams and deployments; there I share contracts, not code. In tests I'll take readable, self-contained cases (DAMP) over clever shared setup. And early in a feature I follow the rule of three, because two examples rarely show the right abstraction. What I never duplicate is business rules, security checks and contracts that must stay in sync. In review I justify the decision with facts rather than taste: who owns each copy, whether they change for the same reason, what coupling a shared version would add, and what drift would cost. I leave a short comment so nobody 'fixes' it later."


1. When duplication is acceptable or preferable

Context Why duplication wins
Two features that look alike today Different reasons to change; merging couples them
Different owners/teams Shared code forces cross-team coordination for every change
Bounded contexts (Sales Customer vs Support Customer) Same word, different meaning and rules
Microservices Shared domain libraries force lock-step releases
Tests Each test should read top-to-bottom on its own (DAMP)
Early in a feature Too few examples to see the real abstraction
Measured hot paths A specialised copy can avoid allocations or virtual calls a generic version needs (only with benchmark evidence)
Integration adapters per vendor Each vendor's quirks evolve independently

2. When it's never acceptable

Duplicated Risk
Pricing, tax, eligibility, overdue rules Wrong charges, inconsistent decisions
Authorisation and security checks One missed copy exposes data
Contracts/schemas that must match Silent drift breaks integrations
Constants with business meaning Inconsistent behaviour after changes

3. The wrong abstraction cycle (and the fix)

  1. Programmer A sees duplication and extracts a shared method.
  2. Programmer B needs something slightly different and adds a parameter.
  3. Repeat until the method is a maze of flags and conditionals nobody fully understands.

Fix: inline the shared code back into each caller, delete what each caller doesn't use, then re-extract only what's genuinely common (often a small value object or pure function).

4. Deciding: a quick cost model

Factor Favours merging Favours duplication
Reason to change Same Different
Owner Same team Different teams
Number of copies Many (3+) Two
Coupling a shared version adds Low (same module) High (across services/teams)
Cost of drift High (money, security, contracts) Low (formatting, wording)
Shared version needs flags? No Yes

5. Explaining it in a code review

"These two validators look identical, but onboarding owns one and billing owns the other, and billing is adding ABN checksum validation next sprint. A shared version would need a isSupplier flag on day one and would couple two teams' releases. I'd rather keep them separate, leave a comment saying why, and revisit if a third appears. The actual shared knowledge, the Postcode value object, is already shared."

The structure: ownership, change reasons, coupling cost, what is shared, when we'll revisit.

Production example

We had one AddressFormatter shared by invoicing, shipping labels and marketing emails. Over [two years] it accumulated [11] boolean parameters, and a shipping-label change (upper case for the carrier) shipped to production and broke invoice layouts. When I lined up the callers, they shared almost nothing except the address data itself. Each used a different flag combination. I split it into three context-owned formatters (ShippingLabel, InvoiceAddress, MarketingAddress) and kept the genuinely shared knowledge in an Address value object with validation. Each team now changes its format independently, and we added a review guideline that shared helpers with boolean flags are a smell, plus comments on deliberate duplication so nobody re-merges them.

🔍 What the interviewer is checking

Checks nuanced judgement and communication in reviews.

Green flags

  • Distinguishes duplicated knowledge from similar code.
  • Mentions the wrong abstraction and rule of three.
  • Uses bounded contexts or service boundaries as reasons.
  • Explains how they would justify the decision to reviewers.

Red flags

  • Treats all duplication as bad, or all as fine.
  • Shares domain libraries across services by default.

📚 Learn in depth

1. Sources

Idea Source
"Duplication is far cheaper than the wrong abstraction" Sandi Metz, The Wrong Abstraction (2016)
Rule of three Don Roberts, via Martin Fowler's Refactoring
Bounded contexts Eric Evans, Domain-Driven Design
DAMP tests Common testing guidance (e.g. Google testing blog)
AHA ("Avoid Hasty Abstractions") Kent C. Dodds

2. Bounded contexts

In DDD the same word means different things in different contexts:

Context Customer means Cares about
Sales A lead/account Pipeline stage, owner, discounts
Support Someone with tickets SLA tier, open cases
Billing A payer ABN, payment terms, credit limit

A single shared Customer class becomes a god object every team edits. Separate models linked by IDs and events (CustomerRegistered) duplicate a few fields but keep each context free to evolve.

3. Across microservices

Shared Effect
Domain library (entities, rules) Coupled releases; a "distributed monolith"
DTO classes in a NuGet package Every contract change requires upgrading all consumers
Contract (OpenAPI/protobuf/AsyncAPI) + generated clients Independent evolution; consumers regenerate when they choose

Duplicated DTOs per consumer are fine; tolerant readers let producers add fields.

4. Tests: DAMP over DRY

DAMP (Descriptive And Meaningful Phrases) means a test should be understandable in isolation: the reader sees the arrange, act and assert without jumping into base classes. Some repeated arrange lines are a fair price. When setup is genuinely complex, use test data builders (new OrderBuilder().WithLines(3).Paid().Build()), which remove noise without hiding intent. Avoid deep test base classes with hidden setup.

5. Performance-motivated duplication

In measured hot paths, a specialised copy (e.g. a Span<byte>-based parser for one format instead of a generic configurable parser) can avoid allocations, virtual dispatch or branching. Only do this with benchmark evidence (BenchmarkDotNet) and a comment linking to it, because it's still a maintenance cost.

6. Making deliberate duplication visible

// Deliberately separate from SupplierAddressRules: owned by Onboarding, diverges for ABN checks.
// See ADR-0042. Revisit if a third address-rules variant appears.
public static class CustomerAddressRules { ... }

A comment or ADR prevents a well-meaning refactor from re-coupling them.

7. Try it yourself

  1. Find a shared helper with boolean parameters; list callers and the flags each passes.
  2. Inline it into each caller and remove unused branches; compare readability.
  3. Re-extract only what's truly common (often a value object).
  4. Find a shared DTO package across services and check how many consumers had to upgrade for its last change.
  5. Rewrite one test that depends on a base class into a self-contained DAMP test with a builder.

💻 Code

// The wrong abstraction: one formatter, a flag per caller.
public static class AddressFormatter
{
    public static string Format(Address a, bool forLabel, bool includeCountry, bool upperCase)
    {
        var text = forLabel
            ? $"{a.Line1}\n{a.Suburb} {a.State} {a.Postcode}"
            : $"{a.Line1}, {a.Suburb} {a.State} {a.Postcode}";
        if (includeCountry) text += forLabel ? "\nAUSTRALIA" : ", Australia";
        return upperCase ? text.ToUpperInvariant() : text;
    }
}

// Deliberate duplication: each context owns its format and can change it alone.
public static class ShippingLabel
{
    // Australia Post style: upper case, suburb line separate.
    public static string Format(Address a) =>
        $"{a.Line1}\n{a.Suburb} {a.State} {a.Postcode}".ToUpperInvariant();
}

public static class InvoiceAddress
{
    public static string Format(Address a) => $"{a.Line1}, {a.Suburb} {a.State} {a.Postcode}";
}

// Shared knowledge stays shared: the Address value object itself.
public sealed record Address(string Line1, string Suburb, string State, string Postcode);

public static class Demo
{
    public static void Main()
    {
        var a = new Address("1 George St", "Sydney", "NSW", "2000");
        Console.WriteLine(ShippingLabel.Format(a));
        Console.WriteLine(InvoiceAddress.Format(a));
    }
}

⚠ Common mistakes

1. Extracting after the first repetition

What happens: shared code created from two examples. Why it is wrong: two examples rarely reveal the right abstraction. Do instead: wait for a third and identify the true common knowledge.

2. Sharing domain models across contexts or services

What happens: one Customer class or domain NuGet package for everyone. Why it is wrong: every team's change ripples; deployments couple. Do instead: separate models linked by IDs, contracts and events.

3. Duplicating business or security rules

What happens: "it's only a few lines" copied into several services. Why it is wrong: copies drift: wrong charges, exposed data. Do instead: one authoritative place.

4. Leaving deliberate duplication unexplained

What happens: no comment or ADR. Why it is wrong: someone "fixes" it later and reintroduces coupling. Do instead: a short comment stating why the copies are separate.

5. Keeping a flag-laden shared method out of sunk cost

What happens: "we spent effort building it". Why it is wrong: the wrong abstraction keeps getting worse. Do instead: inline and re-extract.

6. Arguing duplication from taste in review

What happens: "I prefer it this way". Why it is wrong: reviewers can't evaluate it; debates become personal. Do instead: argue with ownership, change reasons, coupling and drift cost.

7. Over-DRY tests

What happens: deep base classes and shared fixtures hide setup. Why it is wrong: failing tests are hard to understand. Do instead: DAMP tests with builders for complex objects.

8. Performance duplication without evidence

What happens: specialised copies "for speed". Why it is wrong: maintenance cost without proven benefit. Do instead: benchmark first; link evidence in a comment.

9. Treating all duplication as fine

What happens: copy-paste everywhere in the name of decoupling. Why it is wrong: true duplication of knowledge causes drift bugs. Do instead: separate knowledge from incidental similarity.

10. Merging across team boundaries without asking owners

What happens: a refactor unifies two teams' code. Why it is wrong: creates coordination cost the owners didn't agree to. Do instead: involve owners; consider whether changes truly align.

🏛 Senior insight: trade-offs

Seniors judge duplication by coupling and change patterns, not by how text looks. I am strict about duplicated rules and relaxed about duplicated structure.

Duplicated thing My stance
Pricing, tax, eligibility rules Never duplicate
Authorisation checks Never duplicate; centralise in policies
DTOs across services Duplicate; share the contract
Formatting for different audiences Duplicate
Test setup Duplicate for clarity; builders when complex
Generic algorithm used three times Extract

🔗 Follow-ups the interviewer may ask

  1. When: Candidate separates knowledge from similar code Why: Checks DRY understanding
    How to 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.

  2. When: Candidate mentions the wrong abstraction Why: Probes when not to abstract
    How to answer

    I do not abstract when there is no second variant, no boundary to protect and no testing need, because an abstraction costs indirection, a name, a contract and cognitive load. Typical examples are an IOrderService with one implementation used by one controller, a generic repository over EF Core, or a wrapper around HttpClient or ILogger<T>, which are already abstractions. I do abstract at architecture boundaries (ports for payment vendors, email, storage) and where tests need a seam for something slow or non-deterministic. If an abstraction has had one implementation and one caller for a long time and is not at a boundary, I inline it.

  3. When: Candidate avoids shared models across services Why: Probes coupling costs
    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

AddressFormatter with eleven flags
Situation
A shared `AddressFormatter` served [invoicing, shipping labels and marketing emails].
Signals
[11] boolean parameters; a shipping label change broke invoice layouts in production.
Investigation
Each caller used a different flag combination; almost no logic was truly shared.
Resolution
Split into three context-owned formatters with a shared `Address` value object.
Prevention
Review guideline: shared helpers with boolean flags are a smell; deliberate duplication documented.

How to tell it (STAR)

We had one address formatter with [11] flags, and a shipping change broke invoices. When I lined up the callers, they shared almost nothing except the address itself. I split it into three formatters owned by each team and kept a shared address type, and since then each team changes its format without breaking the others.

More in Senior-Level Judgment

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me