Skip to main content
CleanCodeMastery

Consolidate Duplicate Conditional Fragments: Move the Dessert Counter Outside

Learn the Consolidate Duplicate Conditional Fragments refactoring with a canteen story, TypeScript and C# examples, safety rules, and easy step-by-step practice.

21 min read Updated June 11, 2026beginner
refactoringsimplifying conditionalsconsolidate duplicate conditional fragmentsduplicate codeclean codebranches

๐Ÿฎ The Story of the Dessert Counter

Lunchtime at Doon Valley Residential School in Dehradun. The bell rings at one o'clock, four hundred students pour into the dining hall, and the hall splits them into two food lines. Every student must pick one.

The veg line goes: roti counter, then dal counter, then paneer counter, and finally โ€” a dessert counter serving gulab jamun. Ananya, a class eight student and committed paneer loyalist, walks it every day.

The non-veg line goes: roti counter, then chicken curry counter, and finally โ€” a dessert counter serving gulab jamun. Her friend Kabir walks that one.

Wait. Read those two lines again. Both lines end at a dessert counter. The same dessert. Two counters, two serving staff, two trays of gulab jamun, two gas burners keeping them warm โ€” doing one identical job, ten metres apart.

The warden, Mr. Bisht, notices the waste during the annual audit. He stands at the back of the hall with his clipboard and watches both dessert counters spoon out identical gulab jamun to two separate queues. The fix is obvious the moment someone says it aloud: the dessert has nothing to do with the veg or non-veg choice. Every student gets gulab jamun no matter which line they walked. So why is the dessert counter inside each line?

Mr. Bisht rearranges the hall over a weekend. Veg line: roti, dal, paneer. Non-veg line: roti, chicken. Then both lines merge, and there โ€” after the merge point โ€” stands one dessert counter. One tray. One staff member, the cheerful Pratap, who now serves everyone. Every student still gets exactly the same plate as before. Nothing about anyone's lunch changed. Only the layout changed: the shared step moved outside the split.

Figure 1: Lunch as Ananya and Kabir experience it, before and after the rearrangement

And a real bug gets fixed as a bonus. Last term, the kitchen switched dessert to kheer on Mondays โ€” but only the veg line's counter got the notice. For three Mondays, Kabir got gulab jamun while Ananya got kheer, and nobody could explain why, least of all Kabir, who likes kheer better. With a single counter after the merge, such half-updates simply cannot happen. There is only one counter to inform.

Code does this constantly. An if/else splits the flow into branches, and somewhere along the way the same line gets copy-pasted into every branch โ€” a log statement, a save call, a counter update. That line is the dessert counter standing inside both lunch lines. The refactoring that moves it out is called Consolidate Duplicate Conditional Fragments.

๐Ÿ” What is Consolidate Duplicate Conditional Fragments?

Consolidate Duplicate Conditional Fragments is a refactoring where you find code that is identical in every branch of a conditional and move it outside the conditional, so it is written once and runs once.

The placement rule is wonderfully simple:

  • A fragment duplicated at the beginning of every branch moves to just before the conditional.
  • A fragment duplicated at the end of every branch moves to just after the conditional.
  • A fragment stuck in the middle of branches should first be slid to the top or bottom of each branch (only if that does not change behaviour), and then hoisted.

Martin Fowler's classic illustration is a pricing snippet: a special deal sets the total one way, the normal path another way, and both branches then call send(). The send() call does not depend on the deal at all โ€” so it moves below the if/else and appears once. In the second edition of Refactoring, this idea lives on under the broader technique Slide Statements combined with Extract Method, but the classic name describes the situation perfectly, and tools and courses still teach it by this name.

The deep idea is this: a conditional should contain only what actually differs between the paths. Anything shared by all branches is not part of the decision โ€” it is just standing in the wrong place, and its duplication is a copy of the Duplicate Code smell, hiding inside an if.

Figure 2: The placement rules and safety checks at a glance

College corner: compilers perform this exact transformation on your behalf, and the compiler literature gives it precise names. Moving a shared computation above a branch is code hoisting; moving it below the join point is code sinking. The optimiser reasons over the control flow graph (CFG): each straight-line run of statements is a basic block, a conditional creates a split, and the point where branches meet again is the join block. A statement can sink to the join block only if that block post-dominates both branches โ€” meaning every path through either branch must pass through it. That is the formal version of our "no early exits" rule: a return inside one branch breaks post-dominance, so the join block no longer lies on every path, and the move is illegal. When you check branches by eye before hoisting, you are computing post-dominance informally โ€” the same analysis, done with human attention instead of graph algorithms.

๐Ÿ’ก

One line to remember: if every branch does it, the conditional should not contain it. Shared first lines float up above the if; shared last lines sink down below it. The branches keep only their true differences.

โฐ When do we need it?

Look for these signals in everyday code:

  1. The same last line in every branch. A recordDispatch(order), a saveToDb(), a console.log(...) closing both the if and the else block. This is the most common form โ€” trailing duplication grows naturally because each programmer finishes their branch "completely" without looking at the other branch.
  2. The same first line in every branch. Both branches begin by reading the same setting or computing the same subtotal. That work belongs before the decision.
  3. Branches that look long but differ by only a line or two. Squint at the conditional. If the branches are 80 percent identical, hoisting the shared parts will reveal the tiny true difference โ€” and often the slimmed conditional then collapses into a ternary or qualifies for Decompose Conditional.
  4. A bug fixed in one branch but not the other. If your bug tracker shows "fixed for priority orders, still broken for standard orders" โ€” that is duplicated fragments drifting apart, exactly like the Monday kheer incident.
  5. Every case of a switch ending identically. The same rule applies to switch statements: a statement shared by all cases belongs after the switch.

This refactoring also chips away at Long Method: removing repeated lines shortens the method and makes the genuine logic denser and clearer.

When to hold back:

  • A branch exits early. If one branch returns or throws before reaching the trailing fragment, the fragment is not truly common โ€” moving it would change when it runs.
  • The fragments only look alike. total += fee(order) in one branch and total += fee(otherOrder) in another are different statements wearing similar clothes. Consolidating them creates a bug.
  • Order matters. If a branch modifies state that the fragment reads, hoisting the fragment above the conditional changes the sequence of operations. Verify before moving.

Two questions decide the urgency: how identical are the copies, and how often does that code change? Identical copies in frequently edited code are half-update bugs waiting for a release date.

Figure 3: Judge each duplicated fragment by sameness and how often the code changes

๐Ÿ‘€ Before and after at a glance

Here is a shipping function in TypeScript with classic trailing duplication:

interface Order {
  isPriority: boolean;
  totalCost: number;
  carrier: string;
}
 
// BEFORE: recordDispatch is copied into both branches
function ship(order: Order): void {
  if (order.isPriority) {
    order.totalCost += expressFee(order);
    order.carrier = pickFastCarrier(order);
    recordDispatch(order);
  } else {
    order.totalCost += standardFee(order);
    order.carrier = pickCheapCarrier(order);
    recordDispatch(order);
  }
}

recordDispatch(order) runs no matter which branch is taken. It is the dessert counter. Move it below the split:

// AFTER: the conditional holds only what truly differs
function ship(order: Order): void {
  if (order.isPriority) {
    order.totalCost += expressFee(order);
    order.carrier = pickFastCarrier(order);
  } else {
    order.totalCost += standardFee(order);
    order.carrier = pickCheapCarrier(order);
  }
  recordDispatch(order);
}

One line shorter, yes โ€” but the real win is meaning. The conditional now answers exactly one question: how do priority and standard shipping differ? Fee and carrier. That's all. Dispatch recording is visibly universal, and it can never again be updated in one branch and forgotten in the other.

Figure 4: The shared step moves out of the branches to the merge point

Squint at the original ship and count: of the six statements inside the branches, two are copies of each other. A third of the conditional's body was never part of the decision at all.

Figure 5: How much of the branch code was really about the decision

๐Ÿชœ Step-by-step, the safe way

The move looks trivial, but careful programmers follow a checklist, because the traps (early exits, hidden ordering) are silent. Let us walk a slightly trickier example: duplication at the start and the end.

// STARTING POINT: leading AND trailing duplication
function prepareReport(term: Term): Report {
  let report: Report;
  if (term.isFinal) {
    const header = schoolHeader();             // duplicated first line
    report = buildFinalReport(term, header);
    stampDate(report);                          // duplicated last line
  } else {
    const header = schoolHeader();             // duplicated first line
    report = buildMidTermReport(term, header);
    stampDate(report);                          // duplicated last line
  }
  return report;
}

Step 1 โ€” Map the fragments. Read every branch and mark which lines are identical and where they sit. Here: const header = schoolHeader() heads both branches; stampDate(report) tails both branches. Write them down โ€” seriously, on paper if needed. Mis-identifying a "duplicate" is the main source of mistakes.

Step 2 โ€” Check the safety conditions.

  • Does the leading fragment depend on anything a branch computes before it? No โ€” it is the first line, nothing runs before it inside the branch. Safe to lift.
  • Does any branch exit early before the trailing fragment? No return/throw in either branch body. Safe to sink.
  • Do both copies operate on the same variables with the same meaning? Yes โ€” same header, same report. Truly identical.

Step 3 โ€” Hoist the leading fragment above the conditional. One move only:

// Step 3: INTERMEDIATE โ€” leading fragment lifted, trailing still duplicated
function prepareReport(term: Term): Report {
  const header = schoolHeader();
  let report: Report;
  if (term.isFinal) {
    report = buildFinalReport(term, header);
    stampDate(report);
  } else {
    report = buildMidTermReport(term, header);
    stampDate(report);
  }
  return report;
}

Run the tests. Green? Continue.

Step 4 โ€” Sink the trailing fragment below the conditional.

// Step 4: FINAL โ€” each fragment written once, branches show only the difference
function prepareReport(term: Term): Report {
  const header = schoolHeader();
  let report: Report;
  if (term.isFinal) {
    report = buildFinalReport(term, header);
  } else {
    report = buildMidTermReport(term, header);
  }
  stampDate(report);
  return report;
}

Run the tests again.

Step 5 โ€” Reassess the slimmed conditional. Each branch is now a single assignment, so the whole if/else can become a ternary:

// Step 5: BONUS polish โ€” the decision is now one readable line
function prepareReport(term: Term): Report {
  const header = schoolHeader();
  const report = term.isFinal
    ? buildFinalReport(term, header)
    : buildMidTermReport(term, header);
  stampDate(report);
  return report;
}

This cascade is typical: hoisting the shared lines shrinks the branches, and shrunken branches unlock the next simplification. Refactorings love to travel in groups.

โš ๏ธ

Run the tests after every hoist โ€” and especially watch early exits. The classic mistake: one branch contains a return midway, you sink the trailing fragment below the if, and now it runs on a path where it never ran before (or stops running where it did). Tests catch this instantly if you run them per step. If you batch five moves and test once, the failure will not tell you which move broke it.

College corner: the "order matters" safety check is a question of data dependence. Two statements can swap places only if neither writes a variable the other reads or writes โ€” no read-after-write, write-after-read, or write-after-write hazard between them. When you slide a middle fragment to the edge of its branch, you are asserting that it has no data dependence on the lines it slides past. Optimising compilers prove this with dependence analysis before they reorder instructions; you prove it by tracing each variable the fragment touches. If the fragment reads report and the line above it writes report, there is a read-after-write dependence and the slide is forbidden โ€” no matter how identical the copies look.

The whole checklist, as states you walk through per fragment:

Figure 6: The per-fragment safety procedure as a state machine

๐ŸŸ๏ธ A bigger real-life example

Let us code the whole dining hall. The "before" mirrors the two food lines exactly โ€” including the duplicated dessert counter, the duplicated tray-issue step, and the duplicated plate-count update:

interface Student {
  name: string;
  prefersVeg: boolean;
}
 
interface Plate {
  items: string[];
}
 
// BEFORE: tray, dessert, and counting duplicated inside both lines
function serveLunch(student: Student, stats: { plates: number }): Plate {
  let plate: Plate;
  if (student.prefersVeg) {
    plate = { items: [] };
    plate.items.push("roti");
    plate.items.push("dal");
    plate.items.push("paneer");
    plate.items.push("gulab jamun");   // dessert counter inside veg line
    stats.plates += 1;                  // counting inside veg line
  } else {
    plate = { items: [] };
    plate.items.push("roti");
    plate.items.push("chicken curry");
    plate.items.push("gulab jamun");   // dessert counter inside non-veg line
    stats.plates += 1;                  // counting inside non-veg line
  }
  return plate;
}

Count the duplicates: the empty tray (plate = ...) leads both branches; the roti is the second line of both; the dessert and the plate count tail both. The only true difference between the lines is dal-and-paneer versus chicken curry. Let us make the code say so:

// AFTER: lines split only where food actually differs
function serveLunch(student: Student, stats: { plates: number }): Plate {
  const plate: Plate = { items: [] };   // tray issued once, before the split
  plate.items.push("roti");             // roti counter serves everyone
 
  if (student.prefersVeg) {
    plate.items.push("dal");
    plate.items.push("paneer");
  } else {
    plate.items.push("chicken curry");
  }
 
  plate.items.push("gulab jamun");      // ONE dessert counter, after the merge
  stats.plates += 1;                     // counted once
  return plate;
}

Read the "after" top to bottom and it narrates the new dining hall: everyone takes a tray, everyone takes roti, the line splits only for the main course, the lines merge, everyone takes dessert, every plate is counted. The conditional has shrunk to the exact decision it represents. Follow Ananya's plate through the refactored hall:

Figure 7: One student's plate moving through the rearranged hall

Now the Monday kheer change is a one-line edit that cannot miss anyone:

// Monday special: change ONE line, both lines get kheer โ€” guaranteed
plate.items.push(isMonday() ? "kheer" : "gulab jamun");

If the dining hall logic keeps growing, the shared steps naturally become their own small services, and the structure of the hall becomes the structure of the code:

Figure 8: The hall layout as classes โ€” shared counters sit outside the line choice

๐Ÿ’ฌ The same refactoring in C# and Python

A shorter example, straight from the classic pricing shape:

// BEFORE: Send() duplicated at the end of both branches
void Checkout(Order order)
{
    if (order.HasSpecialDeal)
    {
        order.Total = order.Price * 0.95m;
        Send(order);
    }
    else
    {
        order.Total = order.Price * 0.98m;
        Send(order);
    }
}
 
// AFTER: the conditional decides only the discount; sending is universal
void Checkout(Order order)
{
    if (order.HasSpecialDeal)
        order.Total = order.Price * 0.95m;
    else
        order.Total = order.Price * 0.98m;
 
    Send(order);
}

And since each branch is now a single assignment, C# lets you finish with a ternary so the difference reads in one line:

void Checkout(Order order)
{
    order.Total = order.Price * (order.HasSpecialDeal ? 0.95m : 0.98m);
    Send(order);
}

From eight lines of branching to two honest lines: the discount factor is the decision, the send is the constant. Python tells the same story:

# BEFORE: send_order copied into both branches
def checkout(order):
    if order.has_special_deal:
        order.total = order.price * 0.95
        send_order(order)
    else:
        order.total = order.price * 0.98
        send_order(order)
 
# AFTER: the branch decides only the factor; sending happens once
def checkout(order):
    factor = 0.95 if order.has_special_deal else 0.98
    order.total = order.price * factor
    send_order(order)

College corner: this refactoring is the statement-level face of the DRY principle โ€” Don't Repeat Yourself โ€” from Hunt and Thomas's The Pragmatic Programmer. DRY is often misread as "never type similar text twice"; its real claim is that every piece of knowledge should have a single authoritative representation in the system. The dessert step is one piece of knowledge ("every plate gets dessert"); writing it in two branches creates two representations that the world expects to stay synchronised by luck. The Monday kheer bug is what the textbook calls a shotgun update gone wrong: one logical change requiring edits in multiple physical places, where missing any one place produces inconsistent behaviour rather than an error. Single representation turns that whole failure mode into a non-event โ€” which is why DRY violations are judged by how knowledge evolves, not by how text looks.

๐Ÿ› ๏ธ IDE support

This refactoring is mostly a careful cut-and-paste, but modern tools watch your back:

ToolHelp offered
IntelliJ IDEA / Rider / WebStormInspections flag "common part can be extracted from if" and offer one-click fixes via Alt+Enter
Visual Studio + ReSharper"Invert if", "merge", and duplicate-code analysis highlight identical fragments inside branches
SonarQube / SonarLintRules detect identical implementations in conditional branches and duplicated blocks
VS CodeNo dedicated action; rely on selection comparison and your test suite

The IntelliJ-family inspection is genuinely useful for beginners: it spots the duplication you stopped seeing because you wrote it. If the shared fragment is multiple lines, a good combo is Extract Method first (so the fragment becomes one named call in each branch), then hoist that single call. The extraction is automated and safe; the hoist becomes trivially easy to review.

โš–๏ธ Benefits and risks

Mr. Bisht's audit had hard numbers: two counters, two staff, two burners doing one job. Code audits read the same way. Every duplicated fragment doubles the places a change must touch โ€” and the gap between "places that need the edit" and "places that got the edit" is exactly where the kheer bugs live.

Figure 9: Places to edit when the shared step changes, before and after the merge
โœ… BenefitWhy it matters
Branches show only true differencesThe reader sees the real decision in seconds
One home for shared behaviourThe "fixed in one branch, broken in the other" bug class disappears
Shorter methodsEvery removed copy is a line the reader never re-reads
Unlocks further cleanupSlimmed branches often collapse to ternaries or guard clauses
โš ๏ธ RiskHow to handle it
Early exit in a branchA return/throw/break before the fragment makes sinking unsafe โ€” check every branch first
Hidden ordering dependenciesIf a branch writes state the fragment reads, hoisting changes behaviour โ€” trace the data
Look-alike fragmentsSame shape, different variables = not duplicates; consolidating them is a bug
Fragment must run per-branch in futureIf the lines are expected to diverge soon, leaving them separate may be honest

When not to do it: when the fragments are coincidentally identical today but represent different responsibilities that will evolve separately. Like consolidating conditions, consolidating fragments is a statement that "this is one thing" โ€” only make that statement if it is true.

๐Ÿงน Which smells does it cure?

Code smellHow this refactoring helps
Duplicate CodeDirectly removes the copies hiding inside conditional branches
Long MethodDeleting repeated lines shortens the method and clears the view

๐Ÿ“ฆ Quick revision box

+-----------------------------------------------------------------+
|   CONSOLIDATE DUPLICATE CONDITIONAL FRAGMENTS โ€” CHEAT SHEET     |
+-----------------------------------------------------------------+
| Signal  : the SAME line appears in EVERY branch of an if/switch |
| Rule    : leading duplicate  -> move BEFORE the conditional     |
|           trailing duplicate -> move AFTER the conditional      |
|           middle duplicate   -> slide to an edge first          |
| Safety  : no early exits past the fragment                     |
|           no branch-computed state the fragment depends on     |
|           fragments must be TRULY identical                    |
| Test    : run the suite after every single hoist               |
| Result  : branches contain only what truly differs             |
| Cures   : Duplicate Code, Long Method                          |
+-----------------------------------------------------------------+

โœ๏ธ Practice exercise

This notification function from a school app has duplication at the head and tail of the branches โ€” plus one trap. Clean it up:

// Careful: one of these branches has a trap!
function notifyResult(student: Student, passed: boolean): void {
  if (passed) {
    const contact = lookupParentContact(student);
    sendSms(contact, `${student.name} has passed. Congratulations!`);
    logNotification(student);
  } else {
    const contact = lookupParentContact(student);
    if (!contact.smsEnabled) {
      return; // parents opted out of bad-news SMS
    }
    sendSms(contact, `${student.name} needs a re-test. Please meet the teacher.`);
    logNotification(student);
  }
}

Your checklist:

  1. The leading fragment lookupParentContact(student) heads both branches. Is it safe to hoist above the if? (Check: does anything run before it in either branch?)
  2. The trailing fragment logNotification(student) tails both branches โ€” but wait. One branch has an early return before it. Is sinking it below the if safe? What would change?
  3. Decide: can you restructure the opt-out check so the trailing fragment becomes safely hoistable? (Hint: think about where the early exit could move once the contact lookup is hoisted.)
  4. College bonus: draw the control flow graph of this function. Mark the join block after the if/else and explain, in post-dominance terms, exactly why the early return blocks the sink.
  5. Run your tests after every move โ€” especially a test where smsEnabled is false.

If you spotted that the early return makes a blind hoist of logNotification incorrect, give yourself full marks โ€” that is exactly the judgement this refactoring trains. One counter, after the merge, only when every path truly walks past it. Keep practising!

Frequently asked questions

What does Consolidate Duplicate Conditional Fragments mean in simple words?
If the exact same line of code appears in every branch of an if/else, that line does not really belong to the decision. Move it out: code repeated at the start of every branch goes before the if, and code repeated at the end goes after the if. It then runs once, written once.
How is this different from Consolidate Conditional Expression?
They sound similar but fix different duplications. Consolidate Conditional Expression merges several CONDITIONS that lead to one result. Consolidate Duplicate Conditional Fragments moves duplicated ACTION code out of the branches of one conditional. One tidies the question; the other tidies the answers.
When is it unsafe to move a trailing fragment after the if?
Two main cases. First, if any branch can exit early with return, throw, or break, the moved line would run in situations where it previously did not, or not run where it previously did. Second, if a branch changes state that the fragment depends on, moving it changes the order of operations. Check both before hoisting.
The duplicated code is in the middle of each branch. What do I do?
First try to slide it to the top or bottom of each branch โ€” but only if swapping it past its neighbouring lines does not change the result. Once it sits at the same edge in every branch, hoist it out. If the fragment is several lines long, consider Extract Method first so you are moving one named call instead of a block.
Does this refactoring work with switch statements too?
Yes. If every case of a switch ends with the same statement, that statement can move below the switch. The same safety rules apply: no case may exit early past the fragment, and the fragment must not depend on case-specific state in a position-sensitive way.

Further reading

Related Lessons