Refactoring changes HOW code works, not WHAT it does.
// ✅ CORRECT: Preserve original null-on-error behaviorfunctiongetUser(id: string): User | null {
const result = findUserById(id); // Extracted to helperreturn result ?? null; // Same null-on-error behavior
}
// Rule: if behavior must change, do it in a separate commit AFTER refactoring
Use automation for mechanical transformations. Save manual effort for logic improvements.
# Run jscodeshift codemod across entire codebase
npx jscodeshift -t codemod-replace-moment-with-datefns.js src/
# See references/advanced-techniques.md for full codemod script example
✅ REQUIRED: Dependency Snapshot for Rollback Safety
Lock dependencies before refactoring to prevent environment variables.
// ❌ WRONG: All users immediately use new code (HIGH RISK)exportconst paymentService = newNewPaymentService();
// ❌ WRONG: Massive refactor in one commit (500 files changed)// Unreviewable, hard to rollback, high risk
Decision Tree
Need to improve code?
↓
Is there 80%+ test coverage?
NO → Add tests first OR Accept debt with monitoring
YES → Continue
↓
Will code change frequently in next 6 months?
NO → Accept debt (document for future)
YES → Continue
↓
Can behavior be preserved?
NO → REWRITE (new requirements)
YES → Continue
↓
Is technology stack obsolete?
YES → REWRITE with migration pattern
NO → Continue
↓
Estimated effort?
1-4 weeks → REFACTOR (quick wins)
1-3 months → REFACTOR (medium-term)
3-12 months → REWRITE or phased REFACTOR
>12 months → Accept debt, break into phases
↓
Calculate ROI
>200% first month → REFACTOR immediately
100-200% in 3 months → REFACTOR next sprint
50-100% in 6 months → Schedule for next quarter
<50% in 6 months → Accept debt, revisit annually
Example
JavaScript to TypeScript
// Phase 1: Add type definitions to public APIs// BeforeexportfunctioncalculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// After Phase 1exportfunctioncalculateTotal(items: Array<{ price: number }>): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Phase 2: Replace any with explicit typesinterfaceDataItem { value: number; label: string; }
functionprocessData(data: DataItem[]): number[] {
return data.map((item) => item.value);
}
// Phase 3: Enable strict mode (tsconfig.json)// { "compilerOptions": { "strict": true, "noImplicitAny": true } }
Partial migration state: Use feature flags to run old + new code in parallel. Monitor error rates for both implementations.
Breaking changes unavoidable: Create deprecation warnings 2+ versions before removal. Document migration guide with before/after examples.
Rollback during production incident: Feature flags enable instant rollback without code deploy. Monitor metrics: error rate, latency, throughput.
Test coverage gaps: Don't refactor. Either add tests first (separate initiative) or accept tech debt with monitoring.
Circular dependencies during refactor: Indicates poor separation of concerns. Introduce dependency injection or event-driven architecture.
Performance regression: Benchmark before/after with realistic data. If regression >10%, investigate optimization or revert.
Merge conflicts during long-running refactor: Rebase frequently (daily) to stay in sync with main branch. Use git rerere to remember conflict resolutions.
Flaky tests exposed during refactor: Don't fix during refactor. Document flaky tests in separate issue. Refactor assumes stable test suite.
Refactoring reveals bugs in original code: Stop refactoring. Fix bugs first (separate commit/PR), THEN resume refactor. Never mix bug fixes with refactoring.
Team members need original code during refactor: Use feature branch + feature flag. Original code remains accessible until migration complete.