skip to content

As a technical leader, how do you decide when a design should be reached through incremental refactoring to patterns versus committed to up front, and how do you keep a team from either pattern-happiness or abstraction paralysis?

level: principalimportance: nice to knowfreq 18%

answer

  1. reversibility × blast radius decides the split
  2. one-way doors: data, contracts, boundaries, security, consistency
  3. enablers: fast tests, small commits, arch guards, observability
  4. review question: what evidence justifies this abstraction?
  5. third failure mode: refactoring-happiness

basics

~20 s

Decide by reversibility and blast radius. Decisions you can change later inside one codebase (class structure) should emerge from refactoring. Decisions that are expensive to undo — data models, public contracts, service boundaries, security — deserve up-front design. Teams need tests and a norm that abstractions must be earned and can be removed.

solid answer

~50 s

I split decisions by cost of reversal. Internal, class-level structure is cheap to change with tests and CI, so I let it emerge: simplest design first, patterns introduced when a smell proves the flexibility is needed. Cross-boundary and data-shaped decisions — persisted schemas, published API/event contracts, service and module boundaries, authorization model, concurrency and consistency model — are expensive or impossible to refactor locally because other teams, stored data, and deployed clients depend on them; those get deliberate design, written down, before code. The enabling conditions for the incremental half are non-negotiable: fast reliable tests, trunk-based small commits, and static/module checks that keep boundaries honest. Culturally I set two norms: an abstraction must cite the duplication or change pressure that justifies it in review, and removing an unused abstraction is a normal, praised change. That symmetry is what prevents both pattern-happiness and the fear of ever committing to structure.

go deeper

for a junior

Keep it simple: design carefully where mistakes are hard to undo (database, public API); let the internal class structure grow into patterns when duplication appears.

for a middle

Name concrete reversible vs. irreversible categories and mention that tests and small commits are what make the incremental half safe.

for a senior

Structure the answer around reversibility and blast radius, list the enabling practices, and describe the review norm that requires evidence before adding an abstraction.

for a principal

Add leverage moves — engineering reversibility itself (expand/contract, versioning, flags, anti-corruption layers), hot-spot-driven investment, ADRs distinguishing committed boundaries from emergent internals, and treating removal of unused abstraction as praiseworthy.

## The core decision axis: reversibility × blast radius Refactoring is cheap when a change is *local and mechanical*: the compiler and test suite find every affected site, and the change ships in one deploy. It is expensive — or not a refactoring at all — when the structure is embodied in things you don't fully control: - **Persisted data.** A schema or document shape has history. Changing it means migrations, dual writes, backfills, and rollback plans. - **Published contracts.** REST/gRPC APIs, event schemas, library APIs consumed by other teams or external clients cannot be changed by editing your repo; they need versioning and deprecation windows. - **Service/module boundaries.** Moving behavior across a network or ownership boundary is a project, not a refactoring; getting the boundary wrong creates chatty coupling and distributed transactions. - **Security and authorization models.** Retrofitting authorization onto a design that assumed trust is systemic and error-prone. - **Concurrency and consistency choices.** Whether an operation is transactional, idempotent, or eventually consistent leaks into every caller. Everything *inside* those boundaries — class structure, which pattern shapes a subsystem, how variation is expressed — is exactly where refactoring to patterns shines, because a wrong guess is correctable in green steps. A useful framing: **one-way doors get deliberate design; two-way doors get an experiment plus refactoring.** Note that the door can be made two-way by engineering: contract versioning, expand/contract migrations, feature flags, and anti-corruption layers all convert would-be one-way decisions into reversible ones, which is often the highest-leverage thing a leader can invest in. ## The enabling conditions Incremental design is only cheap if the machinery exists: 1. **A fast, trustworthy test suite** — behavior preservation is a claim otherwise. For legacy areas, budget characterization tests before design work. 2. **Small commits / trunk-based flow** — long-lived branches make refactoring a merge-conflict generator, which silently pushes teams toward big-bang redesigns. 3. **Automated structural guards** — module dependency rules, layering/arch tests, static analysis. They protect the boundaries you *did* decide up front while leaving internals free. 4. **Observability** — to know which code paths and abstractions are actually exercised, which is what makes removal decisions evidence-based. ## Governing the two failure modes **Pattern-happiness** (speculative generality) shows up as many types, few behaviors, plugin machinery with one plugin, factories that build one thing. Countermeasures: - In code review, require the *evidence*: what duplication or observed change pressure motivates this abstraction? "We might need it" is not evidence. - Prefer the rule of three for extraction pressure. - Track a lightweight signal: abstractions with exactly one implementation older than N months, reviewed periodically. **Abstraction paralysis / refactoring-never** shows up as 500-line methods, copy-paste variants, and change lead times that grow. Countermeasures: - Refactor opportunistically in the code you're already changing (the "camp-site rule"), so cleanup rides on funded work rather than needing its own permission. - Make smells visible: duplication metrics, hot-spot analysis (files with high churn × high complexity are where pattern work pays). - Time-box: if a feature is fighting the design, do the enabling refactoring first as a separate commit, then the feature. **Refactoring-happiness** is the third, under-discussed failure: mass cleanups in code nobody touches, generating review load, merge conflicts, and risk with no user-visible return. The counter is the same evidence standard applied in reverse — change where work is happening or where change is blocked. ## Communicating the stance Write down the decisions that were deliberately made up front (an ADR-style record: context, options, decision, consequences) so people can tell a *committed boundary* from an *emergent internal structure*. Without that, teams either treat every line as sacred or refactor across a boundary that was load-bearing. Pattern names are also a communication asset — once a subsystem genuinely is a Strategy or a State machine, naming it that way makes onboarding and review cheaper; naming something a pattern it isn't does the opposite. ## The one-paragraph answer "I design up front exactly where reversal is expensive — data, contracts, boundaries, security, consistency — and I invest in making those reversible where I can. Everything inside those lines emerges: simplest thing, then patterns when a smell earns them, then removal when they stop paying. The practice only works on top of fast tests, small commits, and structural guards, and it needs a culture where both adding and deleting an abstraction require the same kind of evidence."

  • Give an example of turning a one-way door into a two-way door so a decision can be deferred.
    Database schema change via expand/contract: add the new column and dual-write, backfill, migrate readers, then drop the old column — each step reversible. Similarly, versioned API contracts plus a translation layer let internal models be refactored freely; an anti-corruption layer around a third-party model keeps its shape from leaking; and feature flags let a new implementation run in parallel with the old and be switched back instantly.
  • How do you decide, at portfolio level, WHERE pattern-level refactoring is worth funding?
    Target hot spots — files or modules with high change frequency combined with high complexity or defect density, since that is where structural cost is actually being paid repeatedly. Code that is complex but never changes is not worth the risk. Tie the refactoring to upcoming roadmap work in that area so it is enabling work rather than speculative cleanup, and measure the outcome in lead time and change-failure rate rather than in class counts.
  • A senior engineer proposes a plugin architecture for a feature with one implementation today. How do you respond?
    Ask what concrete evidence exists: named second implementation with a date, an external consumer, or a contract requiring it. If none, ship the simplest version and note the seam we'd extract if a second case appears, since extracting a Strategy or Decorator later is a bounded refactoring. If the flexibility is contractual (a customer will supply implementations), it is a boundary decision, and then it belongs to the up-front half — design the extension contract deliberately, because it becomes published API.

A building's foundation and load-bearing walls are poured once and are ruinous to move; interior partitions can be rearranged any weekend. Design the foundation deliberately, and let the floor plan inside evolve with how people actually use the space.

saying these in an interview costs you the question

  • "Emergent design means never designing up front" — data models, contracts, and boundaries are not locally refactorable.
  • "Up-front architecture means choosing the patterns" — patterns are internal structure, not boundary decisions.
  • Advocating incremental design without the enabling machinery (tests, small commits, arch guards), which turns it into no design.
  • Treating 'one implementation' as automatically wrong even for deliberate architectural ports or published extension points.
  • Funding large refactorings in cold code that nobody is changing, which is cost without return.
  • Assuming naming a subsystem after a pattern makes it that pattern; wrong names mislead reviewers and onboarding.

context