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 inherit a large technical debt backlog while maintaining strict SLOs. Propose a prioritization approach that balances small, high-impact reliability fixes against larger refactors, explain how you would allocate error budget toward debt remediation, how you would stage the work in production, and how you would define metrics that show progress and return on investment.
Sample Answer
Direct answer
With a large debt backlog and strict SLOs to maintain simultaneously, prioritize small, high-impact reliability fixes ahead of larger refactors, allocate a defined slice of error budget specifically to debt remediation (not ad hoc), and stage riskier work carefully so it can't itself become the incident that consumes the SLO it's trying to protect.
Structured elaboration
- Prioritization: rank debt items by SLO impact first (does this item directly threaten reliability) and size second, favoring several small, high-confidence reliability fixes over one large, uncertain refactor when both compete for the same limited budget, since small fixes have more predictable risk profiles.
- Error-budget allocation for remediation: reserve an explicit slice of the error budget for planned remediation work, so debt work has a legitimate, budgeted claim on risk tolerance rather than competing informally against the margin reserved for unplanned incidents.
- Staging work in production: any remediation with real production risk gets staged (canary, gradual rollout) with the SAME rigor as a feature launch, since "it's just a refactor, not new functionality" is a common but false justification for skipping staging discipline.
- Metrics for progress and ROI: track two separate things. For progress: the SLO compliance trend (is reliability actually improving as debt is paid down), the reserved-budget utilization (is remediation staying within its allocated slice), and the backlog's overall risk-weighted size (is the total risk surface shrinking, not just the item count). For ROI specifically: incident-response hours (or a standard dollar cost per incident-hour) avoided in the period after a fix ships, compared against the period before, divided by the engineer-time invested in that fix; this is what tells leadership whether the invested capacity is actually paying back in reduced incident cost, rather than assuming it is simply because the backlog is getting smaller.
Worked example
A backlog of 40 debt items against a 99.9% availability SLO with currently healthy budget headroom: prioritization surfaces 5 small items directly tied to the two SLO-threatening incidents from last quarter (highest priority, tackled first using a modest reserved-budget slice), while a much larger architectural refactor (addressing a broader but less acutely SLO-threatening set of items) is scheduled later, staged carefully with canary rollout once undertaken, specifically BECAUSE its larger blast radius makes it the item most likely to itself cause an SLO miss if rushed. For those 5 items, the ROI metric is reported explicitly alongside the SLO trend: engineer-time invested versus incident-response hours avoided in the following quarter compared to the quarter before, so the remediation's payback is visible as a number, not just implied by "the backlog shrank." Concretely: the 5 items took 3 engineer-weeks (120 hours) to fix; incident-response hours in the affected area dropped from 40 hours the quarter before to 10 hours the quarter after, a 30-hour reduction. At a fully-loaded cost of $85/engineer-hour, that's 30 * 85 = $2,550 in incident-response cost avoided against 120 * 85 = $10,200 invested, an ROI ratio of 30 / 120 = 0.25 (25%) of the invested cost recovered in the first quarter alone, a payback rate that keeps compounding in every subsequent quarter since the fix is permanent but the invested hours were one-time.
Trade-offs & pitfalls
The pitfall specific to an SRE context is treating debt remediation as inherently safe because it's "cleanup" rather than new feature work; a large refactor executed carelessly under SLO pressure can itself become the incident that burns the budget it was meant to protect, which is why staging discipline for remediation work needs to match, not fall below, the discipline applied to feature launches. A second pitfall specific to this question: reporting only progress/compliance metrics under an "ROI" label without an actual return-relative-to-investment figure lets the backlog look like it's succeeding even if the invested capacity isn't paying back in reduced incident cost.
Design an error-budget policy that explicitly incorporates technical-debt remediation windows for a platform team. Explain how planned remediation affects the error-budget calculation, how you would schedule remediation without harming SLAs, and how you would report exceptions and adjustments to stakeholders.
Sample Answer
Direct answer
Incorporate technical-debt remediation into an error-budget policy by treating planned remediation work as a scheduled, budgeted DRAW on the error budget (like a planned maintenance window), distinct from unplanned incidents, so debt work can proceed without silently eating into the margin reserved for genuine unplanned risk.
Structured elaboration
- Error-budget calculation with remediation windows: reserve a small, explicit slice of the error budget (for example 10-15%) specifically for planned remediation activity, calculated and tracked separately from the unplanned-incident portion, so a bad month of genuine incidents doesn't get blamed on debt work, and vice versa.
- Scheduling remediation without harming SLAs: schedule remediation windows during historically low-traffic periods where possible, and require any remediation activity with a plausible availability impact to have an explicit rollback plan and a pre-agreed abort threshold (if the reserved budget slice is exhausted mid-window, the remediation pauses, it doesn't continue into the unplanned-incident reserve).
- Reporting exceptions and adjustments: if a remediation window runs over its reserved budget, that's reported explicitly and separately from a genuine incident post-mortem, since the response and lessons are different (a scheduling/estimation problem, not a reliability failure), and any adjustment to future reserved allocation is based on that data.
Worked example
A platform team's monthly error budget is 43 minutes of downtime (99.9% SLO on a 30-day month). They reserve 6 minutes (about 14%) specifically for planned remediation activity, tracked in a separate ledger. A database migration planned as part of debt remediation is scheduled for a low-traffic window, budgeted for up to 4 minutes of expected brief unavailability; it runs slightly over at 5 minutes, still within the reserved 6-minute allocation, so no unplanned-incident budget is touched and no incident review is triggered, just a routine note in the remediation log. If it had run to 8 minutes, exceeding the reserved slice, the response is a scheduling/estimation retrospective specifically, not a reliability incident review, since the SLA breach traces to planned work exceeding its own budget, not to an unplanned failure.
Trade-offs & pitfalls
Without separating planned-remediation budget from unplanned-incident budget, teams either become too conservative about debt work (afraid any planned activity will look like it caused an SLA miss) or too cavalier about scheduling remediation (silently eating into the margin meant for real incidents); the explicit separation is what lets debt work proceed confidently within a reliability-conscious org.
What signals and telemetry would you monitor to detect accumulating technical debt across multiple teams and repositories? Name at least five, explain why each is useful, and what tooling you would use to collect it. Also explain how you would guard against teams gaming these metrics once they know they are being tracked.
Sample Answer
Direct answer
Monitor at least five signals: the trend (not just the snapshot) of cyclomatic complexity, the trend of test coverage, mean build and deploy time, PR size or review latency, and incident frequency. Each one is useful because it is a leading or lagging proxy for the thing you actually care about (developer velocity and system reliability), and together they cover code, process, and operational health rather than just one dimension.
Structured elaboration
| Signal | What it approximates | Why it's useful | Tooling |
|---|---|---|---|
| Cyclomatic complexity trend | How hard code is to reason about and test | Rising complexity predicts rising bug rate and slower changes | Static analysis (SonarQube, CodeClimate, radon) |
| Test coverage trend | How much of the system is safety-netted | Falling coverage predicts riskier changes and slower reviews | Coverage tools wired into CI (coverage.py, jacoco, istanbul) |
| Mean build and deploy time | Developer feedback loop speed | A slowing loop is often the earliest visible symptom of debt, before bugs show up | CI dashboards (GitHub Actions insights, Jenkins) |
| PR size or review latency | How safely work can be decomposed and reviewed | Growing PR size or review time signals coupling and unclear boundaries | Git provider analytics (GitHub Insights, LinearB) |
| Incident frequency | Real-world reliability cost of the debt | Ties abstract code metrics to concrete business impact | Incident tracker (PagerDuty, Opsgenie) |
For ML/AI-flavored teams, add training-time-per-epoch and retrain frequency as domain-specific analogues of build time and deploy frequency.
Worked example
A team's dashboard shows cyclomatic complexity flat for six months, test coverage dropping from 78% to 61% over the same period, and mean build time climbing from 6 to 14 minutes. None of these individually triggers an incident, but together they predict that the team's next quarter will show slower PR cycle time and a rising bug rate, roughly two months before that actually shows up in the incident tracker. That lead time is the entire point of tracking trends instead of point-in-time snapshots.
Trade-offs & pitfalls
The moment a metric becomes a target, people optimize the metric rather than the underlying thing it measures (Goodhart's law). Coverage percentage is the classic case: teams write low-value tests (asserting a getter returns what was just set) purely to move the number, without reducing real risk. Guard against this by pairing coverage with a second signal, such as mutation-testing score (deliberately inserting small bugs into the code and checking what fraction the test suite actually catches) or bug-escape rate, that is harder to game, and by treating any single metric as a conversation starter rather than a scorecard.
You manage a backlog of technical debt across multiple low-priority services with limited engineering time. Describe a framework to prioritize which debt to address first, including measurable criteria, and how you would present the trade-offs to product and platform stakeholders. Also cover how you would quantify maintainability itself so the prioritization is grounded in numbers, not gut feel.
Sample Answer
Direct answer
Prioritize the low-priority-service backlog with the same risk-impact-effort logic used elsewhere, but first make maintainability itself measurable, since "low-priority services" often means under-instrumented services where nobody has real numbers to argue with, gut-feel prioritization, and turf disputes.
Structured elaboration
- Instrument first: even a lightweight metric set (test coverage, deploy frequency, incident count, dependency freshness) per low-priority service turns "this service feels neglected" into a comparable, defensible number across the portfolio.
- Score and rank: combine those metrics into a single comparable score by normalizing each one (test coverage, deploy frequency, incident count, dependency freshness) to a 0-100 scale within the portfolio, for example via percentile rank, then take a weighted average that weights incident count and dependency freshness most heavily since they correlate most directly with actual risk; apply this same scoring method to every service, low-priority included, so low-priority services aren't exempted from measurement just because they're less visible, and the resulting ranking is comparable across the whole set.
- Present trade-offs concretely: for each of the top few items, state the specific risk of leaving it (an outage in a service nobody's watching closely can go undetected longer, not shorter, precisely because it's low-priority) against the effort to fix, so stakeholders see the real cost of continued neglect, not just an abstract "this is old."
- Batch small fixes: with limited engineering time, batch multiple small, low-risk items from different low-priority services into a single focused sprint, which is often more efficient than tackling one service's debt in isolation.
Worked example
Across 8 low-priority services, a lightweight audit reveals 3 have zero monitoring at all (the highest-priority finding, since "low priority" and "no visibility into whether it's currently broken" is a genuinely dangerous combination), 2 have dependencies over 2 years stale with known CVEs, and 3 are simply infrequently used with no material risk. Presented to stakeholders: "these 3 services have no monitoring, meaning we wouldn't know if they were failing right now; that's the top priority regardless of how rarely they're used, followed by the 2 with known security vulnerabilities." To make the "grounded in numbers" mechanism concrete rather than only named, here is the percentile-rank-then-weighted-average method actually run on two representative services from an 8-service reference set (using the four metrics test coverage, deploy frequency, incident count, and dependency freshness, with incident count and dependency freshness weighted 35% each and test coverage and deploy frequency weighted 15% each, since incidents and staleness correlate most directly with real risk): a service with 85% test coverage, 2 deploys/month, 9 incidents in the last six months, and 700 days since its last dependency update ranks in the worst portion of the set on incident count and dependency freshness (both at the 85.71 percentile, since most other services fare better) but the best portion on test coverage (14.29 percentile, since it is actually well-tested), giving 0.35*85.71 + 0.35*85.71 + 0.15*14.29 + 0.15*71.43 = 30.0 + 30.0 + 2.14 + 10.71 = 72.85 (rounds to 72.9), a real, computed maintainability score rather than only a description of the method. A second service with better numbers across the board (60% coverage, 7 deploys/month, 1 incident, 90 days since a dependency update) scores 0.35*14.29 + 0.35*14.29 + 0.15*42.86 + 0.15*14.29 = 5.0 + 5.0 + 6.43 + 2.14 = 18.57 (rounds to 18.6), a genuinely lower, directly comparable number, which is what lets stakeholders rank two services against each other by an actual formula instead of only by which category they happened to fall into.
Trade-offs & pitfalls
"Low priority" is a business-usage judgment, not a risk judgment, and conflating the two is the core mistake this question is testing for: a rarely-used service with zero monitoring and a known CVE can carry MORE risk than a heavily-used, well-maintained one, precisely because nobody would notice it failing.
How would you detect architecture-level technical debt, such as cyclic dependencies, inappropriate abstraction layers, or misplaced ownership, using static analysis and dependency graphs? Propose specific checks and tolerances (what counts as acceptable versus unacceptable), and explain which direction each of these signals moves in as debt worsens: developer velocity or cycle time, bug and incident rate, test coverage, cyclomatic complexity, build and deploy time, and mean time to recovery.
Sample Answer
Direct answer
Detect architecture-level debt with static analysis over the dependency graph: flag cyclic dependencies between modules, abstraction layers being bypassed (a UI component calling a database client directly), and ownership mismatches (a module's changes routinely require sign-off from a team that doesn't own it). Set explicit tolerances, since some coupling is normal and zero-tolerance policies get ignored.
Structured elaboration
- Cyclic dependencies: build a module-level dependency graph (via tools like
madge,dep-cruiser, or a custom AST walk) and flag any cycle. Tolerance: zero cycles between top-level modules is a reasonable hard rule, since cycles make independent deployment and testing impossible by construction. - Abstraction-layer bypass: define the intended layering (UI to API to service to data layer) and flag any import that skips a layer. Tolerance: allow explicit, documented exceptions (a performance-critical read path) but flag anything undocumented.
- Misplaced ownership: track which teams' approvals a module's PRs require over time. Tolerance: flag any module where the primary contributing team doesn't match the nominal owning team for more than one quarter, since that's evidence the module has outgrown its original boundary.
These signals move in a consistent direction as architecture debt worsens: developer velocity/cycle time drops (more coordination needed across the tangled boundaries), bug and incident rate rises (changes have unpredictable side effects through hidden coupling), test coverage often looks stable or even improves superficially (tests get added for the parts people touch, not the tangled parts), cyclomatic complexity rises at the integration points, build and deploy time rises (more of the system has to rebuild for a small change, especially with cyclic dependencies), and mean time to recovery rises (harder to reason about blast radius during an incident).
Worked example
A dep-cruiser scan on a mid-size service reveals a cycle between the orders and inventory modules: orders calls into inventory to check stock, and inventory calls back into orders to look up historical demand for restocking suggestions. This cycle means neither module can be deployed, tested, or reasoned about independently, and it explains why the team's cycle time has crept up over two quarters even though no single PR looks unusually large. The fix isn't a code-level refactor of either module; it's introducing a shared demand-history module both can depend on, breaking the cycle.
Trade-offs & pitfalls
Static dependency analysis catches structural coupling but not RUNTIME coupling (two services that are architecturally separate but share an implicit contract via a database table both write to); pair static analysis with a review of shared data stores. A second pitfall: treating every detected cycle as equally urgent, when a cycle between two rarely-changed modules is far less costly than one between two of the most actively developed modules in the codebase.
Unlock Full Question Bank
Get access to all 7 Technical Debt Management and Refactoring interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.