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.
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.
You have inherited a business-intelligence environment with undocumented SQL, fragile ETL jobs, and many untested reports. Propose a prioritized, stepwise refactor plan that adds automated tests, documentation, and safer deployments with minimal downtime. Include how you would identify the highest-impact items to fix first and how you would measure progress.
Sample Answer
Direct answer
For a business-intelligence environment with undocumented SQL, fragile ETL jobs, and untested reports, prioritize a stepwise plan starting with the highest-consumption reports (the ones stakeholders actually rely on for decisions), adding tests and documentation as each is touched, since a BI environment's real risk is a wrong number silently reaching a decision-maker, not a code-quality abstraction.
Structured elaboration
- Identify high-impact items first: audit report usage (view counts, which reports feed executive dashboards or recurring business decisions) to find where a silent error would actually matter, rather than starting with whichever SQL file happens to look messiest.
- Introduce automated testing incrementally: for each high-impact report tackled, add data-quality assertions (row-count sanity checks, null-rate thresholds, a reconciliation check against a known-good source where one exists) before refactoring the underlying SQL, so there's a safety net catching regressions introduced during cleanup, not just hoping the refactor is correct.
- Documentation strategy: as each high-impact report is migrated, capture what the legacy SQL actually did in a short doc note before or alongside the rewrite, specifically the business logic and any known quirks or workarounds discovered during the audit, since undocumented SQL that gets refactored without capturing the tribal knowledge that made sense of it just becomes newly-undocumented SQL under a cleaner syntax. Lean on the tooling's built-in documentation and lineage features (see below) to keep this current going forward, rather than a separate, hand-maintained wiki that drifts out of date the moment the migration pressure eases.
- Introduce dbt or a similar tool incrementally: rather than a big-bang migration of all ETL to a new framework, migrate the highest-impact pipelines first, since dbt's built-in testing, auto-generated documentation, and lineage-tracking features deliver disproportionate value exactly where the risk is highest, and prove the migration pattern on a smaller scope before committing to the full environment.
- Deployment safety: stage any refactored report or pipeline alongside the old version for a validation period (comparing outputs) before fully replacing it, mirroring the shadow-mode discipline used in other data-migration contexts, since "the new query looks equivalent" is not the same as verified equivalent.
- Measuring progress across the full migration: track a simple, visible progress metric independent of any single report's status: the percentage of the 40 reports migrated and passing their reconciliation check, reported on a fixed cadence (biweekly), alongside two leading indicators (the number of reports with automated tests added and the number with a documentation note captured), so stakeholders can see the migration advancing wave by wave rather than only hearing about it when the final report ships.
Worked example
An audit of report view counts finds 3 of 40 reports account for 70% of total views and directly feed the weekly executive business review; those 3 become the first migration targets. For each, a reconciliation check compares the legacy SQL's output against a proposed dbt model's output over a two-week validation window before the legacy version is retired, catching one real discrepancy (a subtly different date-boundary handling in a week-over-week calculation) before it ever reached the executive review. Alongside the rewrite, each report gets a short doc note capturing the business logic recovered during the audit (including the date-boundary quirk just found) plus the dbt-generated lineage graph, so the next engineer who touches it isn't starting from zero the way this migration did. The remaining 37 lower-impact reports are migrated in subsequent waves, using the validated pattern from the first 3, at a much faster pace since the process, tooling, and documentation habit are now proven. Progress is reported biweekly as a simple fraction of the total: after the first 3 reports, that is 3/40 (7.5%) migrated and passing reconciliation, rising wave by wave as the remaining 37 are processed, so stakeholders can see the migration's actual advancement rather than inferring it from silence between report completions.
Trade-offs & pitfalls
The pitfall in a BI-specific remediation is treating this purely as a code-quality problem (messy SQL, no tests) when the real stakes are DECISION quality; prioritizing by usage/impact rather than by code messiness is what keeps the remediation focused on where a silent error would actually cause harm, and the reconciliation-before-cutover discipline is what catches the class of bug (subtle logic drift during a rewrite) that unit tests alone often miss in this domain. A second pitfall specific to this question: treating documentation as something to circle back to later rather than capturing it in the same pass as the rewrite, since the person doing the migration is the one with the freshest, most complete understanding of what the legacy SQL actually did, and that understanding degrades fast once the migration pressure is gone.
Explain how you would apply a risk-impact-effort prioritization matrix to technical debt remediation across modules. Define how you measure each axis and the scoring scale, then score and rank three hypothetical modules: Module A has a high bug rate, low user exposure, and medium effort; Module B has a low bug rate, high user exposure, and high effort; Module C has a medium bug rate, medium exposure, and low effort.
Sample Answer
Direct answer
A risk-impact-effort matrix scores each debt item on three axes (business/reliability risk if left unfixed, user-facing impact, and effort to fix), then prioritizes items with the best risk-and-impact-to-effort ratio first. It beats a gut-feel ranking because it forces the same criteria across dissimilar items and makes the trade-offs visible to non-engineers.
Structured elaboration
Define each axis on a simple, defensible scale, for example 1-5:
- Risk: probability and severity of an incident if this item stays unfixed (low = cosmetic, high = has caused or will likely cause an outage).
- Impact/exposure: how much of the user base or revenue touches the affected code path.
- Effort: engineer-days to remediate, including test and rollout time, not just the code change.
A simple composite score is (risk + impact) / effort, which naturally favors high-value, low-cost fixes; more sophisticated versions weight risk and impact separately since a rare-but-catastrophic risk shouldn't be averaged away by a low-impact score.
Worked example
Score three modules on 1-5 scales (5 = worst for risk/impact, 5 = highest for effort):
| Module | Bug rate (risk) | User exposure (impact) | Effort | (risk+impact)/effort |
|---|---|---|---|---|
| A: high bug rate, low exposure, medium effort | 4 | 2 | 3 | 2.0 |
| B: low bug rate, high exposure, high effort | 2 | 4 | 5 | 1.2 |
| C: medium bug rate, medium exposure, low effort | 3 | 3 | 1 | 6.0 |
Ranking by score: C first (6.0), A second (2.0), B third (1.2). C wins because it is cheap to fix relative to its combined risk and exposure; B, despite having the highest exposure, is deprioritized because its effort is disproportionately large. A senior candidate should flag the limitation explicitly: this ranking can systematically starve high-effort, high-value work (like B) if the formula is applied mechanically every cycle, so a healthy process reserves some capacity for at least one "big rock" item per period regardless of its ratio score.
Trade-offs & pitfalls
A pure ratio score is easy to game (break a large item into many small ones to inflate its score) and can systematically ignore catastrophic-but-rare risks if risk and impact are averaged rather than treated with a floor (a risk-5 item should never be fully offset by a low impact score). The fix is a hard floor: any item scoring risk = 5 gets manually reviewed regardless of its computed ratio.
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 have three debt items with estimates: flaky tests causing 10% weekly CI failures; missing data validation causing 1% production failures; and duplicated preprocessing code across services costing extra maintenance time. Given limited engineering time, prioritize these three items and justify your ordering with metrics and explicit business-impact assumptions.
Sample Answer
Direct answer
Of the three, prioritize the missing data-validation issue first: it has the smallest realized-impact footprint (1% of production runs) but the highest severity per occurrence (a silent correctness failure feeding downstream decisions), followed by the flaky tests (high frequency but bounded, non-customer-facing impact), and the duplicated preprocessing code last (a real cost, but a maintenance tax rather than an active failure mode).
Structured elaboration
Score each on severity-if-it-fires versus frequency, not on frequency alone:
| Item | Frequency | Severity per occurrence | Business-impact reasoning |
|---|---|---|---|
| Missing data validation | 1% of production runs | High: silently corrupts model inputs, may not be caught until a downstream metric looks wrong | Correctness failures compound quietly; a 1% rate against high production volume is still a meaningful absolute count, and each one can propagate into a business decision before anyone notices |
| Flaky tests | 10% of weekly CI runs | Medium: blocks developer velocity, but doesn't reach production | Costs real engineer time (re-running, investigating false failures) but has a natural ceiling on damage, since it's caught before deploy |
| Duplicated preprocessing | Constant (every maintenance touch) | Low-medium: extra effort per change, risk of the two copies silently diverging over time | A tax on every future change, not an active incident risk today, so it compounds slowly rather than causing acute harm |
Worked example
Assume 50,000 production inference runs/week: 1% missing-validation failures means roughly 500 potentially-corrupted runs weekly, each with a real (if hard to quantify precisely) chance of a bad downstream decision, versus the flaky tests costing an estimated 5 engineer-hours/week in wasted re-runs and investigation, and the duplicated preprocessing costing perhaps 2 extra engineer-hours per unrelated change (rare, but recurring). The data-validation issue wins because its downside is a silent CORRECTNESS failure with unbounded downstream cost, while the other two are bounded, visible, recoverable costs; this is the same logic that puts a rare-but-catastrophic risk ahead of a frequent-but-contained one.
Trade-offs & pitfalls
A naive read might rank by frequency alone and put the 10%-frequency flaky tests first; the corrective insight a strong candidate volunteers is that frequency times bounded-severity can still be less costly than a low-frequency, unbounded-severity failure mode, especially one (silent data corruption) that resists detection by construction.
Unlock Full Question Bank
Get access to all 19 Technical Debt Management and Refactoring interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.