# Shotgun Surgery

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

- kind: smell
- category: change-preventers
- canonical: https://aipatterns.dev/refactoring/smells/shotgun-surgery
- inspired-by: https://refactoring.guru/smells/shotgun-surgery

## 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](https://aipatterns.dev/refactoring/smells/divergent-change.md) 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.

```ts title="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:

```ts title="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](https://aipatterns.dev/refactoring/techniques/move-method.md) to gather it in the class that owns the concept's data.
- If the data itself is scattered, use [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) first, then move the methods that use it.
- If gathering leaves behind classes that now do almost nothing, use [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) to fold them away.
- If the scattered pieces are copies of the same logic, treat it as [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) as well.
- Where the scattered code is a type switch, consider [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) 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.
