Skip to main content
CleanCodeMastery

Switch Statements: The Receptionist With the Giant Rulebook

Learn the Switch Statements code smell with a school receptionist story, duplicated switch examples in TypeScript and C#, and the polymorphism cure.

27 min read Updated June 11, 2026beginner
code smellsoo abusersswitch statementsconditionalspolymorphismtypescriptrefactoring

๐Ÿซ The receptionist with the giant rulebook

Come with me to a big school in Pune. At the front gate sits Mrs. Kulkarni, the receptionist. She has worked here for twenty-two years. She knows every parent's scooter and every vendor's tempo. On her desk is a fat, heavy rulebook. Every time a visitor walks in, she opens it and reads:

  • If the visitor is a parent, give a blue slip, call the class teacher, and note the child's name.
  • If the visitor is a student who forgot the ID card, give a yellow slip, call the class monitor, and mark a late entry.
  • If the visitor is a book vendor, give a green slip, call the librarian, and check the delivery list.
  • If the visitor is a sports coach, give a white slip, call the PT teacher, and open the back gate.

That is just for entry. Turn the page, and there is a second list of the same four visitor types for parking: parents park on the left, vendors park near the store room, coaches park near the ground. Turn again โ€” a third list of the same four types for exit passes. The same four cases, written three times, in three chapters.

Now the principal makes an announcement at assembly. The school is starting an Alumni Cell, so a new visitor type is coming: alumni. Old students will visit for reunions and career talks. Poor Mrs. Kulkarni must now fix chapter one, chapter two, and chapter three of her rulebook. She sits late after school with a pen and correction fluid. She updates chapter one. She updates chapter two. The peon brings tea, she chats for five minutes, and she goes home thinking the job is done.

She missed chapter three.

Two weeks later, an old student named Vikram visits for the reunion. He gets his entry slip. He gets his parking spot. At 5 p.m. he tries to leave โ€” and the exit-gate guard flips to chapter three, finds no rule for "alumni", and refuses to let him out. There is a small drama at the gate. The principal hears about it. Mrs. Kulkarni feels terrible, and honestly, it is not her fault. The rulebook's design set her up to fail. Any system where one new idea must be written in three separate places will eventually miss a place.

The school's young computer teacher, Arjun sir, watches all this and says something interesting: "There is a smarter design, and some schools already use it. The receptionist asks just one question โ€” which department are you here for? โ€” and sends the visitor to that department. The library knows how to handle book vendors. The sports office knows how to handle coaches. Each department handles its own visitors. When alumni start visiting, the school opens one new Alumni Cell, and the receptionist's job does not change at all."

That fat rulebook with the same list repeated in three chapters is exactly the Switch Statements code smell. The "send them to the right department" design is polymorphism. Keep Mrs. Kulkarni, Vikram, and Arjun sir in your mind โ€” this whole post is their story, and we will write the rulebook in code soon.

๐Ÿ” What is this smell?

Switch Statements is one of the Object-Orientation Abuser smells. These smells appear when code is written inside classes but does not really use the powers of object-oriented programming โ€” inheritance, interfaces, and polymorphism. The objects are there, but they are not doing the work; a big conditional is doing the work for them.

Here is the plain definition. The smell is a switch (or an if-else-if ladder โ€” same thing, different clothes) that branches on some type code: an enum, a string like "parent" or "vendor", a number code, or a runtime type check. One such switch is usually fine. The smell becomes serious when the same switch is repeated in many places โ€” one switch picks the slip colour, another picks the parking spot, a third picks the exit rule, all branching on the identical set of cases.

This is such an important point that Martin Fowler, in the second edition of his book Refactoring, renamed this smell from "Switch Statements" to "Repeated Switches". He did this because the switch keyword itself was getting an unfair bad name. The crime is not switching once. The crime is switching on the same thing again and again, in different corners of the code โ€” exactly like the same visitor list copied into three chapters.

๐Ÿ’ก

One-line summary: the Switch Statements smell is the same type-based branching copy-pasted across the codebase, so every new type forces you to find and edit every copy โ€” when each type could simply carry its own behaviour.

Why is this an "OO abuser"? Because object-oriented languages already have a built-in, automatic switch: it is called polymorphism. When you call visitor.handleEntry(), the language itself looks at the real type of visitor and runs the right method. You get the branching for free, checked by the compiler, with no chance of forgetting a case. Writing manual switches on a type code means refusing this free gift and doing the dispatch by hand โ€” like owning a washing machine and still washing clothes on a stone.

College corner: what actually happens under the hood when you call visitor.handleEntry()? In languages like C# and Java, each class carries a hidden table of method pointers (often called a vtable). The call looks up the right method in that table at runtime in constant time. This is dynamic dispatch โ€” a switch the runtime performs for you, automatically kept in sync with the class hierarchy. A hand-written switch is static branching: the set of cases is frozen in the caller's source code. The deep difference is where new behaviour goes. With branching, new behaviour means editing every caller. With dynamic dispatch, new behaviour means adding one new class, and every existing call site picks it up without changing. That is the Open/Closed Principle expressed in machinery rather than slogans.

The whole landscape of this smell fits on one map:

Figure 1: The Switch Statements smell at a glance โ€” signs, costs, cures, and what to keep

๐Ÿ•ต๏ธ How to spot it

Use this checklist when you read code. Tick any box, and look closer:

  • There is a field, enum, or string whose only job is to say what kind of thing an object is (type, kind, category, role).
  • A switch or if-else-if ladder branches on that kind value.
  • You search for that enum or string in the project and find it in three or more different switch ladders.
  • Adding a new kind last month meant editing several existing files instead of adding one new file.
  • There are default: branches that throw "unknown type" errors โ€” a runtime confession that the compiler cannot protect you.
  • You see instanceof, typeof, obj.type === "...", or a type check followed by a cast, sprinkled around the code.
  • Two switches on the same cases have drifted apart โ€” one handles five cases, the other handles only four, and nobody knows if that is a bug.

Here is the same checklist as a quick symptom table:

Symptom you seeWhat it really means
A type or kind field on a classBehaviour is being decided outside the object, by whoever reads this field
Same switch cases appearing in 3+ functionsOne concept ("visitor type") is physically scattered; updates must hit every copy
default: throw new Error("unknown type")The set of cases is open-ended and the compiler cannot check completeness
instanceof / typeof checks before calling methodsCallers do not trust a shared interface โ€” likely because there is none
New feature = editing many old filesThe Open/Closed Principle is being violated on every change
Two switch ladders with different case countsThe copies have already diverged; one of them is silently wrong

A very reliable trick: pick the enum (say VisitorType) and run a project-wide search. Count the switch ladders. One ladder โ€” relax. Two โ€” watch it. Three or more โ€” the smell is confirmed, and refactoring will pay off.

When Arjun sir did this search on the school's actual visitor-management software (yes, the rulebook had already been turned into code by a vendor years ago), here is roughly where the same VisitorType switch was hiding:

Figure 2: One visitor-type switch, four hiding places in the school software

Four copies. The rulebook on the desk had three chapters; the software had quietly grown a fourth one inside the monthly-report module that nobody remembered. That is normal. Copies of a switch breed in the dark.

โš ๏ธ Why it is a problem

Let us count the costs in simple words, following what happened to Mrs. Kulkarni.

Cost 1: Every new case is shotgun surgery. When alumni visitors arrive, you must edit the entry switch, the parking switch, the exit switch, and the report switch. Four edits for one idea. Miss one, and the bug ships โ€” Vikram gets stuck at the gate. This "one change, many edits" pain has its own name โ€” the Shotgun Surgery smell โ€” and repeated switches are one of its biggest causes.

Cost 2: The compiler cannot save you. With polymorphism, if you add a new subclass and forget to implement an abstract method, the code does not compile. With scattered switches, a forgotten case compiles happily and fails at runtime, often in front of users, often through that default: throw branch. Mrs. Kulkarni's missed chapter was found by Vikram at 5 p.m., not by any checker at noon.

Cost 3: Knowledge lives far from data. The rule "vendors park near the store room" is knowledge about vendors, but it lives inside a parking function that knows about every visitor type. Each switch is a little gossip centre that knows everyone's business. Cohesion drops; coupling rises.

Cost 4: The copies drift. Today both switches handle four cases. Next month a hurried teammate adds case five to one switch and forgets the other. Now your codebase quietly disagrees with itself โ€” exactly like the software's hidden fourth switch in the report module.

Watch how the rot spreads over time:

Figure 3: How one innocent switch slowly rots the codebase

Notice the loop at the bottom. Each hotfix makes the switches bigger, which makes the next new type even more painful. The smell feeds itself.

Now replay the alumni change as a conversation between the new type and the scattered switches. This is exactly the sequence of events that trapped Vikram at the gate:

Figure 4: The alumni type must personally visit every switch โ€” and one visit is forgotten

And here is what that evening felt like from the developer's chair. Notice where the mood crashes:

Figure 5: A developer's day adding one new visitor type to switch-heavy code

Finally, the cost in plain numbers. The more copies of the switch exist, the more files every single new type must touch. With polymorphism, the answer is always one:

Figure 6: Files you must edit for each new visitor type, by design

The bars on the left also grow over time โ€” every new feature that copies the switch raises them. The bar on the right stays at one forever. That difference compounds, release after release.

๐Ÿ’ป A real-life code example

Let us write Mrs. Kulkarni's rulebook as code โ€” the way the school's old vendor actually wrote it. Here is the smelly version, and pay attention, because the real danger is that the same switch appears three times:

// BAD CODE: visitor is just data with a type tag
type VisitorType = "parent" | "student" | "vendor" | "coach";
 
interface Visitor {
  name: string;
  type: VisitorType;
}
 
// Switch copy #1: entry slips
function issueEntrySlip(v: Visitor): string {
  switch (v.type) {
    case "parent":
      return `Blue slip for ${v.name}. Call the class teacher.`;
    case "student":
      return `Yellow slip for ${v.name}. Call the class monitor.`;
    case "vendor":
      return `Green slip for ${v.name}. Call the librarian.`;
    case "coach":
      return `White slip for ${v.name}. Call the PT teacher.`;
    default:
      throw new Error("Unknown visitor type!");
  }
}
 
// Switch copy #2: parking โ€” SAME cases, different file in real life
function assignParking(v: Visitor): string {
  switch (v.type) {
    case "parent":
      return "Park on the left side.";
    case "student":
      return "Cycle stand only.";
    case "vendor":
      return "Park near the store room.";
    case "coach":
      return "Park near the sports ground.";
    default:
      throw new Error("Unknown visitor type!");
  }
}
 
// Switch copy #3: exit passes โ€” the SAME cases yet again
function exitRule(v: Visitor): string {
  switch (v.type) {
    case "parent":
      return "Exit after teacher signs the slip.";
    case "student":
      return "Exit only after last bell.";
    case "vendor":
      return "Exit after store room checks the delivery.";
    case "coach":
      return "Exit through the back gate.";
    default:
      throw new Error("Unknown visitor type!");
  }
}

Read it slowly. Each function alone looks tidy. That is the trap! No single switch looks evil. The evil is in the repetition: the string "vendor" is now load-bearing in three places. Add an "alumni" type tomorrow and you must:

  1. Update the VisitorType union.
  2. Edit issueEntrySlip. 3. Edit assignParking. 4. Edit exitRule.
  3. Pray you found every other switch hiding in files you forgot about โ€” remember the report module nobody remembered.

In a real project these three functions live in three different files, written by three different people, in three different months. Nobody sees them side by side like you just did. Vikram's 5 p.m. gate drama is the natural result.

๐Ÿงน Cleaning it up, step by step

Arjun sir volunteers to fix the school software during the summer break. We will follow his refactoring gradually, the way you would in a real project, keeping the code working after every step.

Step 1: Gather the scattered behaviour into one place. Before introducing classes, first pull all three switches next to each other so the duplication is visible and testable:

// Step 1: at least the rulebook is now ONE object per type, in ONE place
const visitorRules: Record<VisitorType, {
  entrySlip: (name: string) => string;
  parking: string;
  exit: string;
}> = {
  parent: {
    entrySlip: (n) => `Blue slip for ${n}. Call the class teacher.`,
    parking: "Park on the left side.",
    exit: "Exit after teacher signs the slip.",
  },
  student: {
    entrySlip: (n) => `Yellow slip for ${n}. Call the class monitor.`,
    parking: "Cycle stand only.",
    exit: "Exit only after last bell.",
  },
  vendor: {
    entrySlip: (n) => `Green slip for ${n}. Call the librarian.`,
    parking: "Park near the store room.",
    exit: "Exit after store room checks the delivery.",
  },
  coach: {
    entrySlip: (n) => `White slip for ${n}. Call the PT teacher.`,
    parking: "Park near the sports ground.",
    exit: "Exit through the back gate.",
  },
};

This is already a big win: TypeScript now forces every visitor type to provide all three behaviours. Forgetting a case is a compile error, not a runtime surprise. If this lookup table had existed, Mrs. Kulkarni's missed chapter would have been caught the moment "alumni" was added โ€” the compiler would have demanded all three rules at once. For many small programs you can even stop here.

Step 2: Replace the type code with real classes. When behaviour grows beyond simple strings โ€” vendors need delivery lists, coaches need gate keys โ€” upgrade each case into its own class behind one interface. This is the classic refactoring called Replace Conditional with Polymorphism, and it is exactly Arjun sir's "send them to the department" idea:

// Step 2: each department handles its own visitors
interface SchoolVisitor {
  readonly name: string;
  entrySlip(): string;
  parkingSpot(): string;
  exitRule(): string;
}
 
class ParentVisitor implements SchoolVisitor {
  constructor(readonly name: string, private childClass: string) {}
  entrySlip()   { return `Blue slip for ${this.name}. Call teacher of ${this.childClass}.`; }
  parkingSpot() { return "Park on the left side."; }
  exitRule()    { return "Exit after teacher signs the slip."; }
}
 
class VendorVisitor implements SchoolVisitor {
  constructor(readonly name: string, private deliveryId: string) {}
  entrySlip()   { return `Green slip for ${this.name}. Delivery ${this.deliveryId}.`; }
  parkingSpot() { return "Park near the store room."; }
  exitRule()    { return "Exit after store room checks the delivery."; }
}
 
class CoachVisitor implements SchoolVisitor {
  constructor(readonly name: string) {}
  entrySlip()   { return `White slip for ${this.name}. Call the PT teacher.`; }
  parkingSpot() { return "Park near the sports ground."; }
  exitRule()    { return "Exit through the back gate."; }
}
 
// The receptionist's whole job now:
function receive(visitor: SchoolVisitor) {
  console.log(visitor.entrySlip());
  console.log(visitor.parkingSpot());
  console.log(visitor.exitRule());
}

Look at receive. No switch. No default. No throw. Adding alumni now means writing one new class AlumniVisitor โ€” and not touching a single existing line. That is the Open/Closed Principle working for you: open for extension, closed for modification. Here is the new shape of the design as a class diagram:

Figure 7: One contract, many departments โ€” each visitor type carries its own three behaviours

College corner: here is the precise trade-off, the one your design-patterns professor wants you to be able to state. Branching and polymorphism are duals. A switch keeps all operations for one type-set spread across functions, so adding an operation is cheap (one new function) but adding a type is expensive (edit every function). Polymorphism keeps all operations for one type gathered in one class, so adding a type is cheap (one new class) but adding an operation is expensive (touch every class). This tension is called the Expression Problem. The practical rule: if your types keep growing (new visitor kinds, new payment methods), choose polymorphism. If your operations keep growing over a stable set of types (a compiler doing many passes over a fixed set of AST nodes), a switch โ€” or the Visitor pattern โ€” may genuinely be the better axis. Knowing which axis grows in your project is the whole game.

Step 3: Keep exactly one switch โ€” at the boundary. Real data arrives flat: a form, a database row, a JSON payload with type: "vendor". Somewhere you must convert that flat tag into the right object. That place is a factory, and it is allowed to switch once:

// The ONLY switch left in the whole program โ€” and that is fine
function createVisitor(data: { type: VisitorType; name: string; extra: string }): SchoolVisitor {
  switch (data.type) {
    case "parent":  return new ParentVisitor(data.name, data.extra);
    case "vendor":  return new VendorVisitor(data.name, data.extra);
    case "coach":   return new CoachVisitor(data.name);
    case "student": return new StudentVisitor(data.name);
  }
}
Figure 8: Before and after โ€” three scattered switches become one factory plus polymorphism

The life story of a switch in a healthy project follows a predictable path โ€” and the refactor is just helping it along:

Figure 9: The life cycle of a switch โ€” and the exit ramp at every stage

Note the bottom-left exit: a switch that stays single never needs the journey at all. The diagram is honest about that.

To go deeper into this exact transformation, read Replace Conditional with Polymorphism. And when the "type" is really a mode that changes at runtime (draft โ†’ published โ†’ archived), or the cases are interchangeable algorithms the caller picks, the cures are the State and Strategy patterns โ€” they are this same idea, packaged for those two situations.

โš ๏ธ

Do not delete the last switch! Factories, parsers, and deserializers must branch once to turn flat data into rich objects. The goal is not zero switches โ€” the goal is one switch per type code, at the boundary, and none in the business logic.

The same smell in C# ๐Ÿ›’

The smell looks identical in C#. Here is a shop billing example, smelly first:

enum ItemKind { Book, Toy, Medicine }
 
class BillingService
{
    public decimal Tax(decimal price, ItemKind kind) => kind switch
    {
        ItemKind.Book     => 0m,
        ItemKind.Toy      => price * 0.12m,
        ItemKind.Medicine => price * 0.05m,
        _ => throw new ArgumentException("unknown kind")
    };
 
    // Same cases again โ€” the duplication is the smell
    public string ShelfLabel(ItemKind kind) => kind switch
    {
        ItemKind.Book     => "Aisle 1 - Books",
        ItemKind.Toy      => "Aisle 4 - Toys",
        ItemKind.Medicine => "Counter only",
        _ => throw new ArgumentException("unknown kind")
    };
}

And the polymorphic cure โ€” each item kind becomes a class that knows its own tax and shelf:

abstract class Item
{
    public abstract decimal Tax(decimal price);
    public abstract string ShelfLabel { get; }
}
 
sealed class Book : Item
{
    public override decimal Tax(decimal price) => 0m;
    public override string ShelfLabel => "Aisle 1 - Books";
}
 
sealed class Toy : Item
{
    public override decimal Tax(decimal price) => price * 0.12m;
    public override string ShelfLabel => "Aisle 4 - Toys";
}
 
sealed class Medicine : Item
{
    public override decimal Tax(decimal price) => price * 0.05m;
    public override string ShelfLabel => "Counter only";
}

Adding Stationery is one new class. BillingService never changes. One honest note for C# fans: modern C# switch expressions on a closed enum, with compiler warnings for missing cases, recover some safety. A single exhaustive switch expression in one place can be a fine design. The danger is still the same โ€” the moment that switch gets copy-pasted, the smell is back.

College corner: functional languages handle this same tension with algebraic data types and exhaustive pattern matching โ€” a closed set of cases where the compiler proves every match handles every constructor. TypeScript's discriminated unions plus a never-typed exhaustiveness check give you the same proof. So the modern picture is not "switch bad, classes good." It is: either let the compiler verify a closed set of cases in one place (exhaustive match), or let dynamic dispatch handle an open set of cases across classes (polymorphism). The smell lives in the unguarded middle: open-ended string tags, no exhaustiveness checking, and copies everywhere.

๐Ÿ—บ๏ธ Where this smell hides in real projects

This smell is everywhere once you learn its face. Real places it hides:

  • Payment processing. switch (paymentMethod) โ€” UPI, card, net banking, wallet, cash on delivery โ€” repeated in the charge code, the refund code, the receipt code, and the analytics code. Four switches, one concept.
  • Order and ticket status flows. if (status === "pending") ... else if (status === "shipped") ... scattered across UI rendering, notification sending, and permission checks. This one is usually a state machine in disguise โ€” a job for the State pattern.
  • File and message format handling. Branching on file extension or message type (csv, json, xml) in the reader, the validator, and the exporter separately.
  • Pricing and discount rules. Customer tier switches (regular, silver, gold) copied into cart totals, shipping costs, and loyalty points โ€” a textbook Strategy case.
  • UI rendering by type. Frontend components with switch (widget.type) rendering ladders that mirror identical ladders in validation and serialization code.
  • Deserialized API data. JSON arrives with a "type" discriminator field, and instead of converting it to objects once at the boundary, the raw tag is passed deep into the program where everyone switches on it.

The common thread: data comes in flat with a tag, and nobody takes responsibility for turning the tag into an object once, so the tag travels everywhere and everyone branches on it. The school software's "vendor" string travelling from the gate form into four modules is the same disease.

โš–๏ธ When it is okay to ignore

Honest table time. Not every switch needs surgery โ€” and pretending otherwise is its own kind of over-engineering. Arjun sir did not convert the factory switch, and he was right.

SituationVerdictWhy
One simple switch, in one place, mapping enum โ†’ labelLeave it aloneA class hierarchy for a label lookup is far heavier than the switch
A single switch inside a factory or parserLeave it aloneBoundaries must branch once to create objects; that is the design working, not failing
Exhaustive switch expression on a sealed/closed enum, compiler-checked, in one placeUsually fineModern compilers warn on missing cases, recovering polymorphism's main safety benefit
Same switch in 2 places, cases stable for yearsWatch itLow churn means low cost; refactor when the third copy or the next case appears
Same switch in 3+ placesRefactorEvery new case is now a multi-file hunt; this is the real smell
Switch on a status that changes at runtime, branched in many methodsRefactor to StateYou have a state machine wearing a switch costume
Type tag from JSON passed deep into business logicRefactorConvert tag to object once at the boundary; do not let the tag travel

You can place any switch you meet on this two-question map: is it copied? and do the types keep growing?

Figure 10: Two questions decide the verdict โ€” is the switch copied, and do the types keep growing?

The school rulebook sits deep in the top-right corner: four copies, and the principal keeps inventing new visitor types every year. Refactor now. The factory switch sits safely on the left: even though visitor types keep growing, there is only one of it, and the compiler checks it.

โ„น๏ธ

A good rule of thumb from Fowler's renaming of this smell: count the repetitions, not the switches. Zero or one occurrence of the branching = fine. Two = caution. Three or more = the refactoring will pay for itself the very next time a case is added.

๐Ÿ› ๏ธ Which refactorings cure it

RefactoringUse it when
Replace Conditional with PolymorphismThe main cure: same switch repeated, branching on a type, several behaviours per type
Replace Type Code with SubclassesThe type is fixed at creation and never changes โ€” each code becomes a subclass
Replace Type Code with State/Strategy (State, Strategy)The "type" changes at runtime (State) or is a swappable algorithm (Strategy)
Replace Parameter with Explicit MethodsThe switch keys off a parameter like doTask("save") โ€” split into save(), load()
Introduce Null ObjectOne branch exists only to handle the "nothing there" case
Extract Method + Move MethodFirst aid: pull each switch into a named method so the duplication becomes visible

๐Ÿ“ฆ Quick revision box

+----------------------------------------------------------------+
|  SWITCH STATEMENTS (a.k.a. Repeated Switches) โ€” CHEAT SHEET    |
+----------------------------------------------------------------+
|  Story    : Receptionist re-reads the same 4-case rulebook     |
|             in 3 chapters; smart school sends each visitor     |
|             to its own department.                             |
|  Smell    : Same switch on a type code, copy-pasted around.    |
|  Spot it  : Search the enum -> found in 3+ if/switch ladders.  |
|  Danger   : New case = edit every copy; miss one = runtime bug |
|             via "default: throw unknown type".                 |
|  Cure     : Replace Conditional with Polymorphism;             |
|             State (runtime modes), Strategy (algorithms).      |
|  Keep     : ONE switch at the boundary (factory/parser) that   |
|             turns flat data into the right object.             |
|  Mantra   : Count the repetitions, not the switches.           |
+----------------------------------------------------------------+

โœ๏ธ Practice exercise

Your turn. A food delivery app has this code, with the same branching in three places:

type Plan = "free" | "plus" | "gold";
 
function deliveryFee(plan: Plan, orderTotal: number): number {
  if (plan === "free") return 40;
  if (plan === "plus") return orderTotal > 199 ? 0 : 25;
  return 0; // gold
}
 
function supportPriority(plan: Plan): string {
  if (plan === "free") return "Email only, 48 hours";
  if (plan === "plus") return "Chat, 4 hours";
  return "Phone, 30 minutes"; // gold
}
 
function badge(plan: Plan): string {
  if (plan === "free") return "";
  if (plan === "plus") return "PLUS";
  return "GOLD โ˜…";
}

Do the refactoring in three steps:

  1. Step 1: Build a single Record<Plan, ...> object holding all three behaviours, like our Step 1 above, and rewrite the three functions as one-liners that read from it.
  2. Step 2: The product team now wants a new "corporate" plan with custom per-company fees. Upgrade to full polymorphism: create a MembershipPlan interface with deliveryFee(), supportPriority(), and badge(), and one class per plan.
  3. Step 3: Write the one allowed switch โ€” a createPlan(tag: string) factory โ€” and check: how many existing files would you need to edit to add a fifth plan? (The correct answer is zero, besides the factory.)
  4. Step 4 (story check): Explain in two sentences why Mrs. Kulkarni's missed chapter could not happen in your Step 2 design. Which exact compiler error replaces Vikram's 5 p.m. gate drama?

Bonus question to discuss with a friend: the plus plan's fee depends on orderTotal. Which pattern fits better here if fee rules keep changing every festival season โ€” State or Strategy? (Hint: does a plan transition into another plan by itself, or does the business just pick a rule?)

Frequently asked questions

Is every switch statement a code smell?
No. A single, simple switch in one place is perfectly fine. The smell appears when the same switch on the same type code is copy-pasted in many places, so adding one new case forces you to hunt down and edit every copy.
Why did Martin Fowler rename this smell to Repeated Switches?
In the second edition of Refactoring, Fowler renamed it because the real danger is repetition, not the switch keyword itself. One switch is harmless. The same switch duplicated across the codebase is the true smell.
What is the main refactoring that cures switch statements?
Replace Conditional with Polymorphism. You turn each case into its own class with a shared interface, so each type carries its own behaviour and the branching disappears from the callers.
When is a switch actually the right choice?
At boundaries like factories, parsers, and deserializers, where you must convert flat data into objects. One switch in exactly one place that creates the right object is the normal price of that conversion.
How are if-else ladders related to this smell?
An if-else-if ladder that branches on the same kind value is the same smell wearing different clothes. The keyword does not matter; the repeated branching on a type code is what hurts.

Further reading

Related Lessons