Senior Engineering Interview Handbook / Chapter 106
Technical Debt and Refactoring Strategy
A sustained authorization-system case that develops technical-debt classification, debt economics, incremental refactoring, rewrite gates, verification, and non-blaming decision language.
Page tools
The feature that keeps finding the architecture
A product team wants delegated administration: a customer administrator should be able to grant a limited set of permissions without asking support. The visible feature is small. The estimate is not.
Authorization rules live in API handlers, background workers, export jobs, and the support console. A role name does not mean quite the same thing in each place. There is no representative test set for the decisions the system makes today. The last permission change required three teams, took six weeks, and still allowed one export path to bypass the new restriction.
Someone calls the system technical debt. Someone else calls it old code. Another engineer proposes a clean authorization service and a six-month rewrite. The product lead asks the useful question: what, precisely, are we buying if we delay the feature?
That question turns discomfort into an engineering decision. The system’s age is not the issue. Its current shape repeatedly charges the organization for changes and makes a high-consequence rule hard to enforce. A debt proposal must show that recurring cost, identify what causes it, and offer a repayment plan whose risks are smaller than the risks it removes.
Find the debt underneath the dislike
Old code can be healthy. A ten-year-old module with a stable contract, clear ownership, and predictable failures may ask less of its team than a new service with five undocumented consumers and no recovery path. Unfamiliar code is not debt merely because the current team would design it differently.
The authorization example contains several different problems. The repeated conditionals are messy, but duplicated syntax is only the visible symptom. The team cannot state the behavior it must preserve, so it has testability debt. Several components believe they own the same policy, so it has boundary debt. The bypass was difficult to detect and audit, so it has operational and security debt. Those diagnoses point toward tests, an authoritative contract, and decision telemetry. Renaming helpers would make the code tidier without touching the expensive part.
Other systems accrue debt in other ways. A data model may fit yesterday’s product but block a promised capability. A build graph may make every change wait. An unsupported runtime may create a dated security obligation. A local module may simply be hard to change while its external contract remains sound. Classification matters because refactor is not a universal repair. Refactoring improves internal structure while preserving observable behavior; it cannot by itself settle ownership, migrate consumers, restore operability, or decide which old behavior should survive.
The debt metaphor is useful only while its parts remain concrete:
- The principal is the decision, omission, dependency, data shape, or ownership gap that creates the future cost.
- The interest is what the system keeps charging: slower delivery, defects, incidents, support work, audit exposure, infrastructure cost, or a product option that remains blocked.
- The repayment plan is a sequence that lowers that interest without taking on more delivery or production risk than it retires.
Most principal was not incurred through negligence. The original authorization checks may have been a reasonable way to launch one role and two endpoints. The relevant fact is that the product, threat model, or operating environment changed. A fair account of that change is also a better technical account:
Embedding role checks in handlers was adequate when the product had two fixed
roles and no customer-admin workflow. We now have custom roles, exports, and
delegated administration. The old shape makes each policy change a coordinated
migration and allows enforcement to diverge across execution paths.
This language locates the problem without inventing villains.
Measure the charge before prescribing the cure
Debt has to compete with customer work, reliability work, security work, and the cost of leaving engineers on the current system. Perfect accounting is not required. Evidence strong enough to change a decision is.
For the authorization system, the team can reconstruct the last several policy changes: elapsed time, number of repositories and teams involved, review cycles, late defects, support corrections, and mismatches found after release. It can inspect incident and audit records for authorization failures, then map the product roadmap to policy changes the current design will require. A claim such as “this code is impossible” becomes an estimate that another enterprise role will touch four enforcement paths and repeat the same reconciliation.
Measures should remain close to the alleged interest. A refactor that reduces lines of code but leaves policy-change lead time and inconsistent decisions unchanged has not repaid the debt the team used to justify it. Conversely, one expensive change does not prove a continuing trend. It may be a difficult feature rather than a structural tax.
Some evidence resists a single number. Unclear ownership can surface as repeated escalation, contradictory decisions, or changes that nobody can approve confidently. Security exposure may be unacceptable even if it has not yet produced an incident. Record those obligations honestly instead of manufacturing a monetary estimate. The decision still needs a consequence, an owner, and a time horizon.
Now the product discussion can compare plausible futures. Continuing as-is means accepting another multi-team policy change and the known chance of divergent enforcement. A full pause for replacement delays delegated administration and creates a second authorization system. A smaller repair may let the feature fund the boundary it needs. The evidence does not choose among those options automatically, but it makes their costs visible.
Make one path authoritative
Before moving policy, the team must learn what the system currently does. It collects representative decisions from production-safe logs and support cases: ordinary members, customer administrators, suspended users, deleted resources, cross-tenant attempts, background exports, and the exceptions that accumulated around older contracts. Sensitive values stay out of fixtures. Ambiguous cases are reviewed rather than silently enshrined as correct.
Those cases become characterization tests around a small decision contract:
authorize(subject, action, resource, context) -> allow | deny | indeterminate
The third result matters. Missing tenant context or an unavailable policy source must not accidentally become permission. The contract also needs a reason code and policy version for audit and comparison. Whether the eventual implementation is a library or a service is secondary; first the system needs one vocabulary for asking and explaining the decision.
This is the smallest useful boundary repair. It does not require every caller to move at once, and it does not pretend that the old behavior is already understood. The delegated-administration feature can use the contract for its new paths while an adapter evaluates selected existing paths both ways. During comparison, disagreement is evidence to investigate, never a reason to let a shadow decision grant access.
The export worker is a revealing first existing slice because it exposed the recent bypass and exercises asynchronous identity context. Moving it forces the team to answer real questions: Which user initiated the export? Which tenant’s policy version applies after a retry? Does suspension invalidate queued work? What audit record must remain after the file expires? A low-consequence demo endpoint would produce faster apparent progress and teach much less about the boundary.
After the slice moves, the team compares old and new decisions, investigates differences, and cuts over only when the required cases are explained. It then tracks the measures that justified the work: time to add the delegated role, number of enforcement paths changed, policy mismatches, authorization incidents, and support intervention. Temporary adapters, comparison logs, and flags receive owners and deletion conditions. Otherwise the repayment plan leaves behind a new layer of debt.
The implementation boundary creates obligations
A shared library avoids a network dependency and may suit one language and one release train. It can also leave policy versions scattered across independently deployed applications. A service centralizes updates and audit decisions, but authorization now depends on its latency, availability, caching rules, and failure semantics. Neither topology is inherently the senior answer.
The decision follows the actual constraint. If most checks occur inside one application, an in-process policy module with explicit versioning may be the safer repair. If many languages and independently deployed systems must share rapidly changing policy, a service may earn its operational cost. In either case, callers need a defined response to timeouts, stale policy, partial context, and emergency revocation. Centralizing a flawed rule only makes it consistently flawed.
Local code debt admits a smaller treatment still: characterize the stable contract, refactor in place, and keep the blast radius inside the module. Operational debt may require telemetry, rollback, or a practiced recovery path rather than a new abstraction. An end-of-support dependency may require a time-bound fleet migration. The repair should match the source of interest, not the remedy the team happens to enjoy using.
Pass the rewrite gate
A rewrite offers emotional clarity. Frustration, exceptions, and awkward history disappear from the architecture diagram. In production, they remain as behavior to rediscover, traffic to migrate, and an old system that still needs owners while its replacement is built.
Replacement is justified when required reliability, security, scale, or product behavior cannot be reached safely through staged repair—or when the staged repair would cost more and expose users to more risk. That conclusion needs more than a cleaner target design. The team should be able to answer:
- Which required outcome is blocked by the current implementation rather than by missing tests, ownership, or attention?
- Which behaviors and invariants must the replacement preserve, and how will old and new decisions be compared?
- Can callers, traffic, or data move in independently useful slices?
- Where does fallback remain safe, and where would reversal require repair or compensation?
- Who operates and secures the old path throughout the transition?
- What evidence permits the next slice, and what evidence stops the program?
- Who removes the old system and all migration machinery?
For the authorization system, the answers currently favor boundary repair. Characterization tests and a decision contract are prerequisites even if a replacement eventually proves necessary. They make the next feature safer now and create the seam through which implementation can move later. Calling that work “throwaway” would miss its value: it turns unknown behavior into an inspectable contract.
If the comparison later shows that the old design cannot express customer policy without pervasive special cases, the same seam supports replacement in slices. Evidence has earned a larger intervention. If the smaller repair reduces change time and inconsistency enough, the organization can stop without having to complete a grand redesign merely because it once appeared on a roadmap.
Deferral can be the responsible decision
Some debt should remain. An ugly component that rarely changes, has contained failure modes, and belongs to a retiring product may be cheaper to isolate than to improve. A speculative abstraction built before the next requirement is known can increase principal rather than repay it. Even important work may be deferred while an incident risk or contractual deadline takes precedence.
Explicit deferral records the bargain: the recurring cost being accepted, the assumption that keeps it tolerable, the event that should reopen the decision, and the owner who will notice. For example, the team might leave a legacy support-only permission path in place while its workflow is being retired, but block new callers and add an audit alert. That is different from placing an unowned “clean up auth” ticket in a backlog.
The opposite mistake is paying debt only with spare time. Work that matters needs scope, an owner, a product or risk outcome, and an acceptance condition. Attaching the first authorization slice to delegated administration is not hiding maintenance inside a feature; the boundary is part of delivering that feature safely. The team should say so and price it honestly.
Explain the choice without staging a grievance
Debt discussions cross incentives. Product leaders may hear a request to stop shipping; engineers may hear that correctness is optional; operators may hear that the pages will continue. A useful proposal gives each group a decision it can inspect:
The existing checks were reasonable for two fixed roles. Custom roles and
delegated administration now require the same policy to hold across APIs,
workers, exports, and support tools. Our last policy change took three teams
and still left one divergent path. I propose that this feature establish one
decision contract, characterize the high-consequence cases, and migrate the
export path first. We will continue only if decision mismatches are explained
and policy-change lead time falls. We are not proposing a new authorization
service until the contract shows that an in-process boundary is insufficient.
In an interview, the same account demonstrates senior judgment because it contains a rejected path, an incremental commitment, and evidence that can disprove the plan. “I cleaned up the legacy code” hides all three. A credible project story explains what had once been reasonable, what changed, which cost recurred, why the chosen repair matched it, how production stayed safe, and whether the original interest actually fell.
Rehearse the decision, then change one fact
Choose a difficult system you know and write a one-page debt proposal without company names or confidential values. Name the principal in one sentence. Describe the recurring interest using the nearest available evidence. Separate local code, testability, boundary, operational, product-fit, security, and platform concerns instead of calling the whole system “legacy.” Propose the first slice, its safety invariant, the measure that permits continuation, and the temporary machinery that must later disappear.
Then make the proposal resist an easy answer. Suppose the product will retire in nine months. Suppose a regulatory deadline arrives in six weeks. Suppose the system has no reliable tests, or one external consumer cannot migrate this year. Decide whether to repair, isolate, replace, or defer, and state which assumption changed your choice.
The final check is simple: if the proposed work succeeds, which delivery, reliability, security, cost, or product-option risk will be lower? If the answer is only that the code will look better, call it cleanup. Cleanup can be worth doing. It does not need the economics of debt to disguise itself.
Related links
Continue reading
Full table of contents