skip to content

What does "refactoring away from a pattern" mean, what signals tell you a pattern is over-applied, and how would you safely remove one from a live codebase?

level: seniorimportance: should knowfreq 30%

answer

  1. patterns are a purchase — stop paying rent
  2. one implementation, long-lived, no external implementers
  3. Inline Singleton / Collapse Hierarchy / Remove Middle Man
  4. published API → deprecate + shim, not delete
  5. architectural port ≠ speculative interface

basics

~20 s

It means removing a pattern that costs more than it gives — for example a Strategy interface with one implementation or an unnecessary Singleton. You inline the indirection step by step, with tests green, until the simpler direct code remains.

solid answer

~50 s

Kerievsky treats refactoring as bidirectional: toward a pattern when a smell justifies it, and away from one when it no longer earns its keep. Signals of over-application: an abstraction with exactly one implementation that has existed for a long time; every change requiring edits across several indirection layers; factories/registries that only ever produce one type; a Singleton used purely as a global variable and hurting testability; a Visitor over a hierarchy that never changes; deep decorator or wrapper chains where every layer is always present; "framework" hooks nobody implements. Removal method: verify with usage search and telemetry that only one path is live; inline the pattern in small behavior-preserving steps (inline the delegating call, collapse the hierarchy, move the single implementation's body into the caller, delete the now-unused interface); keep tests green at each step and commit frequently. If the abstraction is published API for other teams, deprecate through a compatibility shim rather than deleting, and remove it after consumers migrate.

go deeper

for a junior

Say patterns can be removed too: if a Strategy has one implementation or a Singleton is just a global, you inline it in small steps with tests green.

for a middle

List concrete over-application signals and name the removal mechanics (Inline Function/Class, Collapse Hierarchy, Remove Middle Man), one commit per step.

for a senior

Add usage/telemetry verification, published-API deprecation with a shim, and the caveat that a single-implementation interface may be a deliberate architectural port.

for a principal

Frame it as portfolio management of accidental complexity: remove opportunistically where work is already happening, quantify the carrying cost, and set team norms that make both introduction and removal routine rather than political.

## Why the direction exists Patterns are a *purchase*: you pay indirection, extra types, more files, longer stack traces, and slower comprehension, in exchange for a specific flexibility. Requirements change; sometimes the flexibility was never used, or the reason evaporated (the second payment provider was never built; the plugin marketplace was cancelled). Keeping the pattern then means paying rent on an asset you don't use. Kerievsky's contribution over pure pattern advocacy is making removal a *named, legitimate move* with the same mechanical discipline as introduction — Fowler's catalog contains the machinery (Inline Function, Inline Class, Collapse Hierarchy, Remove Middle Man, Replace Superclass with Delegate/inverse). ## Concrete over-application signals | Signal | What it usually means | |---|---| | Interface with exactly one implementation, unchanged for a long time | Speculative generality; the seam was never needed (mock-only implementations don't count as a second implementation) | | Factory/Builder that always returns the same concrete type with the same arguments | Creation logic has no variation to encapsulate | | Singleton accessed statically from everywhere | Global mutable state in disguise; hidden dependency, test-order coupling | | Every feature change touches 4 files across layers | Indirection exceeds the variation it serves | | Wrapper/decorator layer present in 100% of compositions | Not optional — belongs in the core | | Observer/event indirection with a single, synchronous, always-registered listener | A direct call would be clearer and debuggable | | Visitor over a hierarchy that hasn't changed in years and whose operations rarely grow | Cost of the double-dispatch machinery isn't repaid | | Abstract base class with one subclass | Collapse Hierarchy candidate | **Important caveat:** "only one implementation" is evidence, not proof. Legitimate reasons to keep a lone abstraction: an architectural port at a module/hexagonal boundary you deliberately enforce; a published extension point with external implementers; a dependency-inversion seam that keeps a domain module free of infrastructure; enforced compile-time module boundaries (as in Spring Modulith-style `*Api` surfaces). Judge by whether the boundary is *load-bearing*, not by counting classes. ## Safe removal procedure 1. **Confirm the assumption.** Search all usages including reflection/DI configuration, string-based wiring, serialized type names, and other repositories. Check telemetry/feature flags to be sure the alternative path is dead. 2. **Check the blast radius.** Internal-only? Refactor freely. Published to other teams/artifacts? Deprecate first, ship a shim that delegates, migrate consumers, then delete — a removal you can't coordinate is a breaking change, not a refactoring. 3. **Strengthen tests** on the behavior the pattern currently produces, at the level that will survive the change (test through the public entry point, not through the class you're deleting). 4. **Inline in small steps**, e.g. for a one-implementation Strategy: (a) make the context depend on the concrete class instead of the interface; (b) inline the delegating call; (c) move the implementation body into the context, or keep the class but drop the interface; (d) delete the interface; (e) simplify the wiring/factory; (f) delete the factory if it now does nothing. For a Singleton: (a) add a constructor and instance methods; (b) convert call sites to injected dependencies one at a time; (c) delete the static accessor last. 5. **Commit per step.** Small commits mean cheap reverts and a reviewable history. 6. **Delete dead tests** that only exercised the removed indirection, and keep the behavioral ones. ## Judgment: when to leave it alone Removal is itself a cost (review time, merge conflicts, risk, churn in blame history). Prefer to remove when: the code is being touched anyway for a feature; the indirection is actively slowing comprehension or onboarding; or it blocks a needed change. Avoid opportunistic mass removals across code nobody is working in — that is the mirror image of pattern-happiness, *refactoring-happiness*, and it burns the same budget with no user-visible return. ## What to say in an interview "I treat patterns as reversible. If an abstraction has one implementation, no external implementers, and hasn't varied, I'll inline it in green steps while I'm in that code — unless it's a deliberate architectural boundary, in which case its value is enforcement, not variation."

  • Isn't an interface with one implementation still justified for mocking in tests?
    Usually not on its own. Modern mocking tools can fake concrete classes, and test-only abstraction pressure often signals the design has a real coupling problem (hidden I/O, static state) better fixed directly. The interface is justified when it marks a genuine boundary — a hexagonal port, a module's published surface, or a seam that keeps domain code independent of infrastructure — where its value is enforcing direction of dependency, not enabling substitution.
  • How do you remove a Singleton that is called statically from two hundred places?
    Incrementally, never in one commit. Give the class a normal constructor and instance methods; keep the static accessor delegating to a single instance so nothing breaks. Then migrate call sites in batches to constructor/DI-injected dependencies, starting with code you're already touching and with anything whose tests suffer from shared state. When the accessor's usage count reaches zero, delete it. Guard the whole path with tests that don't rely on static reset hooks.
  • What's the difference between refactoring away from a pattern and just deleting code you don't like?
    Behavior preservation and mechanics. Refactoring away follows named steps (Inline Function, Inline Class, Collapse Hierarchy, Remove Middle Man), each individually green, with usage analysis and a compatibility plan for external consumers. Deleting because you dislike it skips the evidence step — you might be removing a load-bearing boundary or a seam another team implements.

saying these in an interview costs you the question

  • "Removing a pattern is a step backwards / admitting the design was wrong" — reversibility is the point; requirements changed.
  • "Any interface with one implementation must be deleted" — architectural ports and published extension points are legitimate.
  • Counting mock implementations as a second implementation to justify keeping an abstraction.
  • Doing the removal as one big commit with no usage analysis of reflection/DI/string-based wiring.
  • Deleting a published abstraction other teams implement instead of deprecating with a shim.
  • Mass-removing patterns across untouched code purely for tidiness — refactoring-happiness with no payoff.

context