Skip to main content
Refactoring Safely: A Practical Checklist
Back to blog

Refactoring Safely: A Practical Checklist

Every team I've asked about dead-code removal has a story about a broken staging endpoint or test paths that stopped running. Usually, the damage isn't catastrophic, but remembering it makes later removals political, so leaving code alone feels safer.

Bad removals usually follow one pattern: someone spots a seemingly unused function, deletes it, and skips the verification that would reveal its dependency. Deletion wasn't the issue. The missing gate was.

This post lists those gates in the order they should run.

Gate 1: Know Your Test Coverage

Before deleting anything, learn what your test suite actually exercises. "We have tests" is not the same as "our tests cover the paths next to the code being removed."

A silent break is found only when a test traverses the affected path. If integration tests cover 60% of the codebase by line, the other 40% can break or be deleted without a failure. The most dangerous removals hide there.

Before removal work begins, run a coverage report across the full test suite. It shows where you are flying blind. Automated removal is riskier on paths without coverage. YAGNI puts coverage data beside its dead-path analysis so you can distinguish candidates in well-covered territory from those in the dark.

Gate 2: Check the Dependency Boundary

Dead code does not always mean code with no callers. Its callers may be dead too. Removing a function called only by other dead functions is safe; removing one called by live code is not.

The idea gets harder with mixed-liveness modules. A module may hold five functions: two live, two dead, and one called only by the dead pair. The safe set includes the two clearly dead functions and the one that exists only for them. A module boundary does not determine removability.

For each candidate, trace every inbound call. If all callers are also removal candidates, it is safe from a call-chain perspective. If one caller is live, the candidate is not yet removable and must be tracked as blocked until that caller is addressed.

Gate 3: Check External Interfaces

Static analysis can label functions dead even when hidden mechanisms keep them live: a REST endpoint definition checked by an API client in another repository, a queue message handler matched to a string key published by a separate service, or a webhook callback registered in a third-party platform configuration.

These cases are real. In early-access pilots where we ran YAGNI, we found handler functions with zero internal callers that remained live because an external job queue published events to their named handlers. Their names lived in the queue configuration, not in a source file the analyzer could reach.

Part of the external interface check is manual. For every high-confidence dead candidate handling messages, requests, or events of any kind, someone must confirm that no external system addresses it by name or path. YAGNI marks these cases "verify externally" instead of proposing automatic removal.

Gate 4: Review Recent Commit History

A function added three weeks ago that now looks dead differs from one unused for three years. Recency matters for two reasons.

First, a new function may belong to a feature still in development. It is dead in the current production build but becomes live when the feature ships. Removing it deletes work in progress. YAGNI checks the git blame timestamp for candidates and uses a recency threshold: code added or modified in the last 30 days is excluded from automated removal proposals.

Second, recent changes to a function that remains dead may indicate active work in that area. The safest response is to show the candidate to a human reviewer instead of automating its removal.

Gate 5: Run Every Test, Not Only Unit Tests

This procedural step is often skipped: after a removal proposal is generated, the full test suite must pass against the proposed change before opening the pull request.

Unit tests by themselves are insufficient. Because they mock dependencies, they can pass after a deletion that breaks an integration path. End-to-end and integration tests catch removals that disrupt runtime behavior. If the suite has tiers, all should pass, not only the fast tier.

YAGNI will not open a pull request when any test fails against the proposed change. Tests are the gate, not a nice-to-have. From our perspective, this is non-negotiable. A tool that ships removals before tests pass is simply a fast way to break your codebase.

Gate 6: Check Serialized Formats and Schema Contracts

Serialization often creates a hidden dependency. A class field never assigned anywhere in the codebase may still belong to a serialized format read by external consumers. Removing it breaks deserialization for a client whose stored or transmitted payload includes it.

This happens often with data models persisted to a database or written to message queue records. New code may no longer write a model-class field, while millions of existing rows still contain it. If deserialization expects the field might be present, removing its definition breaks reads of old records.

Checking schema contracts means knowing your serialization boundaries and whether stored data might reference the code being removed. YAGNI searches for decorator patterns marking serialized fields, ORM field definitions, and protobuf/Avro/JSON Schema definitions, then flags overlapping candidates for explicit review.

What This List Isn't

This checklist does not oppose automated removal. It supports removal with gates. Manual deletion that skips them fails for the same reasons automated removal would. The gates are not about automation being unsafe; they specify what must be verified before code leaves the codebase.

Teams that apply these gates consistently, whether removal is manual or automated, see substantially fewer removal incidents than teams treating deletion as low-stakes because "it's just dead code." Incidents occur when dead code was actually live. The gates are there to catch that case.

Try YAGNI on your codebase. Free plan, no credit card required.

Get Early Access

More from the blog

View all articles