Skip to content

From code smell to pattern, and back

Name the smell by what the next change costs; move, delete or split code first, and add a pattern only when the smell keeps returning.

Predict first

Before you read: predict

A chat app keeps old messages for a set period. Retention moves from 30 days to 14 days. A new test will check the constant, the purge cutoff, the export window and the banner against 14 days, and that every other limit keeps its value. Its files are below.

The smallest change that passes that test: which existing files does it edit?

Code for this question
// policy.tsexport const RETENTION_DAYS = 30; // purge.tsimport { RETENTION_DAYS } from './policy';const DAY_MS = 86_400_000;export const purgeCutoff = (now: number): number => now - RETENTION_DAYS * DAY_MS; // export.tsexport const exportWindowSeconds = 2_592_000; // banner.tsexport const retentionBanner = 'Messages older than 30 days are deleted.'; // limits.tsexport const MAX_ATTACHMENTS = 30;export const MAX_MEMBERS = 500;
Pick one

The 30-second version

A smell is a sign that a change costs more than it should: one change spread over many files, one file changed for many reasons, a copied switch, a flag picking half a method, code built for a future that never came. Count what the next change edits, before and after. Move, delete or split first; patterns come later.

Name the smell by what the next change costs; move, delete or split code first, and add a pattern only when the smell keeps returning.

How it works

Step 1 of 14

Smell first. Start from what hurts in the code you have, not from a pattern you would like to use.

Analogy: A pest-control firm's bait-station visits (pest control)

A pest-control firm services bait stations every 6 weeks. The interval is printed on each station's label, in the van's job sheet, in every client contract, and in the booking app as "42 days".

At the firm In code
the app's "42 days", missed by whoever searched for "6 weeks" a copy in another unit (step 4)
one service schedule that every label says to check moved together (step 5)
a job sheet saying "food premises: skip steps 4–6" a flag parameter (step 9)
a fumigation column nobody ever fills in speculative generality (step 11)

Breaks down when: a printed label can't be checked, while a test can catch a copy that was missed.

In real terms: before choosing a fix, list each place a likely change must touch; the fix that leaves the fewest places, with the least new code, wins.

Examples, run for real

4.1 ExamHall EXH-4030 → MR !4330: drag-order in three switches

ExamHall grades, renders and exports each question with a switch on its type, in grade.ts, render.ts and export.ts. EXH-4030: adding the drag-order type had to find and edit all three.

4.1 EXH-4030 three switches, drag-order added the smallest way: types.ts 1, grade.ts 1, render.ts 1, export.ts 1 line(s) · 4 existing files · test 10 of 10
4.1 MR !4330 one entry per type: the same test 10 of 10, as with the switches (10 of 10) · grade.ts, render.ts and export.ts name no question type
4.1 then hotspot: types.ts 1, question-types.ts 1 line(s) · 2 existing files (with the three switches: 4)

That is shotgun surgery on a switch (steps 3, 7). MR !4330 moves each type's grading, rendering and export cell into one entry of a table keyed by the type (step 5); the three files become one-line lookups. The next type, hotspot, edits two files instead of four. The table holds plain functions, not a class per type; how such a table is typed belongs to pat-ocp-strategy.

4.2 Chat: the fix is a deletion

A chat app sends notifications through an abstract base class with one subclass, and every caller holds the base type (step 11).

4.2 chat notifier: an abstract base class with one subclass, every caller holding the base type · 4 files, 14 lines
4.2 a mute feature edits `notifier.ts` and `push.ts` (4 lines)
4.2 hierarchy collapsed into one class (Collapse Hierarchy): 3 files, 8 lines · send unchanged · the mute feature edits `notifier.ts` only (3 lines)

Collapsing the base class into its one subclass removes a file and 6 of 14 lines, and the mute feature edits one file instead of two; nothing was added (steps 2, 14).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A chat app's last three feature MRs each added one field to a message. Their diffstats are below.

Which smell in refactoring.guru's catalogue do these MRs show?

Code for this question
reactions MR   message.ts +1   api.ts +2   store.ts +3   render.ts +4   export.ts +1pinned MR      message.ts +1   api.ts +1   store.ts +2   render.ts +3   export.ts +1edited MR      message.ts +1   api.ts +2   store.ts +2   render.ts +2   export.ts +1
Pick one

Question 2 of 7

ChatRoom.ts holds a room's membership rules, its message formatting and its database queries. Last quarter's commits that touched it are below, with every file each touched and the methods edited.

Which smell in refactoring.guru's catalogue does this log show?

Code for this question
commit  request                files           methods edited in ChatRoom.ts4e1a    new emoji set          ChatRoom.ts     formatBody7c02    read-receipts column   ChatRoom.ts     save, load9b6f    moderator role         ChatRoom.ts     canPost
Pick one

Question 3 of 7

A chat server's setRoomOption(name: string, on: boolean) has grown to 180 lines: one if per option name ('slowMode', 'readOnly', 'guestAccess', 'linkPreviews'), each running its own checks, audit entry and broadcast. Every caller passes a literal name; the settings screen's handler is a switch whose four cases each pass it a different name. Its history for the last twelve months is below.

Which change does refactoring.guru's catalogue support?

Code for this question
2025-11  slowMode      audit entry gains the moderator id2025-12  readOnly      broadcast skips muted members2026-01  guestAccess   check reads the room's plan2026-02  slowMode      interval read from settings2026-03  linkPreviews  option added2026-04  readOnly      audit entry gains the reason2026-05  guestAccess   broadcast goes to admins only2026-06  slowMode      check skips moderators2026-07  linkPreviews  check reads the domain list2026-08  readOnly      check allows pinned posts2026-09  guestAccess   audit entry gains the inviter
Pick one

Question 4 of 7

An interviewer says: "Walk me through how you would refactor this 400-line function." It is ExamHall's submitAttempt(), which validates answers, scores them, writes the attempt and emails the candidate.

Write your answer in at most 150 words.

Question 5 of 7

A chat app sells sticker packs in several currencies, and four of its files each decide how many decimal digits an amount has. KRW is next; like JPY, it has no minor unit, and more such currencies will follow. The KRW change comes with a test: prices, receipts, payouts and refunds in KRW, while JPY and USD amounts keep today's values. The files are below.

Which refactor, done before KRW, leaves the KRW change editing or adding the fewest files?

Code for this question
// price.tsexport const priceLabel = (amount: number, currency: string): string =>  `${amount.toFixed(currency === 'JPY' ? 0 : 2)} ${currency}`; // receipt.tsexport const receiptLine = (item: string, amount: number, currency: string): string =>  `${item}: ${amount.toFixed(currency === 'JPY' ? 0 : 2)} ${currency}`; // payout.tsexport const payoutMinor = (amount: number, currency: string): number =>  Math.round(amount * 10 ** (currency === 'JPY' ? 0 : 2)); // refund.tsexport class RefundRounder {  digits(currency: string): number { return currency === 'JPY' ? 0 : 2; }}export const refundMinor = (amount: number, currency: string): number =>  Math.round(amount * 10 ** new RefundRounder().digits(currency));
Pick one

Question 6 of 7

A chat app handles each message kind in four files, and voice messages are next. The MR's test sends one: its preview reads [voice note], its notification says new voice note, it is not indexed for search, and its size cap is 1_000_000 bytes; text and image messages keep today's previews, notifications and caps. The files are below.

The smallest change that passes that test: which existing files does it edit?

Code for this question
// types.tsexport type Kind = 'text' | 'image' | 'poll';export interface Message { kind: Kind; body: string } // preview.tsimport type { Message } from './types';export function previewOf(m: Message): string {  switch (m.kind) {    case 'text': return m.body.slice(0, 40);    case 'image': return '[photo]';    case 'poll': return '[poll]';  }} // notify.tsimport type { Message } from './types';export function notificationFor(m: Message): string {  switch (m.kind) {    case 'text': return 'new message';    default: return 'new attachment';  }} // search.tsimport type { Message } from './types';export function searchable(m: Message): boolean {  switch (m.kind) {    case 'text': return true;    default: return false;  }} // size.tsimport type { Kind } from './types';export const maxBytes: Record<Kind, number> = { text: 4_000, image: 5_000_000, poll: 2_000 };
Pick one

Question 7 of 7

A chat app's message store was given a Mongo path in the commit "store: ready for the Mongo move" on 2024-03-04; no Mongo move was ever scheduled. The comments hold the call counts from its logs, from that day to 2026-10-13. The file is below.

Which smell in refactoring.guru's catalogue is this?

Code for this question
// store.tsimport type { Pool } from './db';import type { Message } from './types';export class PgMessageStore {  constructor(private readonly db: Pool) {}  save(m: Message, retries = 0): void {            // 9,114,220 calls · calls passing retries: 0    this.db.query('INSERT INTO messages (id, room, body) VALUES ($1, $2, $3)', [m.id, m.room, m.body]);  }  byRoom(room: string): Promise<Message[]> {       // 2,480,115 calls    return this.db.query('SELECT id, room, body FROM messages WHERE room = $1', [room]);  }  exportForMongo(room: string): string {           // 0 calls    throw new Error('not implemented');  }}export function createStore(kind: 'pg' | 'mongo', db: Pool): PgMessageStore {   // 'pg': 412 calls · 'mongo': 0 calls  if (kind === 'mongo') throw new Error('not implemented');  return new PgMessageStore(db);}
Pick one

Go deeper

Pro tip
  1. In review, name the smell and its cost before the fix. For example, "this switch is in three files, so every new question type touches three places"; then propose the smallest change. Naming a pattern is optional. When: reviewing an MR that copies a rule or a case. Status: ADVICE (the curriculum row's tip; keys nothing).
Source

A web page can change, so every claim from these pages is labelled as of 2026-10-13.

  • S1 · refactoring.guru, "Shotgun Surgery" (as of 2026-10-13): https://refactoring.guru/smells/shotgun-surgery (refactoring-guru-shotgun-surgery.txt). "Making any modifications requires that you make many small changes to many different classes." (step 3); "Use Move Method and Move Field to move existing class behaviors into a single class." (step 5); "If moving code to the same class leaves the original classes almost empty, try to get rid of these now-redundant classes via Inline Class." (step 5).
  • S2 · refactoring.guru, "Divergent Change" (as of 2026-10-13): https://refactoring.guru/smells/divergent-change (refactoring-guru-divergent-change.txt). "You find yourself having to change many unrelated methods when you make changes to a class." (step 6); "Split up the behavior of the class via Extract Class." (step 6).
  • S3 · refactoring.guru, "Switch Statements" (as of 2026-10-13): https://refactoring.guru/smells/switch-statements (refactoring-guru-switch-statements.txt). "You have a complex switch operator or sequence of if statements." (step 7); "When a new condition is added, you have to find all the switch code and modify it." (step 7); "If there aren’t too many conditions in the operator and they all call same method with different parameters, polymorphism will be superfluous."; "When a switch operator performs simple actions, there’s no reason to make code changes." (step 8); "Often switch operators are used by factory design patterns (Factory Method or Abstract Factory) to select a created class." (step 8).
  • S4 · refactoring.guru, "Replace Parameter with Explicit Methods" (as of 2026-10-13): https://refactoring.guru/replace-parameter-with-explicit-methods (refactoring-guru-replace-parameter-with-explicit-methods.txt). "A method is split into parts, each of which is run depending on the value of a parameter." (step 9); "Extract the individual parts of the method into their own methods and call them instead of the original method."; "Don’t replace a parameter with explicit methods if a method is rarely changed and new variants aren’t added inside it." (step 10); "Find all places where the original method is called. In these places, place a call for one of the new parameter-dependent variants. When no calls to the original method remain, delete it.".
  • S5 · refactoring.guru, "Speculative Generality" (as of 2026-10-13): https://refactoring.guru/smells/speculative-generality (refactoring-guru-speculative-generality.txt). "There’s an unused class, method, field or parameter." (step 11); "Sometimes code is created “just in case” to support anticipated future features that never get implemented." (step 11); "Before deleting elements, make sure that they aren’t used in unit tests." (step 12).
  • S6 · refactoring.guru, "Dead Code" (as of 2026-10-13): https://refactoring.guru/smells/dead-code (refactoring-guru-dead-code.txt). "A variable, parameter, field, method or class is no longer used (usually because it’s obsolete).".
  • S7 · refactoring.guru, "Lazy Class" (as of 2026-10-13): https://refactoring.guru/smells/lazy-class (refactoring-guru-lazy-class.txt). "So if a class doesn’t do enough to earn your attention, it should be deleted.".
  • S8 · refactoring.guru, "Temporary Field" (as of 2026-10-13): https://refactoring.guru/smells/temporary-field (refactoring-guru-temporary-field.txt). "Temporary fields get their values (and thus are needed by objects) only under certain circumstances. Outside of these circumstances, they’re empty.".
  • The row's sources, Fowler, Refactoring (2nd ed., 2018) ch. 3, and Kerievsky, Refactoring to Patterns (2004), are books that cannot be fetched as text, and key nothing.

Steps 1, 2, 4, 13 and 14 are this lesson's framing and its executed counts; no source states them, and no cited key rests on them.

Checked against the tools and docs as of 2026-10-13.