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.
What are the most common anti-patterns organizations fall into when they try to pay down technical debt? Pick the three you consider most damaging, explain the consequences of each, and propose a pragmatic alternative for each.
Sample Answer
Direct answer
The three most damaging anti-patterns: one-off hacks presented as the plan (a shortcut framed as a fix, with no repayment plan, so it becomes permanent by default), big-bang rewrites (a full rewrite attempted without incremental validation, which systematically underdelivers against its promised timeline and risk), and paying down debt without ever measuring it ("we did a cleanup sprint" with no before/after data, so the org can't tell if it worked and repeats the same mistake next time).
Structured elaboration
| Anti-pattern | Consequence | Pragmatic alternative |
|---|---|---|
| One-off hack as "the fix" | Debt becomes permanent since there's no forcing function to revisit it | Require every shortcut to be filed with an explicit repayment plan and lifespan at the moment it's taken |
| Big-bang rewrite | Timeline and risk both systematically underestimated; the team is exposed to a single high-stakes cutover instead of many small, recoverable ones | Default to incremental refactor; reserve full rewrite for cases where a proof-of-concept has validated the approach first |
| Unmeasured debt paydown | No evidence the work helped; the same debt (or a re-accumulated version of it) resurfaces within a year with nobody able to say whether the last effort was wasted | Capture a baseline before starting and measure leading and lagging indicators afterward |
Worked example
A team ran a "tech debt week" eighteen months ago with no baseline captured and no follow-up measurement. Today, the same complaints (slow builds, brittle tests) are back, and nobody can say whether the tech debt week actually helped or whether the codebase simply re-accumulated debt at the same rate it always had; the org has no evidence either way, which makes the NEXT ask for debt-paydown capacity harder to justify, since the last one has no track record to point to.
Trade-offs & pitfalls
All three anti-patterns share a root cause: treating debt paydown as an EVENT (a hack, a rewrite, a cleanup week) rather than a measured, ongoing PROCESS with accountability for whether it worked; the fix for each is some form of making the work trackable and its outcome verifiable, not just well-intentioned.
Design a 6-month program to shift product and engineering culture toward preventing technical debt accumulation in the first place. Include the practices, metrics, and incentives you would introduce, any onboarding changes, and both a short-term win and a structural change that sustains the shift.
Sample Answer
Direct answer
A 6-month culture-shift program needs a mix of process changes (making prevention the default, not an extra step), visible incentives (recognizing prevention, not just heroics fixing incidents), and a genuine short-term win in the first month to build momentum, paired with a structural change that survives after the 6-month program formally ends.
Structured elaboration
- Practices: bake debt-prevention checks (the CI gates and code-review standards described above) into the DEFAULT workflow so avoiding new debt requires no extra deliberate effort: concretely, the CI gates check a coverage-delta floor (a PR cannot drop test coverage on the files it touches below a set threshold) and a complexity-delta ceiling (a PR cannot push a touched file's cyclomatic complexity, a count of the independent decision paths through the code, above a set threshold), while the code-review standards require the reviewer to explicitly confirm the change doesn't bypass an architectural layer boundary before approving; this beats a purely awareness-based campaign, since a cultural shift that depends on everyone remembering to care doesn't survive the next deadline crunch.
- Metrics: track new-debt-introduction rate (via the debt-gate signals) as the primary program metric, since it directly measures prevention, distinct from existing-debt-paydown metrics tracked elsewhere (the composite debt-score trend and incident-rate reduction used elsewhere to measure how much PRE-EXISTING debt is being paid down, as opposed to how much NEW debt this program is preventing).
- Incentives: explicitly recognize prevention in team retrospectives and performance conversations ("this team caught and fixed a complexity spike before it shipped" gets the same visibility as "this team heroically fixed a production incident"), since organizations that only celebrate incident heroics implicitly under-value prevention.
- Onboarding changes: new hires get explicit context on the team's debt-prevention practices and the reasoning behind them during onboarding, not left to absorb it informally over months, which both accelerates their contribution to the culture and signals the practice is a genuine organizational priority, not an informal habit.
- Short-term win: a visible early success (the first month's CI-gate rollout catching and preventing a real complexity regression before it shipped) shared broadly, to demonstrate the program's value concretely rather than asking for six months of trust up front.
- Structural change that sustains the shift: the CI gates and code-review standards themselves are the structural change; once they're the default tooling, the culture shift persists even after the dedicated 6-month program's active push ends, since the mechanism doesn't depend on ongoing conscious effort to maintain.
Worked example
Month 1: roll out the advisory-mode quality gate (the gate runs on every PR and posts its verdict as a comment, but does not block the merge) and get one clear, visible "caught it before it shipped" story to share org-wide. Months 2-3: move the gate to blocking mode (a failing gate now prevents the merge outright) for volunteer teams, and formally add debt-prevention context to the onboarding curriculum. Months 4-5: roll out blocking mode org-wide, with prevention explicitly discussed in team retrospectives as a recognized category of good work. Month 6: the program's formal push ends, but the gate, the onboarding module, and the retrospective practice all continue as standing parts of how the org works, not as a program that quietly winds down.
Trade-offs & pitfalls
A culture program that's purely awareness-based (posters, all-hands talks about caring about quality) without a structural mechanism baked into the default workflow tends to fade the moment the program's active push ends and the next deadline crunch arrives; the structural changes (gates, onboarding content) are what make the shift durable past month 6.
After a major refactor, describe a pragmatic post-remediation measurement plan to demonstrate the outcome. Specify the baseline metrics you would capture before the work, the follow-up cadence, the leading versus lagging indicators you would track, and how you would present the results to both engineering teams and business stakeholders.
Sample Answer
Direct answer
A post-remediation measurement plan needs a baseline captured BEFORE the work starts, a defined follow-up cadence, a mix of leading indicators (early, faster-to-move) and lagging indicators (slower, more definitive), and a results presentation tailored differently for engineering teams versus business stakeholders.
Structured elaboration
- Baseline metrics captured before work begins: cycle time, incident rate, and coverage in the affected area, snapshotted at the START of the remediation, not estimated retroactively (retroactive baselines are unreliable and easy to dispute).
- Follow-up cadence: leading indicators (build time, coverage, PR cycle time) checked at 2 and 6 weeks post-remediation, since they move fast; lagging indicators (incident rate, bug escape rate) checked at 3 and 6 months, since they need more data to be statistically meaningful.
- Leading vs lagging: leading indicators tell you EARLY whether the remediation is working as intended (did complexity actually drop, did build time actually improve); lagging indicators tell you whether it translated into the outcome that mattered (fewer incidents, less customer impact). Report both, since a leading indicator improving without a corresponding lagging-indicator improvement is itself a useful, actionable finding (the fix addressed a symptom, not the root cause).
- Presentation: engineering teams get the full metric set with context (what changed, why, and the confidence level); business stakeholders get the 2-3 metrics that map directly to business impact (incident rate, deploy frequency), framed against the original business case that justified the work.
Worked example
A refactor targeting a high-complexity billing module: baseline (captured at kickoff) shows cyclomatic complexity (a count of the independent decision paths through a piece of code, roughly how many branches and loops it has; higher means more ways a change can break something) at 28, build time at 9 minutes, incident rate at 3/month. At 2 weeks post-remediation, complexity has dropped to 14 and build time to 5 minutes (leading indicators moving as expected). At 3 months, incident rate has dropped to 1/month (lagging indicator confirming the leading indicators translated into real impact). Presented to engineering: the full trend with the specific architectural changes that drove each metric. Presented to leadership: "incident rate in billing down two-thirds since the Q2 remediation, consistent with the reduced complexity we measured immediately after the work."
Trade-offs & pitfalls
Measuring only immediately after the work (leading indicators) and declaring victory skips the harder, more important lagging-indicator confirmation; a remediation that improves complexity and coverage numbers but shows no corresponding drop in incidents over the following quarter is a finding worth reporting honestly, not one to omit because it complicates the success narrative.
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.
Explain technical debt using the principal-and-interest loan analogy, then distinguish intentional (strategic) debt from accidental (unintentional) debt. For each, give one concrete example of how it accumulates and name which stakeholder group typically introduces or first notices it.
Sample Answer
Direct answer
The loan analogy: technical debt has a principal (the corners cut to ship faster) and interest (the ongoing tax you pay in extra effort every time you touch that code until the principal is repaid). Martin Fowler's technical debt quadrant then splits debt along two axes: deliberate versus inadvertent, and reckless versus prudent. Reckless debt is a shortcut taken without understanding or caring about the consequences (skipping error handling entirely because nobody thought about what happens when a call fails, for example); prudent debt is a deliberate, informed trade-off made with eyes open, like the hardcoded-ranking-weight example below. The distinction that matters most day to day is intentional (strategic) debt versus accidental (unintentional) debt.
Structured elaboration
Intentional (strategic) debt is a conscious trade-off: the team knows a shortcut is being taken, why, and (ideally) has a repayment plan. Accidental (unintentional) debt is debt nobody chose, it emerged from evolving requirements, from not knowing a better approach at the time, or from the codebase outgrowing its original design.
| Intentional / strategic | Accidental / unintentional | |
|---|---|---|
| Example | Hardcoding a config value to hit a launch date, with a ticket filed to fix it | A module designed for 10 users now serving 10,000, and nobody planned for the scale shift |
| Who typically introduces it | Engineering + product, jointly, under deadline pressure | Any engineer, often without realizing it, as requirements evolve |
| Who typically first notices it | The same team, since they wrote the ticket | A different engineer months later, debugging something unrelated |
| Interest rate | Usually known upfront (the team estimated the cost of the shortcut) | Usually discovered too late (the interest was compounding silently) |
Worked example
A team ships a recommendation feature using a single hardcoded ranking weight instead of the planned configurable weighting system, to hit a launch date. That's intentional debt: product and engineering both signed off, and a ticket exists to build the real system next quarter. Separately, the same codebase's user-profile module was designed assuming one profile per user; eighteen months later, a business-account feature needs multiple profiles per organization, and half the codebase implicitly assumes the one-profile invariant. Nobody "chose" that debt; it's accidental, and it's usually more expensive to unwind because it's woven through more of the system before anyone notices.
Trade-offs & pitfalls
The common mistake is treating all debt as accidental (a moral failure to avoid) or all debt as intentional (a lever always available). In practice a large majority of real debt is accidental, which is exactly why passive vigilance (waiting to notice it) fails; you need active detection (metrics, architecture reviews) because accidental debt does not announce itself the way a filed ticket does.
Unlock Full Question Bank
Get access to all 25 Technical Debt Management and Refactoring interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.