DebtGENERALLIFETIME-SPECIFICCONTESTED

Deliberate Debt

A shortcut taken knowingly is a strategy — if the trade-off is understood, the impact is bounded, and someone knows the way out. Two of those three are usually missing.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

We need to ship in three weeks and the right design takes six. How do we take the shortcut without it becoming permanent?

The requirement

A pilot customer needs an integration with their inventory system by the end of the month. The right design is a general inbound-integration boundary; the fast version is a hard-coded adapter that talks to their specific API.

The obvious build

Ship the fast version, add a // TODO: generalise this comment, and clean it up after the pilot.

Why it breaks

The TODO is not a plan. It has no owner, no trigger and no impact statement, so it survives every prioritisation conversation by never appearing in one (Documentation Decay).

How it breaks as requirements change
  • The TODO is not a plan. It has no owner, no trigger and no impact statement, so it survives every prioritisation conversation by never appearing in one (Documentation Decay).
  • The four months pass, the pilot succeeds, and the second customer's integration is written by copying the first — because copying is now the established pattern and there is no boundary to add it behind (Duplicate Knowledge).
  • The shortcut is only bounded if someone bounded it. Hard-coded assumptions that reached the domain model or the database schema cannot be withdrawn later without a data migration (Data Migration).
  • By the time anyone revisits it, the people who understood the trade-off have moved on, so the code looks like an inexplicable mess rather than a priced decision (Accidental Debt).
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • The pilot decides whether the product line continues, so shipping late and shipping wrong cost the same.
  • There will be either zero more customers or a dozen, and nobody will know which for four months.
  • The team is four people and cannot maintain two integration mechanisms.
Invariants
  • The shortcut must not be able to corrupt data that outlives it — whatever is written to the database must still make sense after the shortcut is removed.
  • The shortcut must not leak into the domain: the hard-coded assumptions live in one adapter and nothing downstream may learn them (Anti-Corruption Layer).

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The person taking the shortcut owns writing down the trade-off at the moment they take it. Not later — the reasoning is only available now.
  • The team owns the trigger: the observable event that converts "we are carrying this" into "we are repaying this" (Revisit Triggers).
  • Someone by name owns the item. An unowned entry in a register is a wish (The Debt Register).
Boundaries
  • Contain the shortcut behind a seam, even a crude one. A hard-coded adapter behind an interface with one implementation is repayable; the same code spread through the request handler is not (Finding Seams).
  • Never let a shortcut reach persisted data or a published contract, because those are the two things you cannot unilaterally take back (Backward Compatibility as a Constraint).
  • Bound it in time as well as in code: an expiry date on the entry forces a decision rather than a drift.

Three conditions, and the one everyone skips

Deliberate debt is acceptable when three things hold: the trade-off is understood, the impact is bounded, and a cleanup path is known. Teams reliably manage the first, sometimes manage the third, and almost never do the second — which is unfortunate, because bounding is the only one that changes what repayment costs.

The sequence below is what "taking debt deliberately" actually looks like as an activity. It costs about half a day in total, most of which is the containment step.

Taking a shortcut on purpose
  1. 1
    Price both options

    Write the fast version and the proper version with an estimate each, and the consequence of each being wrong.

    fails by Comparing only the build costs, which always favours the shortcut, and omitting what the second customer costs under each.

  2. 2
    Decide the boundary the shortcut may not cross

    Name the files, the module, and specifically whether it may touch the schema or a published contract.

    fails by Being stated as a principle rather than as a list of paths, so nobody can tell when it has been crossed.

  3. 3
    Build the seam, even crudely

    One interface with one implementation, so the general version has somewhere to go.

    fails by Being dropped under deadline pressure — the one step that is always cut and the only one that makes repayment cheap.

  4. 4
    Write the entry

    Problem, impact, risk, owner, trigger. Five lines, in the place where work is prioritised (The Debt Register).

    fails by Becoming a TODO comment, which is invisible to prioritisation and decays (Comments).

  5. 5
    Test the behaviour, not the shortcut

    Capture what the customer depends on so the suite survives the rewrite.

    fails by Testing the adapter's internals, so the tests have to be thrown away with the code and repayment gets more expensive.

  6. 6
    Watch for the trigger

    Attach the entry to the observable event — a second integration request — so it surfaces automatically.

    fails by Relying on a quarterly review that gets cancelled twice and then forgotten.

Only the third step costs real time. It is also the step that decides whether the first customer's shortcut costs a week or a month to unwind.

What the record looks like

The entry is short on purpose. Anything longer will not be written under deadline pressure, and anything shorter cannot be acted on by someone who was not there.

Five lines, written the day the shortcut ships
1DEBT-014 Hard-coded Northwind inventory adapter
2
3Problem integrations/northwind/ talks to one customer's API shape
4 directly. No inbound-integration boundary exists.
5Impact A second integration costs ~1 week to add the boundary
6 first, or ~3 weeks if written by copying this one.
7Risk Medium. Contained to one module; nothing outside it
8 imports Northwind types (enforced by a dependency test).
9 Schema is genericno customer-specific columns.
10Owner R. Okafor
11Trigger A second inventory integration is requested, OR
12 2026-03-01, whichever comes first.

The Risk line is the one that repays writing. It records that containment was actually done and how it is enforced, which is what a future reader needs to know before deciding whether repayment is a week or a quarter (The Debt Register).

The shortcut, priced against its alternatives

The comparison people actually face is not "shortcut versus proper design" — it is a set of options with different failure modes, and the shortcut wins some of them honestly.

Three weeks, one pilot customer
OptionSimplicityFlexibilityMigration costOperationalNote
Build the general boundary nowSix weeks. Misses the pilot, and the generality is designed against one known integration, so the abstraction is a guess (Premature Abstraction).
Hard-coded adapter, contained behind a seamThree weeks. Repayment is about a week when the trigger fires. This is the deliberate-debt option, and the containment is what earns the middle column.
Hard-coded adapter, spread through the handlerTwo and a half weeks. Fastest now and the assumptions reach the domain and the schema, so repayment is a rewrite plus a data migration.
Cut scope: manual CSV import for the pilotOne week of engineering and ongoing operational toil. Frequently the best option and rarely considered, because it does not look like engineering (Toil).

caveat These scores compare implementations of a decision whose real variable is not in the table: whether a second customer ever arrives. If none does, the third row was right and the second wasted a week on a seam nobody used. If a dozen arrive, the first row was right all along. Nothing here expresses that, and no scoring exercise can — which is why the trigger, not the score, is the part of this decision that does the work.

How to build it

Most important first.

  • State the trade-off in one sentence with both halves: what you are getting (three weeks) and what it will cost (a rewrite of the adapter, roughly a week, if a second customer arrives).
  • Bound the blast radius before writing the code. Decide which files the shortcut may exist in and hold that line, because containment is what makes repayment cheap (Reducing Blast Radius).
  • Write the trigger as an observable event — "a second inventory integration is requested" — not as a date in the future when someone will feel like it.
  • Record it where work is prioritised, with an owner, and link it from the code rather than describing it in the code (The Debt Register).
  • Make repayment cheap in advance: the tests you write for the shortcut should be about the *behaviour* customers depend on, so they survive the rewrite (Characterization Tests).

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • With the shortcut, contained: the second integration costs a week — write the general boundary you deferred, move the first adapter behind it, add the second. The tests written for the first customer still apply, because they describe behaviour.
  • With the shortcut, uncontained: the second integration costs three to four weeks, because the first customer's assumptions are in the handler, the service and two columns, and every one of them has to be found and unpicked.
  • Without the shortcut: the pilot ships three weeks late, which in this specific case may cost the product line. That is the real alternative and pretending it is free is how "always do it properly" loses credibility.
  • The compounding point: an uncontained shortcut raises the cost of the *third* integration as well, because the second one was written by copying the first (Interest: Why Debt Compounds).
What the recommended approach costs
  • Containment costs time now, exactly when you have least of it, and buys an option you may never exercise.
  • Writing down the trade-off makes the shortcut visible to people who will use it as evidence that engineering cuts corners; that is a real political cost and it is the price of the practice working.
  • A trigger and an expiry date force a decision at a moment that may be inconvenient, and sometimes the honest decision at that moment is to carry it another year — which feels like failure and is not.

What can go wrong

Failure modes
  • The trade-off is understood by one person and never written down, so the debt becomes accidental debt the moment they change teams (Bus Factor).
  • The trigger fires and nobody notices, because it was written as a feeling rather than as something observable in a backlog or an alert.
  • The shortcut is contained in code and not in the schema, so the adapter is replaceable but three columns of customer-specific data are permanent.
  • The mitigation fails: bounding the shortcut behind an interface takes time you claimed not to have, and a team under real pressure will skip exactly that step — which is why the containment decision has to be made before the deadline bites, not during.
Dependencies, and their direction
  • The shortcut depends on assumptions about one customer's API, and those assumptions must not become dependencies of anything else.
  • Repayment depends on the seam existing. Without it, repayment is a rewrite and will be scheduled accordingly, which is to say never (The Risk in a Rewrite).
Misreads
  • "Deliberate debt means we can skip design when we are busy." The deliberate part is the *pricing*, not the skipping. An unpriced shortcut is reckless debt regardless of intent (What Technical Debt Actually Is).
  • "A TODO comment counts as recording it." It does not appear in prioritisation, has no owner and no trigger, and decays out of accuracy within months (Comments).
  • "We will clean it up next sprint." Next sprint is not a trigger; it is a hope with a date attached. Triggers are observable events (Revisit Triggers).
  • "Deliberate debt is always better than accidental." Not necessarily. Accidental debt from genuinely learning the domain is often cheaper and more valuable than a deliberate shortcut that was avoidable (Accidental Debt).
Smells this explains
  • duplicate-knowledge
  • shotgun-surgery

Testing it, and how it ages

What to test, and at which boundary
  • Test the behaviour the customer depends on, not the shape of the adapter, so the suite survives the generalisation (What a Unit Is).
  • Add one test that asserts containment: nothing outside the adapter module imports the customer-specific types. A dependency test is the cheapest way to keep a boundary honest (Dependency Cycles).
  • If the shortcut touches persistence, test that the stored data is interpretable without the shortcut's assumptions, because that is the part you cannot undo.
How this design ages
  • Contained deliberate debt tends to be repaid, because the trigger is observable and the repayment is bounded. Uncontained deliberate debt becomes indistinguishable from accidental debt within about two quarters.
  • A team that takes deliberate debt well accumulates a register that shrinks; one that takes it badly accumulates a register that only grows, which is the diagnostic (The Debt Register).
  • The pattern stops working when it is used continuously rather than occasionally: sustained deliberate debt is just reckless debt with better paperwork (The Complexity Budget).

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • GENERALTrade-off, bound, trigger and owner apply to any technology; what changes is the containment mechanism, which might be an interface, a module boundary, a feature flag or a separate deployable.
  • LIFETIME-SPECIFICFor a pilot that will be deleted if it fails, the correct amount of containment is small — you are buying an option on a future that probably will not happen. For a system with a decade ahead of it, the same shortcut needs a real seam, because the option will certainly be exercised.
  • CONTESTEDThe strongest opposing view is that "prudent deliberate debt" is mostly a story teams tell themselves: empirically the trigger rarely fires, the cleanup rarely happens, and naming a shortcut as debt mainly makes it easier to approve. On this view the honest choice is to either build it properly or cut scope, and never to ship a shortcut you have promised to fix. That is supported by a lot of experience, and the practical answer is that the containment step — not the register entry — is what separates the two cases.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • System Design — the same "ship the specific thing, generalise on the second customer" pattern governs capacity and multi-tenancy decisions, where the retrofit cost is far higher and the bound has to be drawn much earlier.