Design Patterns & Practices  

DRY Principle in C#: Don't Repeat Yourself - A Practical Guide With Real Code Examples

What DRY Actually Means

Ask ten developers what DRY means and nine will say some version of "don't copy-paste code." That answer is close enough to sound right in a standup and wrong enough to cause real bugs in production — because the actual rule isn't about copy-pasting at all.

DRY = Don't Repeat Yourself. Here's the more accurate version, and the one worth actually internalizing:

Every piece of knowledge in your system should exist in exactly one place.

That distinction matters more than it sounds like it should. Two pieces of code can look completely different and still violate DRY if they encode the same business rule in two places. And two pieces of code can look nearly identical and not violate DRY at all, if they represent two genuinely unrelated rules that just happen to look the same today.

We'll get to that second case — it's where most developers overcorrect.

Violates DRY: The Same Rule, Copied Twice

Here's a leave-eligibility rule, living in two different places in the same codebase:

// LeaveRequestsController.cs
[HttpPost]
public async Task<IActionResult> Create(CreateLeaveRequestDto dto)
{
    var employee = await _employeeRepository.GetByIdAsync(dto.EmployeeId);

    bool isEligible = employee.ProbationEndDate <= DateTime.UtcNow
        && employee.EmploymentStatus == "Active";

    if (!isEligible)
        return BadRequest("Employee is not eligible to apply for leave.");

    // ... create the leave request
    return Ok();
}


// LeaveAutoApprovalJob.cs — a nightly background job
public async Task ProcessPendingRequestsAsync()
{
    foreach (var request in await _leaveRepository.GetPendingRequestsAsync())
    {
        var employee = await _employeeRepository.GetByIdAsync(request.EmployeeId);

        bool isEligible = employee.ProbationEndDate <= DateTime.UtcNow
            && employee.EmploymentStatus == "Active";

        if (!isEligible)
        {
            request.Reject("Employee is not eligible.");
            continue;
        }

        request.Approve();
    }
}

Nobody copy-pasted this maliciously — it's a completely natural thing to happen when two different files need the same check and writing it inline feels faster than going looking for a shared place to put it.

The cost shows up later: when the rule changes, there are now two places to remember, and only one of them is in front of the person making the change.

Follows DRY: One Source of Truth

// Domain/Entities/Employee.cs
public enum EmploymentType
{
    FullTime,
    PartTime,
    Contractor
}

public class Employee
{
    public DateTime ProbationEndDate { get; init; }
    public string EmploymentStatus { get; init; } = "Active";
    public EmploymentType EmploymentType { get; init; }

    public bool IsEligibleForLeave =>
        ProbationEndDate <= DateTime.UtcNow
        && EmploymentStatus == "Active"
        && EmploymentType != EmploymentType.Contractor;
}

Both call sites shrink to one line:

if (!employee.IsEligibleForLeave)
    return BadRequest("Employee is not eligible to apply for leave.");
if (!employee.IsEligibleForLeave)
{
    request.Reject("Employee is not eligible.");
    continue;
}

A new rule — say, contractors don't get paid leave — is now a one-line change to IsEligibleForLeave, and both the Controller and the background job pick it up automatically because there's only one place the rule actually lives.

DRY Isn't Just About Logic — Magic Strings Count Too

A quieter version of the same problem: values, not logic, scattered across the codebase.

// Violates DRY — the same status string, typed by hand, repeatedly
if (request.Status == "Pending") { /* ... */ }

var pendingCount = await _context.LeaveRequests
    .CountAsync(r => r.Status == "pending"); // casing mismatch — silently returns 0

await _context.LeaveRequests
    .Where(r => r.Status == "Pending")
    .ToListAsync();

That casing mismatch isn't hypothetical — it's exactly the kind of bug that happens when "Pending" exists as a string in five different files instead of one declared value.

// Follows DRY
public enum LeaveRequestStatus
{
    Pending,
    Approved,
    Rejected
}

Now the compiler — not a human remembering the exact casing — enforces consistency.

The same applies to configuration values like tax rates or thresholds: a 0.18m hardcoded in three different calculators should instead live once in appsettings.json, read through the IOptions<T> pattern.

When DRY Goes Wrong

This is the part most "DRY" advice skips, and it's the part senior engineers actually get asked about in interviews: merging code that looks the same but isn't the same thing.

public bool IsValidLeaveRequest(LeaveRequest request) =>
    request.StartDate <= request.EndDate && request.NumberOfDays > 0;

public bool IsValidTrainingRequest(TrainingRequest request) =>
    request.StartDate <= request.EndDate && request.NumberOfDays > 0;

These two methods are identical today. It's tempting to "DRY them up" into one shared IsValidDateRange(start, end, days) method used by both.

Do that, and watch what happens six months later when training requests get a new rule: minimum 3 days' notice before the training start date. Leave requests have no such rule.

Now the "shared" method needs a flag:

// What premature DRY-ing leads to
public bool IsValidDateRange(DateTime start, DateTime end, int days, bool requiresAdvanceNotice)
{
    if (start > end || days <= 0) return false;
    if (requiresAdvanceNotice && start < DateTime.UtcNow.AddDays(3)) return false;
    return true;
}

That bool requiresAdvanceNotice parameter is the tell.

The two validations were never actually the same rule — they just looked the same because neither had diverged yet.

Merging them created a false coupling, and now every future change to either request type risks affecting the other through a method that's pretending to be one thing while doing two.

The better move: keep them separate, or extract only the part that's genuinely shared — the mechanical "is this a valid date range" check — as its own small concept, while each request type keeps its own business rule on top.

public readonly record struct DateRange(DateOnly Start, DateOnly End)
{
    public bool IsValid => Start <= End;
    public int NumberOfDays => End.DayNumber - Start.DayNumber + 1;
}

public bool IsValidLeaveRequest(LeaveRequest request) =>
    request.Range.IsValid && request.Range.NumberOfDays > 0;

public bool IsValidTrainingRequest(TrainingRequest request) =>
    request.Range.IsValid
    && request.Range.NumberOfDays > 0
    && request.Range.Start >= DateOnly.FromDateTime(DateTime.UtcNow.AddDays(3));

DateRange captures the one piece of knowledge that's genuinely shared — what makes a date range valid.

Each request type still owns its own business rule on top, with no flag, no branch, and no false coupling.

This is the line worth holding onto:

DRY duplicated knowledge, not duplicated-looking code.

Some engineers summarize the corrective as "prefer duplication over the wrong abstraction" — it's not a formally attributed law, but it's the right instinct.

How DRY Relates to SOLID

DRY and the Single Responsibility Principle reinforce each other in a direct way: a method or class that does exactly one job is far less likely to get duplicated elsewhere because there's nowhere else for that one job to sensibly live.

Most DRY violations in real codebases trace back to logic that didn't have an obvious home — which is itself usually a sign that responsibilities weren't clearly separated in the first place.

And just like the SOLID series cautioned against treating SOLID as a checklist, the same discipline applies here:

DRY is a tool for eliminating duplicate sources of truth, not a mandate to eliminate every visually similar block of code you see.

Quick Reference

Violation TypeExampleFix
Duplicated business ruleSame eligibility check in a Controller and a background jobMove the rule onto the entity (e.g., Employee.IsEligibleForLeave)
Duplicated magic strings"Pending" typed in five places, one with a casing typoUse an enum
Duplicated config values0.18m hardcoded in three calculatorsCentralize in configuration, read via IOptions<T>
False duplication (the trap)Two validation methods that look identical todayKeep them separate, or extract only the genuinely shared mechanic

Bonus: Interview Questions That Go Beyond "What Does DRY Stand For"

Is DRY About Code or About Knowledge? What's the Difference?

DRY is about knowledge.

Two blocks of code can look totally different in syntax and still violate DRY if they encode the same business decision in two places — and two blocks can look nearly identical in syntax and not violate DRY at all if they represent two unrelated rules that simply haven't diverged yet.

Interviewers ask this specifically to see if you'll default to the surface-level "don't repeat code" answer.

Can You Give an Example Where Applying DRY Made Code Worse?

Yes — merging two validation methods (or two similar-looking calculations) that represent different business rules into one shared method "to avoid duplication."

The moment one of the two rules changes independently, the shared method needs a conditional flag to handle the divergence, and now unrelated logic is coupled together through that flag.

The fix often isn't "undo DRY" — it's recognizing the original duplication wasn't knowledge duplication to begin with.

How Is DRY Different from Refactoring Duplicate Code into a Shared Utility Method That Turns Out to Be Wrong Six Months Later?

The DRY principle itself didn't cause that problem — premature abstraction did.

DRY says "don't duplicate knowledge." It doesn't say "merge anything that looks similar before you're sure it's actually the same thing."

A senior-level answer distinguishes between the principle and a specific bad application of it.

What's the Relationship Between DRY and the Single Responsibility Principle?

A class or method with a single, well-defined responsibility tends to have one obvious home for its logic, which makes it far less likely that the same logic gets reimplemented elsewhere out of convenience.

Conversely, when responsibilities are scattered or unclear, developers often re-derive logic locally because finding (or trusting) the "real" implementation elsewhere isn't obvious.

The two principles support each other rather than overlapping.

When Should Two Pieces of Identical-Looking Code NOT Be Merged?

When they represent different business concepts that simply haven't diverged yet — different request types, different customer tiers, different regulatory regions — where a future, foreseeable change to one has no reason to affect the other.

The test worth stating out loud in an interview:

"If I changed the business rule behind one of these tomorrow, would I want the other one to change too?"

If the honest answer is no, they were never duplicated knowledge — just a coincidence.

The Takeaway

DRY isn't "never write similar code twice."

It's "never let the same business decision live in two places where one of them can quietly fall out of sync."

SOLID, DRY, KISS, and YAGNI all pull in slightly different directions by design — over-apply any one of them and you create a different problem than the one you started with.

The actual skill isn't memorizing the acronyms. It's recognizing, in a specific piece of code, which principle is being violated worse right now — and that's a judgment call worth a second opinion in code review whenever it's genuinely unclear, not something a linter can settle for you.

Summary

DRY is fundamentally about maintaining a single source of truth for business knowledge, not eliminating every instance of similar-looking code. True DRY violations occur when the same business rule, configuration value, or decision is duplicated across multiple locations. However, forcing unrelated concepts into shared abstractions can create tighter coupling and future maintenance problems. Effective use of DRY requires distinguishing between duplicated knowledge and coincidental code similarity, while balancing it with principles like SRP, KISS, and YAGNI to build maintainable and adaptable software systems.