Back to blog
Series · Day 27
Product Mindset for Engineers in 30 Days
View all lessons →
Tech debt expiry dates

Day 27: Tech Debt Has an Expiry Date

Ask for "time to refactor" and you're asking for a favour. Favours lose to features, and most of the time they should. Say "this gets five times more expensive on a date we already know" and you've put a business decision on the table instead. A product person can act on that sentence. They can't do anything with the first one.

The hook: a two-sprint refactor that turned into a contract renegotiation

Early in a product I built, we took a shortcut in the data model: an account and an organisation were the same thing. One table, one ID. Every engineer knew it was wrong. Splitting them was a few days of migration work, so it went into the backlog as "cleanup: separate account/org".

For about a year the fix stayed cheap. Then we built billing on that same table. Invoices went out per account. Pricing was quoted per account. Contracts were signed per account. Nobody marked the moment, and the ticket kept its old size estimate.

Some months later, a customer with several subsidiaries asked for one consolidated invoice across all of them. Engineering scoped the split at "two sprints". Then we read the contract. Separating account from org changed what a billable unit meant for that customer, so the fix wasn't a migration any more. It was a phone call, then a pricing conversation, then a contract amendment. The cheap option had expired the day billing shipped, and none of us had noticed.

What actually went wrong

The shortcut wasn't the problem. Shipping a simplified model was the right call when we made it. The problem was that the ticket had no date on it.

  • ▹We filed it as "cleanup". To planning, that word means it can wait forever.
  • ▹Put an undated cleanup ticket next to features with real customers and real deadlines and it loses every time. It should lose, too. Going by what the ticket said, waiting really did look free.
  • ▹What the ticket didn't say was the trigger: "once billing is built on this table, the fix turns into a customer conversation." One sentence. With it on the ticket, we'd have had an actual decision to make.

People often take the wrong lesson from stories like this. It isn't "never take shortcuts". It isn't "argue harder for refactoring time" either. It's this: write down when the shortcut stops being cheap.

The reframe: each debt item is an option with three numbers

Stop treating tech debt as one pile to shrink, or as a 20% tax you renegotiate every quarter. Treat each item as an option you're holding, and describe it with three numbers:

  • ▹Cost now: the fix today, in engineer-days, with today's dependents.
  • ▹Cost after: the fix once the next dependent feature is built on top of it. That feature is the trigger.
  • ▹Expiry date: when the trigger feature is expected to land. That's the day the cost jumps from the first number to the second.

You already have the tools for this from the trade-offs and opportunity cost lesson. This is the same reasoning applied over time. The cost of waiting isn't zero, and it isn't constant either. It moves in steps, and your own roadmap decides where those steps fall.

The good news: most debt has no expiry

Here's the part nobody says out loud. If nothing is going to be built on top of a piece of debt, its cost doesn't grow. An ugly module that nobody extends costs about the same to fix next year as it does today, and it might never need fixing. You're allowed to leave it alone. Deciding to leave it is a real outcome, and it beats feeling vaguely guilty about it every sprint.

Two items from our PhoenixDX backlog that we deliberately marked "no expiry":

  • ▹An internal reporting script with copy-pasted query logic and no tests. Ugly, yes. But only the team uses it, it runs on a schedule, and nothing on the roadmap reads its output. If it breaks, one person fixes it in an afternoon. Nothing coming up will build on it, so no trigger, no expiry. We left it alone.
  • ▹An old admin screen with hand-rolled form handling that doesn't match the rest of the frontend. Everyone who opens it is annoyed by it. It's also a leaf: nothing imports from it, and there's no admin work on the roadmap. A rewrite would make it prettier and change no future cost at all. We marked it "no expiry, rewrite only if admin features come onto the roadmap". That condition is the trigger we're watching for.

So the test isn't "is this code bad?" It's "is anything going to be built on this?" Bad code that nothing depends on is cheap to keep. Decent code that everything is about to depend on can be your most urgent debt.

Debt that only product can repay

Some debt can't be fixed in code alone, because the shortcut has leaked into something a customer relies on: a pricing unit, an API contract, a data export format, an SLA, what a "user" or a "seat" means. One question finds these items:

"Does fixing this require telling a customer something?"

  • ▹No? Then it's an engineering trade-off. Engineering proposes the fix and owns the estimate.
  • ▹Yes? Then it's a product promise hiding in the code. Whoever made that promise to customers, usually the product owner or founder, signed the loan. Paying it back means going back to customers, so they have to agree to it.
  • ▹Put that person's name on the ticket. In my story, billing per account was a pricing decision. The founder, which was me, signed that loan without realising it. No engineer could have refactored their way out of it.

Pitfall: engineers often try to pay this kind of debt off quietly, with a migration that keeps customer behaviour the same behind layers of compatibility shims. Sometimes that works. Often it just adds more debt and pushes the customer conversation later, when it costs more.

The template

Keep it short. One row per item, six columns:

markdown
| Item | Cost now | Trigger feature | Cost after | Expiry date | Who has to say yes |
|------|----------|-----------------|------------|-------------|--------------------|
| Account and org share one table | ~4 eng-days: schema split + migration | Billing v1 (invoices per account) | 2+ sprints AND pricing/contract change for multi-entity customers | Billing v1 start date (Q2) | Founder / product owner (pricing promise) |
| Internal reporting script, no tests | ~3 eng-days | None planned | Same | No expiry | Nobody. Leave it. |
| Legacy admin form handling | ~1 sprint | Admin features (not on roadmap) | ~1 sprint + rework of new admin work | No expiry until admin work is scheduled | Eng lead, if it becomes dated |

That first row is what the schema ticket should have looked like a year before the customer call. The estimates aren't precise, and they don't need to be. What matters is the shape: a small number, a trigger, a much bigger number with a customer attached, and a date. Once that row is on the backlog, "cleanup" becomes a decision with a deadline.

  • ▹Cost now and cost after can be rough. The ratio and the step change are what count.
  • ▹The trigger has to be a real roadmap item. "Someday we might" doesn't qualify.
  • ▹The expiry date is when the trigger feature starts, not when it ships. As soon as people start building on the schema, the cost is already climbing.
  • ▹"Who has to say yes" is an engineer when it's an engineering trade-off, and the product owner when a customer would have to hear about it.

How to say it in planning

The table does the thinking. The meeting is just delivery. Compare these two:

text
BEFORE
"The account/org model is really messy and it's slowing us down.
 We need a couple of sprints to refactor it properly."

-> Heard as: engineers want time off features for code quality.
-> Outcome: "Let's revisit after Q2." (Correct, on the information given.)

AFTER
"Splitting account from org is about four days today. Once the Q2
 billing work starts, invoices and pricing get built on that table,
 and the same fix becomes two-plus sprints plus a pricing change for
 any customer with multiple entities. So we need to decide by March 1:
 either we do the four days now, or we accept that consolidated
 invoicing becomes a contract conversation later. That second part
 is a product call, so I need [product owner] to own it."

-> Heard as: a cost that jumps on a date, and a decision with an owner.
-> Outcome: either it gets scheduled, or someone consciously accepts
   the risk. Both are fine. Silence is the only bad outcome.

Look at what changed. You're no longer asking permission to do engineering work. You're laying out two options with prices, a deadline, and the name of the person who has to choose. If they choose to wait, write that on the ticket. It's the debt version of an architecture decision record.

ASE aside: agents bring expiry dates closer

With coding agents in the loop, the next feature often reaches production sooner than it used to, and that feature is often the trigger that builds on your shortcut. So expiry dates can arrive sooner than your gut expects. And unless you tell them otherwise, agents will usually build on a bad schema without a word of complaint. This isn't a separate argument. It's one more reason to put a date on every debt item that has a trigger.

Common pitfalls

  • ▹Ranking debt by how ugly the code is, rather than by what's about to depend on it.
  • ▹Treating "no expiry" as failure. It's a decision, and it frees up your attention.
  • ▹Using the trigger feature's ship date as the expiry. Use its start date.
  • ▹Quietly repaying product-layer debt in code, without the person who signed the loan.
  • ▹Inflating "cost after" to win the argument. Do it once and the room stops trusting your next ten items.

Day 27 exercise

  • ▹Pick the three debt items your team complains about most.
  • ▹Fill in the six columns for each: item, cost now, trigger feature, cost after, expiry date, who has to say yes.
  • ▹No trigger on the roadmap? Mark it "no expiry" explicitly and stop worrying about it.
  • ▹For each item, ask: "Does fixing this require telling a customer something?" If yes, write down who signed the loan.
  • ▹Take the item with the nearest expiry date to your next planning meeting and use the "after" structure: cost now, trigger, cost after, decide-by date, owner.

Day 28 picks up from here. You walk into planning with one dated item, and it has to compete with features someone has already promised. Tomorrow is about making that a fair comparison.

Flashcards
Check yourself

Extend your knowledge

  • ▹Ward Cunningham's original framing of the debt metaphor, from his 1992 OOPSLA experience report and the video he made later explaining it. Compare his idea of 'repaying' with the expiry framing here.
  • ▹Martin Fowler's 'Technical Debt Quadrant' article on martinfowler.com. Sort your three exercise items into deliberate or inadvertent, prudent or reckless.
  • ▹Michael Nygard's write-up on Architecture Decision Records. Borrow the format to record 'we chose to let this expire' decisions.
  • ▹Read your own roadmap for next quarter and list every feature that touches a shared schema or a public contract. Each one is a candidate trigger.
Test yourself on this lesson →

Discussion

Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.

Ask me anything about “Tech debt expiry dates” — trade-offs, decisions, or the story behind it.