Index

Changing Existing Software

Technical Debt Is a Change Tax You Can Name

A compatibility change introduced a contract version and a measured fallback lookup. Related work made acquisition atomic, separated expiration from wall-clock dates, narrowed compatibility fields, enforced authorization, and protected unselected drafts.

That history proves several rules had to become explicit. It does not prove that the old design charged technical-debt interest. The changes might be planned slices, newly discovered requirements, corrections, or work a different design would also need. A busy commit history can look expensive without telling us what made it expensive.

“The cart system is technical debt” adds no missing evidence. It does not say which future change becomes harder, what extra work the current design causes, how often that tax recurs, or what bounded repair would remove it.

Technical debt becomes actionable when that tax can be named. Until then, it is usually a judgement about code wearing an accounting metaphor, which may feel precise in a planning meeting but gives nobody a useful decision.

Debt belongs to a change path

Old code is not debt merely because it is old. Complicated code is not debt merely because it is unpleasant. Ugly code can sit quietly for years while a cleaner area consumes weeks of work because its contract keeps moving. A stable component which rarely changes may be cheaper to leave alone than a beautiful replacement.

Debt appears relative to work the product needs to do:

requested change
    -> ordinary implementation
    -> extra work caused by an existing constraint
    -> delivery or operating consequence

The constraint might be an ambiguous contract, shared mutable state, a slow test environment, an unindexed query, a manual deployment step, or a runtime at its capacity limit. The extra work might be investigation, repeated translation, rework, waiting, incident response, or infrastructure.

This definition avoids grading an entire application as “good” or “legacy.” One compatibility path can impose a large tax on existing-cart lookup and none on creating a new cart. The debt entry should follow the affected path, because that is where the business will either pay or avoid the cost.

Commit count is not interest

A run of corrective-looking commit titles is tempting evidence. It is also easy to misuse, especially when the conclusion we already want is “this system needs a rewrite.”

Several changes may be:

  • independently planned slices;
  • one change copied or combined across branches;
  • tests added after behavior was already correct;
  • new requirements rather than repairs;
  • cleanup discovered while nearby code was open; or
  • genuine rework caused by a weak contract.

The compatibility history contains related and overlapping changes. Counting every diff as a separate failure would exaggerate the work. Counting only the final combined diff would hide the decisions made along the way.

The honest initial row looks like this:

FieldRetained evidence
change pathexisting-cart compatibility
observed workcontract version, fallback lookup, acquisition, expiration, authorization, draft protection
active effortnot retained
elapsed delaynot retained
repeated taxnot established
consequencenot retained
cause confidenceseveral contracts changed; avoidable debt is unproven

This row does not justify a rewrite, and that can be an annoying answer when the code is plainly difficult to work with. It identifies what must be measured the next time the path changes. Annoyance is worth recording; it just is not a cost model.

Elapsed delay and engineering effort must also stay separate. A long calendar delay is not automatically the same number of engineer-days.

It may contain:

  • waiting for a test environment;
  • discovering which environment-specific branch ran;
  • coordinating a release;
  • repairing incompatible data;
  • repeating manual verification;
  • waiting for an external answer; and
  • active implementation.

Those are different taxes with different repairs. More engineers will not fix environment waiting. A contract test will not remove release coordination. Automation will not clarify a product rule which nobody owns. Lumping all of them into “the legacy system cost us three weeks” guarantees that the proposed repair will solve whichever problem is easiest to sell, not necessarily the one that caused the delay.

Record those parts separately rather than converting calendar time into fictional salary cost or a return on a six-month rewrite.

Interest has more than one unit

The debt metaphor becomes misleading when every consequence is converted into one score. A tidy dashboard number is attractive precisely because it removes the awkward parts, including whether two numbers mean anything comparable.

Useful units include:

delivery
    elapsed lead time
    active engineering hours
    blocked changes

quality
    corrective changes
    escaped defects
    repeated investigations

operations
    incident minutes
    queue age
    saturation
    manual release steps

optionality
    unsupported customer cases
    changes rejected because the path cannot carry them

Do not add incident minutes to query latency and call the total 83 debt points. That is numerology with a progress bar. Keep each unit visible so the proposed repair can be matched to the cost it is supposed to remove.

The compatibility work illustrates the limit. Its history proves change, not how much an earlier design cost, whether each follow-up was avoidable, or whether they share one cause. Commit count is not an invoice for technical debt.

An interest ledger needs a counterfactual it can defend

An actionable entry can fit in one row:

FieldQuestion
pathWhich requested work crosses the constraint?
observed taxWhat extra delay, effort, failure, or capacity did we see?
comparisonWhat makes the work “extra” rather than ordinary complexity?
recurrenceHow often has this class of work appeared?
confidenceWhich part is measured, recalled, or inferred?
bounded repairWhat is the smallest intervention expected to remove the tax?
proofWhich next observation would show that the tax changed?
expiryWhen should the entry be reviewed or discarded?

The comparison is often the weakest field, because it asks us to prove the work was extra rather than merely difficult.

A similar change which does not cross the boundary is useful. So are before-and-after traces, repeated phases within one delivery, or a controlled reproduction. “This would be easy in a clean system” is not a counterfactual. It is a preference, and usually one stated by someone who has not yet had to build that clean system.

This compatibility case has no defensible comparison yet. It shows several rules becoming explicit, not how long an alternative design would have taken. Its debt entry should remain measure until another occurrence supplies that comparison.

The repair must name the tax it removes

“Modernize the legacy application” is not a debt payment. It is a programme whose relationship to the observed tax remains unproven. It may still be the right programme, but the word debt does not make its scope or return obvious.

A bounded repair might be:

  • record the shared request and response contract in executable examples;
  • make test and production execute the same business rule;
  • provide deterministic fixtures for the affected data shapes;
  • replace a manual release step with an enforced gate;
  • isolate one state owner; or
  • route one recurring change class through a stable adapter.

Each proposal should complete the sentence:

If we do this, the next change on this path will no longer need this specific extra work.

Then define the check. The next three cross-boundary changes might be expected to complete integration within an agreed elapsed range, require no manual data repair, or pass the same contract suite before release. The observation does not need to promise perfection. It needs to be capable of proving the repair irrelevant or insufficient.

If the repair cannot name a future observation, it may still be worthwhile design work. Say that plainly. It is not yet an evidence-backed debt payment.

Paying, containing, and accepting are all valid

An identified tax does not automatically deserve removal. Companies do not exist to produce immaculate source trees, and engineering time spent removing one annoyance is time not spent on another risk or customer need.

There are four ordinary choices:

ChoiceWhen it fits
acceptthe path is rare and the bounded repair costs more than likely recurrence
containa wrapper, checklist, specialist review, or capacity limit makes the tax affordable
pay downrecurring measured cost is likely to exceed a scoped repair
replacethe constraint is structural and narrower repairs cannot meet required behavior or capacity

Containment is often underrated. A stable adapter and focused reviewer may turn a dangerous integration into an acceptable one without making the legacy system elegant. That is not giving up; it is paying for the outcome we need instead of the redesign we happen to want. Acceptance can also be rational when a component is near retirement or the company has more valuable work.

Risk can override the time comparison. A rare path which can duplicate payments, expose another customer’s data, or corrupt durable state may need containment or removal before its expected engineering hours exceed the repair. Those consequences should remain named risks rather than being disguised as a larger time estimate.

The synthetic reproduction uses ranges rather than a universal debt score. It recommends a repair only when the lowest projected recurring effort exceeds the highest repair estimate, accepts only when the ranges are separated in the other direction, and asks for more evidence when they overlap. An unacceptable risk is contained regardless of the hour comparison.

That arithmetic is a decision aid, not an economics model. Frequency can change, estimates can be wrong, and removing one bottleneck can reveal another. Its value is that every input and assumption remains visible.

A debt register should be allowed to forget

Debt backlogs tend to become permanent museums of engineering frustration. Entries survive after the feature is removed, the team changes, or the constraint stops affecting planned work. Years later, somebody points at the size of the museum as proof that the codebase is collapsing.

Every entry needs an expiry condition:

  • a date;
  • the next occurrence;
  • completion of a named product plan;
  • retirement of the affected path; or
  • evidence that the tax no longer recurs.

Reviewing may close the entry without changing code. That is not failure. If the product no longer needs the path, the debt stopped charging interest. We do not get extra credit for repairing a road nobody uses.

Technical debt is most useful as a claim about future work: this existing constraint makes this needed change slower, riskier, or more expensive in this observable way. Name the path, keep the units honest, propose a bounded repair, and check the next occurrence. Anything broader may be a design opinion worth discussing. It just is not yet a debt account the business can act on, no matter how strongly we dislike the code.