Skip to content
aipatterns.dev

Code smellChange Preventers

Shotgun Surgery

One logical change forces small edits in many classes, so one missed edit ships an inconsistent system.

Signs

  • A single requirement ("add a CAD currency", "rename the trial plan") produces a diff that touches eight files with two lines each.
  • You find the places to change with a global text search, not by following the code.
  • Bugs appear because one of the many copies was missed.
  • The same small decision (a threshold, a format, a list of allowed values) is encoded in several classes.
  • Several classes each hold a fragment of one concept, and none of them owns it.

Why it happens

Responsibility for one concept got split across layers, often by a rule like "controllers format, services validate, repositories map" applied mechanically. Each layer then encodes its own copy of the same knowledge. Copy-paste of a small snippet into several places has the same effect. Over-eager splitting to cure Divergent Change can also cause it.

Example

A SaaS app supports subscription plans. Adding a plan means editing the validator, the pricing code, the feature gate and the email template.

plans.ts
// validation.ts
export function isValidPlan(plan: string): boolean {
  return plan === "free" || plan === "pro" || plan === "team";
}

// pricing.ts
export function monthlyPriceCents(plan: string): number {
  if (plan === "pro") return 1200;
  if (plan === "team") return 4900;
  return 0;
}

// features.ts
export function maxProjects(plan: string): number {
  return plan === "team" ? 100 : plan === "pro" ? 20 : 3;
}

// emails.ts
export function planLabel(plan: string): string {
  return plan === "team" ? "Team" : plan === "pro" ? "Pro" : "Free";
}

Adding "enterprise" needs four edits in four files. After moving the knowledge into one place:

plans.ts
export const PLANS = {
  free: { label: "Free", priceCents: 0, maxProjects: 3 },
  pro: { label: "Pro", priceCents: 1200, maxProjects: 20 },
  team: { label: "Team", priceCents: 4900, maxProjects: 100 },
} as const;

export type PlanId = keyof typeof PLANS;

export function isValidPlan(plan: string): plan is PlanId {
  return plan in PLANS;
}

// Callers read PLANS[plan].priceCents, PLANS[plan].maxProjects, PLANS[plan].label.

A new plan is now one entry, and the type checker flags any caller that cannot handle it.

Treatment

  • If behavior about one concept is spread across classes, use Move Method to gather it in the class that owns the concept's data.
  • If the data itself is scattered, use Move Field first, then move the methods that use it.
  • If gathering leaves behind classes that now do almost nothing, use Inline Class to fold them away.
  • If the scattered pieces are copies of the same logic, treat it as Duplicate Code as well.
  • Where the scattered code is a type switch, consider Replace Conditional with Polymorphism or a lookup table so a new case is one addition.

Payoff

  • One change, one place. Reviews show the whole change at once.
  • Missed-copy bugs disappear.
  • The concept gets a name and a home that new contributors can find.

When to ignore

  • The edits are spread across genuinely separate deployables (a client and a server, two services) where sharing code would couple release cycles. Use a shared schema or contract test instead.
  • The repeated knowledge is trivial and stable, such as an HTTP status code.
  • Generated code (protobuf, ORM models) produces the spread. Fix the generator input, not the output.

Agent checklist

  • You searched for every place that encodes the concept, including tests, templates and config.
  • The concept now has one owning module, and other code reads from it.
  • Adding a new case requires one edit, and you verified that with a dry run or the type checker.
  • You did not create a new cross-service dependency to centralize code.
  • All tests pass after each move, not only at the end.

Treatments

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.