Delete it and watch where complexity goes
Surface smell can put architecture on trial. Only a responsibility trace can tell you whether to delete, extract, preserve, or deliberately defer.
Conditions · architecture undressed
The wrapper looked guilty.
It sat across from me in the review, and not for the first time. It had a respectable name, its own file, a carefully exported interface, and dozens of callers. It also did almost nothing. Each call entered through one door, crossed a room containing no policy, no transformation, no protection, and no coordination, then left through another door carrying the same value.
The safety layer looked guilty too. It was much larger than present traffic appeared to justify. It carried counters, penalties, cleanup work, categories, and protections that the system did not exercise at full scale. On a line-count report, it was the more persuasive defendant.
Deleting the wrapper made the system smaller. Nothing had to be rebuilt in its callers because the wrapper had owned nothing.
Deleting the safety layer made the file tree smaller and the architecture less true. The need to limit abuse, coordinate penalties, and protect entry points did not disappear. It either went unmet or returned as fragments in every caller.
The suspicious structure survived. The polished abstraction did not.
That is the trouble with refactoring by appearance. Size, repetition, layer count, present scale, and an impressive name are useful reasons to investigate. They are terrible verdicts.
Surface smell is an accusation
Architecture review has a familiar visual grammar. Long functions want to be split. Repeated code wants a helper. A wrapper wants to justify its indirection. A mature subsystem in a young product looks overbuilt. A special case looks like the first member of a future framework.
Sometimes those instincts are right. The mistake is allowing the clue to decide the case.
A long function may be a linear orchestration whose individual operations are already well separated. Splitting it can move the same decision sequence across more files while making the whole harder to read. A mature security subsystem may be carrying guarantees that every exposed entry point genuinely needs. A special merge rule may be narrow because the data has narrow semantics, not because its author forgot to generalize.
The reverse mistakes are just as common. A layer can look architectural while merely renaming the thing beneath it. Several local implementations can look appropriately independent while quietly reproducing one policy with different edge cases. A new abstraction can promise consistency while adding a second representation that now has to remain consistent with the first.
Surface smell can begin an investigation. It cannot decide one.
I already use Salt Substitution Referenced concept Salt Substitution Remove a convenience or abstraction to expose what the underlying system can no longer carry without it. to describe the opening move: temporarily withdraw a convenience or abstraction and observe what the underlying system can no longer carry. But architectural survival is not yet an ownership decision. Once something has been removed, the important question is not only whether the system stands. Salt Substitution asks whether the system still stands. The responsibility trace asks something narrower:
Where did the work go?
Follow the responsibility
The constraint about redesign matters. If I remove a layer and immediately invent a cleaner replacement, I have hidden the evidence inside my preferred answer. The first pass should be deliberately literal. Take the structure away. Leave its consumers where they are. Account for what breaks and what must now be said repeatedly.
Start by naming the claims made on the structure’s behalf. Does it transform data? Enforce a guarantee? Coordinate a multi-step operation? Hide an unstable provider? Centralize a policy? Translate between two real models? Or does it merely make a call look more official?
Then remove it in thought, in a spike, or behind a reversible change. Follow each claimed responsibility separately.
If the callers become direct and no rule, protection, or transformation needs a new home, the complexity vanished. The structure was charging rent for no responsibility.
If every caller must now reproduce the same rule, the file vanished but the complexity survived. Deletion has not simplified the system. It has distributed the argument and made future disagreement cheaper.
If one large decision sequence remains one large decision sequence in the caller, decomposition did not remove complexity either. It changed its address—and probably added travel.
If the removed concern appears across several modules, with the same semantics, and changes for reasons independent of their core work, something more interesting has happened. The concern is real, but its current owner may be wrong.
RESPONSIBILITY TRACE
Where the work goes when the structure leaves
Remove the structure. Follow each obligation to its destination.
01 · Vanishes
Callers go direct
→ Delete
02 · Scatters
Every caller re-owns the rule
→ Preserve
03 · Recurs
Same semantics, many modules
→ Extract
04 · Stays local
Narrow semantics, narrow domain
→ Keep local
The trace is useful because it makes absence concrete. Instead of asking whether a layer feels unnecessary, I have to produce an inventory of what the system loses without it. Instead of defending a large module by pointing at its age or test suite, I have to show the work that would reappear elsewhere.
Delete it and watch where complexity goes.
Two removals, opposite answers
Consider a pass-through wrapper around stable store actions. It adds no subscription, transformation, validation, memoization, or compatibility boundary. Removing it changes imports and call sites, but it does not require a new policy. The direct calls are no less truthful than the wrapped ones.
The wrapper’s caller count is not evidence that it was load-bearing. It only shows that the empty room had many visitors.
Now consider rate limiting at a collection of public entry points. Present traffic may be small. The implementation may be extensive. But conceptually remove it and the responsibilities remain: distinguish actors, count attempts, apply limits, coordinate penalties, detect suspicious behavior, and clean up state. Either those obligations disappear from the product—which is a product decision, not a refactor—or they reappear independently at each entry point.
That does not prove every line of the existing subsystem is correct. It proves that the review cannot call the subsystem waste merely because its responsibility exceeds today’s load.
The same distinction applies to large orchestration functions. If the function is a sequence of already-deep operations and removing it forces the caller to own the same sequence, its length may be honest. If a branch inside it is a policy repeated across many orchestrators, that branch may deserve a different owner. The unit under review and the responsibility under review are not always the same size.
This is why “we deleted two thousand lines” is weak architectural evidence. A line can disappear because a responsibility disappeared, because a responsibility moved behind a deeper interface, because behavior was silently lost, or because the same work was spread into smaller pieces. The number alone cannot tell those stories apart.
Four ownership verdicts
The responsibility trace produces four useful outcomes.
Delete the ornament
The structure adds a name, hop, or ceremony but owns no transformation, policy, guarantee, coordination, or unstable boundary. Its removal makes the system smaller without making any caller more responsible.
Delete it. Do not preserve an empty abstraction because many files have learned its name.
Preserve the load-bearing body
The removed structure contains irreducible domain decisions or coordinates a sequence that must be understood together. Removing it scatters those decisions or forces its caller to absorb nearly the same context.
Preserve the responsibility. This is not a pardon for the current design. The module may still need clearer names, a smaller interface, better internal organization, or repaired tests. Passing the deletion test proves that work exists; it does not prove that its present owner performs it well.
Extract the reusable dress
The responsibility is real, but several bodies wear it. It recurs with consistent semantics and changes independently of each module’s central work. Authorization, retries, telemetry, validation, provider error classification, and presentation policy can take this form.
Extract only when shared ownership lowers total coordination cost. The second real consumer is much stronger evidence than the first imagined one. A concern that merely sounds cross-cutting is not yet infrastructure.
Keep legitimate behavior local
Sometimes a responsibility survives removal but belongs exactly where it is. A narrowly defined merge rule, a one-off translation, or a special validation can be local because its semantics are local. Generalizing it would force unrelated consumers through configuration and lifecycle machinery built for a family that does not exist.
Keep it local. One unusual module does not justify a costume department.
These verdicts separate two questions that reviews often collapse: is the responsibility real? and does this structure own it correctly? The first is answered by tracing what survives deletion. The second requires evidence about semantic cohesion, recurrence, independent change, attachment seams, and total coordination cost.
The undressing test
Here is the working method I now use in architecture review:
- Name the structure and every responsibility it claims to own.
- Remove it conceptually without improving the surrounding design.
- Trace each obligation into the callers, sibling modules, external systems, or product behavior where it reappears—or record that it vanishes.
- Classify the result as ornament, load-bearing body, reusable dress, or legitimate local behavior.
- For extraction, require recurrence, consistent semantics, independent change, a clear attachment seam, and lower total coordination cost.
- Choose one action: delete, extract, preserve, or explicitly defer.
The method is intentionally more demanding than “could this be abstracted?” Almost anything can be abstracted. The question is whether the abstraction would own a coherent decision and spare the system more coordination than it creates.
It is also more demanding than “did anything break?” A component can be load-bearing only because the rest of the system has been trained to depend on its accidental shape. The trace must name the surviving responsibility, not merely point at the surviving callers.
The same remove-and-observe move has a different polarity in verification. When I remove an architectural layer, survival can demonstrate strength and redistribution can expose responsibility. When I remove a safeguard that a test claims to verify, the test must fail; survival there exposes a toothless test. The gesture is shared. The defendant and the signal are not.
Architecture before the wardrobe
At design time, separating body from dress is useful before either becomes entangled. It is also where the metaphor can do the most damage.
Once a designer has named a concern “cross-cutting,” it becomes easy to build a framework for hypothetical consumers. The framework gains configuration, registration, lifecycle rules, adapters, and an extension vocabulary before a second real module asks to use it. Convolution arrives dressed as foresight.
The safer sequence is to recognize the possible seam, keep the first implementation narrow, and wait for evidence that the concern recurs. The second consumer can reveal what is actually shared. The third can show whether the shared semantics are stable or merely similar from a distance.
At maintenance time, the correct owner may become obvious before there is time to move the responsibility safely. That is not failure. A forced extraction during a high-pressure change can create more risk than an honestly bounded compromise.
But lack of time changes the schedule, not the architecture. Calling temporary ownership correct because the release is close destroys the evidence needed to repair it later.
Record the stitch
Put that record where the stitching exists: beside the interface, branch, or adapter that currently joins the responsibilities. A backlog item can link to it, but the backlog should not be the only place where the architectural truth lives.
The trigger matters. “Clean this up later” records mood, not intent. A useful trigger is observable: a second wearer arrives, the policy changes independently, the same defect appears twice, coordination cost crosses a named threshold, or the current seam blocks a required capability.
Without a trigger, deferral becomes a polite burial. With one, temporary ownership remains a falsifiable decision.
Where the test lies
The deletion test has a preservation bias. Almost any structure with callers can make work reappear when removed. A bad abstraction can pass simply because the codebase has accumulated dependencies on it.
That is why redistribution is evidence, not vindication. The positive case for preservation still has to identify a coherent responsibility. The positive case for extraction still has to show cheaper coordination and stable shared semantics. The positive case for local behavior still has to show why locality matches the domain.
There is also work that should disappear at the product level. If the system no longer promises a capability, deleting the code that served it may correctly delete the responsibility too. The trace should make that choice visible. It should not smuggle a product decision into an architecture cleanup.
Finally, conceptual deletion is not runtime evidence. It cannot prove that the edited system compiles, preserves behavior, resists concurrency, or survives production failure. It is a reasoning instrument for deciding what to test and what the design must continue to carry.
Used honestly, that is enough. It replaces an aesthetic argument with an accounting exercise. The wrapper does not survive because it looks clean. The safety layer does not die because it looks large. The shared helper does not exist because repetition offends us. Each structure has to show what it owns, where that work goes without it, and why its chosen home costs less than the alternatives.
Architecture is not the number of layers left standing after a cleanup. It is the truthful placement of responsibility.
Delete the costume. Then watch the work.
More from this theme
The table was full
A stateful design that omits a reachable input does not stay neutral; the default branch decides. And a state table can enumerate every cell of the wrong axis and still read as complete.
The backlog that no longer existed
A descriptive coordination artifact can keep directing work long after the implemented system has made its instructions false.