Technical Debt Management and Refactoring Questions
Identifying, prioritizing, and paying down technical debt sustainably. Covers recognizing debt, making the case to invest in it, refactoring safely behind tests, and balancing debt reduction against feature velocity. Includes keeping a codebase maintainable over the long term.
You must decide whether to rewrite or incrementally refactor a system suffering from memory leaks and frequent restarts. Outline a decision framework that includes discovering what the system actually does today, cost estimation, risk profiling, a rollback plan, testing requirements for either path, and a stakeholder communication plan.
Sample Answer
Direct answer
For a system with memory leaks and frequent restarts, don't jump to a rewrite: first characterize what the system actually does today (since "suffering from memory leaks" is a symptom, not necessarily evidence the whole design is wrong), estimate cost and risk for both paths, and require a rollback plan and testing strategy before committing either way.
Structured elaboration
- Discover current behavior first: profile the memory leak specifically (which allocation pattern, which code path) before assuming it requires a full rewrite; many "the whole system is broken" symptoms trace to a small number of specific, fixable causes (an unclosed resource handle, an unbounded cache) that an incremental fix resolves without touching the rest of the design.
- Cost estimation for both paths: incremental fix cost is usually estimable directly (profile, patch, verify); rewrite cost requires estimating re-implementation of every behavior the current system has, which is systematically harder to estimate accurately precisely because some of that behavior isn't documented anywhere except the existing code.
- Risk profiling: a rewrite of a serving system carries cutover risk (a new set of bugs, however well-tested) on top of whatever risk motivated the rewrite; an incremental fix carries lower cutover risk but the possibility the underlying design genuinely can't be patched into good health.
- Rollback plan: for the incremental path, ship the fix behind a flag or as a small, independently revertible commit, so a bad patch can be pulled within minutes without redeploying the rest of the service; for the rewrite path, keep the old implementation live and swappable (a feature flag or traffic split) for a defined post-cutover window, with an explicit automatic trigger (a crash-rate or memory-growth threshold) that forces a revert to the old path rather than relying on a judgment call made under incident pressure.
- Testing requirements for either path: for the incremental path, a load test that specifically reproduces the leak pattern, run before and after the fix, to prove it's actually resolved rather than just less frequent; for the rewrite path, a full behavioral parity test suite built from the current system's observed behavior.
- Stakeholder communication: state the finding from step 1 plainly, since "we profiled it and found a specific fixable cause" is a much easier sell than "we need to rewrite this" and should be the default recommendation whenever the profiling supports it.
Worked example
Profiling reveals the memory leak traces to a single connection pool that isn't releasing connections on a specific error path, present in roughly 15% of requests under load. That's a targeted, incremental fix (patch the error-handling path, add a regression test that specifically exercises it under load), not evidence the serving layer's overall design is unsound. The frequent restarts are a symptom of this one leak, not of pervasive architectural rot, so the decision framework correctly routes to incremental refactor once the discovery step is done, even though the initial framing ("suffering from memory leaks and frequent restarts") sounded like a case for a rewrite.
Trade-offs & pitfalls
The pitfall this question is testing is reacting to a scary-sounding symptom (frequent restarts) with a scary-sounding fix (full rewrite) without first doing the cheap diagnostic work that, in a large fraction of real cases, reveals a narrower, more tractable root cause.
Given a PostgreSQL builds table (build_id primary key, team_id, duration_seconds int, status enum('success','failure'), created_at timestamp) and a tests table (test_id, build_id foreign key, name, result enum('pass','fail'), duration_ms int), write a SQL query that returns the weekly build failure rate and average build duration per team for the last 12 weeks. Explain your key choices, including how you handle weeks with zero builds for a team.
Sample Answer
Direct answer
Join the two tables on build_id, bucket by week using the created_at timestamp, and compute the failure rate as failed builds divided by total builds per team per week, handling weeks with zero builds by simply not producing a row for them (an explicit zero-fill requires a separate calendar table, which is worth doing only if downstream reporting needs a continuous week axis).
Structured elaboration
Key choices: bucket weeks starting Monday (date(created_at, 'weekday 0', '-6 days') in SQLite, or date_trunc('week', created_at) in Postgres) for a stable, standard definition; compute failure rate as a ratio rather than a raw failure count, since raw counts aren't comparable across teams with different build volumes; and group by both team_id and week so the result is one row per team per week, ready to plot as a trend line.
WITH weeks AS (
SELECT build_id, team_id, duration_seconds, status,
date_trunc('week', created_at) AS week_start
FROM builds
WHERE created_at >= now() - interval '12 weeks'
)
SELECT team_id,
week_start,
COUNT(*) AS total_builds,
SUM(CASE WHEN status = 'failure' THEN 1 ELSE 0 END) AS failures,
ROUND(1.0 * SUM(CASE WHEN status = 'failure' THEN 1 ELSE 0 END) / COUNT(*), 2) AS failure_rate,
ROUND(AVG(duration_seconds), 1) AS avg_duration_seconds
FROM weeks
GROUP BY team_id, week_start
ORDER BY team_id, week_start;
(Postgres syntax shown; the equivalent SQLite form uses date(created_at, 'weekday 0', '-6 days') in place of date_trunc('week', created_at).)
Worked example
Executed against SQLite with a small seeded fixture (builds rows for teamA with 3 builds in one week, 2 of them failures, plus 1 build two calendar weeks earlier; teamB with 2 builds, both successes, in the same recent week):
team_id week_start total_builds failures failure_rate avg_duration_seconds
teamA 2026-06-15 1 0 0.0 310.0
teamA 2026-06-29 3 2 0.67 360.0
teamB 2026-06-29 2 0 0.0 190.0
The query correctly separates teamA's two distinct weeks (0% failure the first week, 67% the second) and shows teamB has no failures in the window it built during; teamB has no row for the earlier week, which is the "zero builds for a team" case handled by simply omitting that row rather than fabricating a misleading 0/0 ratio.
Trade-offs & pitfalls
Omitting rows for weeks with zero builds is correct for THIS query's purpose (showing the trend where activity happened), but if the consuming dashboard needs a continuous week axis (to visually show a team went quiet, not just that no data exists), that requires an explicit generate_series/calendar join to zero-fill, which is a deliberate design decision to make, not an oversight to silently patch over.
Describe a decision checklist you would use before deliberately accepting new technical debt to meet a launch date, for example by skipping some integration tests. Include the criteria to evaluate, which stakeholders must sign off, what documentation you would require (including a repayment plan), and the maximum lifespan you would allow the accepted debt to have before it must be revisited.
Sample Answer
Direct answer
A decision checklist for deliberately accepting debt needs four things: explicit criteria for what makes debt acceptable right now, a named sign-off from both engineering and the business stakeholder requesting the deadline, required documentation including a repayment plan, and a hard maximum lifespan after which the debt must be revisited regardless of other priorities.
Structured elaboration
- Criteria to evaluate: is the affected area isolated (low blast radius if something goes wrong) or does it sit on a critical path? Is the shortcut reversible (can it be safely undone if it causes problems) or does it lock in a hard-to-change decision? What's the realistic worst case if this debt is never paid down?
- Required sign-off: both an engineering lead (confirming the technical risk assessment) and the business stakeholder driving the deadline (confirming they understand and accept the trade-off, not just that they want the date met), so the acceptance isn't a unilateral engineering decision nor a business decision made in ignorance of the risk.
- Required documentation: what specifically is being skipped, why, the acceptance criteria from step 1, and a repayment plan with an owner and target date, filed as a ticket at the moment of acceptance.
- Maximum allowed lifespan: a hard date (not "soon") after which the debt is automatically escalated for review regardless of competing priorities; a common default is one quarter for most debt, shorter for anything touching a critical or customer-facing path.
Worked example
A team wants to skip load-testing a new endpoint to hit a launch date. Checklist walkthrough: isolated (yes, new endpoint, no existing traffic depends on it) and reversible (yes, can be feature-flagged off); worst case if debt persists is a capacity incident under unexpectedly high launch-day traffic. Sign-off: engineering lead confirms the technical risk is bounded given current traffic projections, and the product stakeholder explicitly acknowledges the capacity risk in writing. Documentation: ticket filed noting the skipped load test, owner assigned, repayment (running the load test) scheduled for the following sprint. Maximum lifespan: two weeks, shorter than the default quarter, because this item touches a customer-facing launch path.
Trade-offs & pitfalls
A checklist with no enforcement mechanism (no actual sign-off required, no automatic escalation at the lifespan deadline) becomes theater that gets rubber-stamped under the same deadline pressure it's meant to check; the maximum-lifespan escalation needs to be automated (a bot, a recurring calendar review) rather than relying on someone remembering to revisit it.
You must decide whether to rewrite a legacy module or incrementally refactor it. Using a structured decision framework, list the criteria you would evaluate, how you would estimate each, and the decision thresholds you would use to choose one approach over the other.
Sample Answer
Direct answer
Decide with four criteria evaluated explicitly, not by gut feel: measurable risk of the change, customer impact if something goes wrong, effect on ongoing developer productivity, and time to deliver either path. Default to incremental refactor; only choose a full rewrite when the criteria clearly and jointly favor it, since rewrites systematically underdeliver against their promised timeline.
Structured elaboration
| Criterion | Favors incremental refactor | Favors full rewrite |
|---|---|---|
| Risk | Existing behavior partially understood, tests exist or can be added incrementally | Existing behavior is essentially undocumented AND untestable in place |
| Customer impact | Feature delivery must continue in parallel | The module is isolated enough that a parallel rewrite doesn't block other work |
| Developer productivity | The pain is localized (one team, one service) | The pain is systemic and actively blocking multiple teams |
| Time to deliver | Any bounded time horizon | Only when there's genuine slack (a rewrite with a hard deadline is a red flag, not a plan) |
A useful decision threshold: choose full rewrite only if you can characterize the CURRENT system's behavior well enough to know what "done" means for the rewrite (via characterization tests or a clear spec), and if you can ship the rewrite behind a flag with a real rollback path. If either of those isn't true, the risk of a rewrite is understated no matter how bad the existing code looks. Estimating each criterion concretely, not just qualitatively, is part of the framework too: risk from the incident count traced to the module over the last 6 months (more incidents, higher risk); customer impact from the percentage of traffic or revenue that touches the affected code path; developer productivity from the engineer-hours per month currently spent on workarounds in that module; and time to deliver from an engineer-weeks estimate for each path (an incremental fix is estimated directly from the known change, while a rewrite is estimated from re-implementing every currently-used behavior, which is why rewrite estimates are systematically less reliable).
Worked example
A legacy pricing module: undocumented but has decent test coverage (72%), is used by three other services, and the team has two sprints of slack before the next major deadline. Risk is moderate (tests exist), customer impact of an incremental approach is low (small, reviewable changes), developer productivity impact is currently moderate (the module is annoying but not blocking), and the time-to-deliver criterion clearly favors incremental (a rewrite would need to reproduce three services' worth of edge cases from scratch). Two sprints might look like the "genuine slack" the table's threshold calls for, but it isn't enough here: reproducing three services' worth of undocumented edge cases from scratch is realistically a multi-month effort, not a two-sprint one, so the slack criterion is not actually satisfied for THIS rewrite even though slack exists in the abstract; genuine slack means enough runway for the scope of rewrite under consideration, not just any nonzero buffer. Verdict: incremental refactor, prioritizing tests where coverage gaps exist first.
Trade-offs & pitfalls
The classic mistake is choosing rewrite because the existing code is unpleasant to work in, which is a developer-experience signal, not a risk/customer-impact/time signal. A second-order trap: a rewrite that starts as "just the pricing logic" and scope-creeps into touching everything the original module touched, at which point its risk profile has silently become worse than the incremental path it was chosen over.
You join a team that has a backlog of technical debt slowing feature development, and you are not the tech lead. Propose a plan to take the initiative to reduce that debt over the next two quarters while maintaining feature velocity, including how you would get buy-in, schedule the work, and measure success.
Sample Answer
Direct answer
Without formal authority, reduce debt by starting small and visible: pick one bounded, low-risk item, fix it, show the measurable before/after, and use that credibility to earn support for a slightly larger next item, rather than trying to launch a two-quarter program from a position with no mandate.
Structured elaboration
- Get informal buy-in first: talk to the tech lead and a couple of teammates about the specific pain point before doing anything, so the work isn't a surprise and ideally has at least tacit support.
- Pick a bounded first item: something completable in days, not weeks, with a clearly measurable before/after (a flaky test suite, a slow build step), so the win is undeniable and quick.
- Schedule it inside normal work, not as a separate ask: fold it into an existing sprint alongside regular feature work, framing it as "cleanup while I'm in this area" rather than requesting dedicated time upfront, which avoids needing permission you don't yet have standing to ask for.
- Make the result visible: a short note in the team channel or standup with the concrete before/after number, which builds the track record needed to propose something bigger next.
- Escalate gradually: use the credibility from 2-3 small wins to propose a slightly larger, more visible item, and eventually a real capacity ask to the tech lead or EM, now backed by a track record rather than a cold pitch. Treat the first quarter as earning trust via small, visible wins, and the second quarter as converting that trust into a modest, standing capacity allocation, so the full two-quarter window ends with both delivered debt reduction and a repeatable process, not just a one-off flurry of early activity.
Worked example
Week 1: fix a specific, well-known flaky test that's been annoying the whole team, reducing CI failure rate on that suite from 15% to under 2%, posted with the before/after number in the team channel. Week 3: propose and complete a small build-time optimization (caching a slow dependency step), cutting build time by 4 minutes, again shared visibly. By month 2, propose a slightly larger item (splitting an overloaded test suite) to the tech lead, now backed by two concrete, credible wins rather than a first-time cold ask; the tech lead is far more likely to grant informal time for this third item given the track record. By the end of Quarter 1, that track record (3-4 completed, visible wins) is used to formally propose a modest recurring capacity allocation (for example, one day every two weeks) for Quarter 2, framed as a request backed by evidence rather than a speculative ask. Quarter 2 is then spent executing one pre-agreed, larger item (for example, decomposing the single largest source of recurring friction) using that allocated time, reporting progress at the same visible cadence as the earlier small wins, so that by the end of the two-quarter window the team has both delivered debt reduction throughout and established a durable process that survives beyond the initial push.
Trade-offs & pitfalls
The risk of not being the tech lead is starting too big: proposing a two-quarter debt-reduction program without any track record or mandate is likely to be politely ignored or actively resisted as scope creep from someone without the standing to drive it; earning trust through small, visible wins first is what makes the eventual larger ask land.
Unlock Full Question Bank
Get access to all 20 Technical Debt Management and Refactoring interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.