Refactoring Toward Better Design
Continuous Improvement
Read aloud is unavailable in this browser.
Beyond Episodes: Building Improvement into the Fabric
Refactoring is most effective not as an occasional 'cleanup sprint' but as a continuous practice woven into daily development. The most maintainable codebases aren't those that underwent a heroic refactoring effort—they're codebases where small improvements happen constantly, preventing decay from accumulating.
This page explores how to transition from episodic refactoring (the cleanup after things get bad) to continuous improvement (preventing things from getting bad in the first place). This mindset is what distinguishes teams that maintain velocity over years from those that slow to a crawl as technical debt compounds.
In LLD interviews, demonstrating awareness of continuous improvement signals that you're not just capable of good design—you understand how to sustain it over time in real engineering environments.
By the end of this page, you will understand how to integrate refactoring into regular development workflow, apply the Boy Scout Rule and opportunistic refactoring, manage technical debt strategically, create a culture of continuous improvement, and measure the impact of improvement efforts.
The Cost of Delayed Improvement
Understanding why continuous improvement matters requires appreciating the compounding cost of delayed maintenance. Technical debt isn't free—it charges interest.
The compound interest of technical debt:
When you defer a refactoring, the cost doesn't stay fixed. It grows:
- Immediate cost: Time saved by not refactoring now
- Hidden cost: Additional complexity navigated on each subsequent change
- Coupling cost: Other code couples to the suboptimal design, making future refactoring harder
- Knowledge cost: Understanding of why the code is structured this way fades
- Compound cost: Each problem makes other problems harder to fix
| Timing | Relative Cost | Why |
|---|---|---|
| During initial development | 1x | Code is fresh in mind, scope is contained, tests are being written |
| Within the same week | 2-3x | Context still mostly available, fewer dependencies accumulated |
| Within the same quarter | 5-10x | Context must be rebuilt, callers have adapted to the smell |
| After a year | 10-50x | Original developer may have left, design rationale forgotten, extensive coupling |
| Legacy code (years) | 50-100x+ | Archaeology required, unknown side effects, fear of change |
The deferred maintenance trap:
Teams often justify deferred refactoring with 'we'll pay it back later.' But later never comes:
- New features are always more visibly valuable than maintenance
- Pressure to ship never decreases—it compounds
- The refactoring scope grows, making it harder to schedule
- Team members who knew the code move on
- Eventually, 'later' becomes 'never' or 'rewrite'
The alternative: Pay small amounts continuously. Like financial investing, consistent small contributions outperform occasional large ones. Refactor a little with every feature, and technical debt never accumulates to crisis levels.
'We'll schedule a refactoring sprint next quarter' is often a polite way of saying 'never'. Large scheduled refactorings compete with features for priority, and features usually win. Continuous small improvements don't require explicit scheduling—they're woven into feature work.
The Boy Scout Rule: Leave It Better Than You Found It
The Boy Scout Rule, credited to Robert Martin, provides a simple heuristic for continuous improvement:
"Always leave the campground cleaner than you found it."
Applied to code: whenever you touch a file to add a feature or fix a bug, improve something small before moving on. The improvement need not be related to your change—just make the code a little better.
What 'better' means:
- •Rename a confusing variable — 5 seconds, immediate readability improvement
- •Extract a method — 2 minutes, reduces complexity of the file
- •Add a clarifying comment — 30 seconds, documents non-obvious logic
- •Fix a minor code smell — 5 minutes, prevents future confusion
- •Add a missing test case — A few minutes, increases coverage
- •Update outdated documentation — 2 minutes, prevents future developers from being misled
- •Remove dead code — 1 minute, reduces maintenance burden
- •Improve error messages — 2 minutes, helps future debugging
The cumulative effect:
If every developer makes one small improvement on every file they touch, the effect compounds:
- 5 developers × 10 files touched/day × 250 working days = 12,500 improvements/year
- High-traffic files get improved frequently
- Low-traffic files (less important anyway) improve when needed
- The codebase trends toward clarity instead of toward chaos
Practical guidelines:
- Keep improvements small (under 10 minutes)
- Improvements should not alter behavior (pure refactoring)
- Improvements in the same commit are fine if clearly labeled
- If a bigger issue is found, note it for later (don't scope-creep)
If an improvement takes less than 10 minutes, do it now. If it takes more, create a task and continue with your original work. This prevents refactoring from derailing feature work while capturing improvement opportunities.
Opportunistic Refactoring: Improvement in Context
Opportunistic refactoring extends the Boy Scout Rule to larger improvements that arise naturally during feature work. When you're already working in an area and understand it deeply, that's the optimal time to improve it.
The 'preparatory refactoring' opportunity:
When implementing a feature, you often encounter code that would be easier to work with if structured differently. Instead of fighting the existing structure:
- Pause the feature work
- Refactor to make the feature straightforward
- Resume the feature work in the improved environment
- The feature is completed more quickly, and the code is better
- •Code you're about to modify anyway
- •Logic you just finished understanding deeply
- •Patterns you've repeated for the third time
- •Tests that are hard to write for the code
- •Bug fixes that reveal structural issues
- •Cross-cutting concerns during feature implementation
- •Code you're just reading, not modifying
- •Areas outside your current focus
- •Improvements that block urgent bug fixes
- •Large restructuring during time-sensitive projects
- •Refactoring for its own sake (gold plating)
- •Unfamiliar code without proper context
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// SITUATION: Adding notification preferences feature// You're modifying UserService anyway, and you notice a problem class UserService { // BEFORE: You encounter this while adding notification preferences async updateUser(userId: string, changes: any): Promise<void> { const user = await this.userRepository.findById(userId); // Existing code: lots of duplication with poor structure if (changes.name) { user.name = changes.name; await this.auditLog.log('name_changed', userId); } if (changes.email) { user.email = changes.email; await this.auditLog.log('email_changed', userId); await this.emailService.sendVerification(user.email); } if (changes.phone) { user.phone = changes.phone; await this.auditLog.log('phone_changed', userId); } // ... 10 more similar blocks // You need to add notification preferences here // Adding more duplication makes this worse // OPPORTUNISTIC REFACTORING: Improve structure first } // AFTER: Refactored, then feature added cleanly async updateUser(userId: string, changes: UserChanges): Promise<void> { const user = await this.userRepository.findById(userId); const handlers = this.buildChangeHandlers(user); for (const [field, value] of Object.entries(changes)) { if (value !== undefined && handlers[field]) { await handlers[field](value); } } await this.userRepository.save(user); } private buildChangeHandlers(user: User): Record<string, (value: any) => Promise<void>> { return { name: async (value) => { user.name = value; await this.auditLog.log('name_changed', user.id); }, email: async (value) => { user.email = value; await this.auditLog.log('email_changed', user.id); await this.emailService.sendVerification(value); }, // NEW: notification preferences added cleanly notificationPreferences: async (value) => { user.notificationPreferences = value; await this.auditLog.log('notification_preferences_changed', user.id); }, }; }} // The refactoring happened when you were already deep in this code// Total time: 30 minutes of refactoring + 10 minutes for feature// Without refactoring: 10 minutes for feature, but harder next time// Net: Small upfront cost, ongoing paybackWhen refactoring opportunistically, note in your commit message or PR that you improved the code 'in the neighborhood' of your feature. This helps reviewers understand why the change is larger than expected and validates the practice for the team.
Strategic Technical Debt Management
Not all technical debt should be paid immediately. Like financial debt, technical debt is a tool that can be used strategically. The key is intentionality—knowing what debt you're taking on, why, and when you'll pay it back.
Types of technical debt:
| Deliberate | Inadvertent | |
|---|---|---|
| Prudent | "We must ship now and deal with consequences" (acceptable trade-off) | "Now we know how we should have done it" (learning) |
| Reckless | "We don't have time for design" (dangerous attitude) | "What's layering?" (knowledge gap) |
Managing the debt portfolio:
-
Track it explicitly: Maintain a list of known technical debt with estimated severity and location. This prevents debt from being invisible.
-
Prioritize by impact × opportunity: Pay down debt that affects frequently changed code first. Debt in stable, rarely touched code is low priority.
-
Budget for debt payment: Allocate a percentage of each sprint to debt reduction (commonly 10-20%). This ensures continuous progress.
-
Don't let interest compound: Address debt before it spreads. Problematic patterns have a tendency to be copied.
-
Know when to declare bankruptcy: Sometimes code is so indebted that incremental payment is inefficient. Strategic rewrite (with careful planning) may be appropriate.
- •Location:
src/services/PaymentProcessor.ts - •Description: Switch statement handles payment methods; adding new method requires modifying existing code (OCP violation)
- •Impact: Each new payment method requires 2-3 days extra work
- •Frequency: New payment methods added ~3x per year
- •Est. Remediation: 1 week to refactor to Strategy pattern
- •Priority: High (pays back in ~4 months)
- •Trigger: Refactor when next payment method is added
Some teams establish a 'debt ceiling'—a maximum amount of tracked technical debt. When the ceiling is reached, no new debt can be taken on until existing debt is paid down. This prevents debt from accumulating to crisis levels.
Creating a Culture of Continuous Improvement
Continuous improvement isn't just an individual practice—it's a team and organizational capability. Creating a culture that supports and rewards improvement requires deliberate effort.
Organizational enablers:
- •Code Review as Learning — Reviews should suggest improvements, not just find bugs. 'This works, but have you considered extracting this method?' creates learning moments.
- •Blameless Postmortems — When issues arise from technical debt, focus on systemic causes and prevention, not blame. This creates safety for honesty about code quality.
- •Celebrate Improvement — Recognize and appreciate team members who improve code quality, not just those who ship features. This signals organizational values.
- •Budget Time Explicitly — Allocating specific time for improvement (20% rule, Hack Week, Cleanup Friday) legitimizes the activity and removes guilt.
- •Track Improvement Metrics — Measure and discuss metrics like test coverage trend, time between commits, or defect rates. What gets measured gets managed.
- •Pairing on Refactoring — Pair programming on refactoring spreads knowledge and techniques. Less experienced developers learn by watching.
Overcoming resistance:
Teams often resist continuous improvement for understandable reasons:
-
"We don't have time" → Small improvements don't require scheduled time; they're part of feature work. 10 minutes saved later justifies 2 minutes now.
-
"It's not my code" → Collective ownership means everyone improves everything. Silos prevent improvement.
-
"It's too risky" → With proper testing and small steps, refactoring is lower risk than leaving code to decay.
-
"Management won't understand" → Frame improvement in business terms: faster delivery, fewer bugs, lower costs. Make the value visible.
-
"It's just cosmetic" → Good names, clear structure, and proper abstractions directly affect delivery speed. Clarity is not cosmetic.
Cultural change starts with individual action. If you consistently apply the Boy Scout Rule, explain improvements in PRs, and advocate for code quality, others notice and follow. Senior engineers set the tone for team culture.
Measuring Improvement: Knowing You're Getting Better
Improvement efforts should be measurable. Without metrics, it's hard to know if practices are working or just creating activity without value. The right metrics provide feedback and justify continued investment.
Direct quality metrics:
| Metric | What It Measures | Improvement Signal |
|---|---|---|
| Cyclomatic Complexity | Number of independent paths through code | Trending down over time in frequently changed files |
| Test Coverage | Percentage of code exercised by tests | Increasing coverage, especially on critical paths |
| Code Churn | How often files are modified | Concentrated churn (hotspots) decreasing over time |
| Coupling Metrics | Dependencies between modules | Reducing incoming dependencies on problematic modules |
| Duplication | Amount of repeated code | Decreasing copy-paste code across codebase |
| Technical Debt Ratio | Est. remediation time / development time | Ratio staying stable or decreasing |
Outcome metrics (the ones that matter most):
Ultimately, code quality metrics are proxies for business outcomes:
- Lead Time: Time from commit to production. Better code enables faster delivery.
- Change Failure Rate: Percentage of changes causing incidents. Cleaner code has fewer bugs.
- Time to Restore: How long to recover from failures. Understandable code is fixed faster.
- Developer Velocity: Story points or features per sprint trend. Healthy code enables sustained velocity.
The feedback loop:
- Measure baseline quality metrics
- Apply improvement practices
- Measure again after a period (quarter, year)
- Correlate with outcome metrics
- Adjust practices based on what works
Metrics can be gamed—100% coverage with meaningless tests, complexity reduced by moving it elsewhere. Use metrics as indicators, not targets. Combine with qualitative assessment: Is the code actually easier to work with? Are developers happier? Do code reviews go faster?
Demonstrating Continuous Improvement in LLD Interviews
LLD interviews often include opportunities to demonstrate refactoring awareness and continuous improvement thinking. Knowing how to discuss these topics signals engineering maturity.
Common interview scenarios:
- •"How would you improve this design?" → Discuss specific refactoring patterns you'd apply and why. Reference code smells you identified.
- •"What if requirements change?" → Explain preparatory refactoring: 'I would first refactor the structure to make this extension natural, then add the new feature.'
- •"Tell me about a time you improved existing code." → Have a concrete story with measurable impact: 'Reduced complexity from 30 to 8, enabling new hires to contribute 2 weeks faster.'
- •"How do you balance refactoring with feature work?" → Discuss opportunistic refactoring, the Boy Scout Rule, and time-boxing strategies.
- •"This code has no tests. What do you do?" → Explain characterization testing: 'I would write tests that capture current behavior before making structural changes.'
- •"How do you convince a team to prioritize refactoring?" → Frame in business terms: developer velocity, bug rates, onboarding time. Show don't tell.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// INTERVIEWER: "Here's some code for a notification system.// What improvements would you suggest?" class NotificationManager { send(userId: string, type: string, message: string): void { if (type === 'email') { const user = this.db.query(`SELECT email FROM users WHERE id = '${userId}'`); // ... 20 lines of email sending } else if (type === 'sms') { const user = this.db.query(`SELECT phone FROM users WHERE id = '${userId}'`); // ... 20 lines of SMS sending } else if (type === 'push') { const user = this.db.query(`SELECT device_token FROM users WHERE id = '${userId}'`); // ... 20 lines of push notification } this.db.query(`INSERT INTO notification_log ...`); }} // STRONG RESPONSE:// "I see several opportunities for improvement: // 1. **SQL Injection vulnerability** - The template literals allow injection.// I'd use parameterized queries. This is a bug fix, not refactoring. // 2. **Extract Method** - Each notification type's logic should be extracted.// This reduces the method's complexity from ~60 lines to 10. // 3. **Replace Conditional with Polymorphism** - The type-checking suggests// a Strategy pattern. Each notification channel becomes a class implementing// a NotificationChannel interface. // 4. **Extract Repository** - User lookup should move to a UserRepository.// This separates concerns and improves testability. // 5. **The logging at the end** - This cross-cutting concern could be handled// by a decorator or event, making the core logic cleaner. // If I were to tackle this incrementally:// Step 1: Extract methods for each notification type (same behavior, cleaner structure)// Step 2: Create interface and classes for Strategy pattern// Step 3: Extract UserRepository// Step 4: Consider @Logged decorator or event for audit trail // Each step is independently valuable and keeps the code in a working state."Interviewers appreciate when you prioritize improvements. Don't just list everything wrong—explain which improvement you'd do first and why. 'The SQL injection is a security issue, so I'd fix that immediately. The Extract Method refactoring I'd do opportunistically when next in this code.'
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.