$ cat /blog/on-technical-debt-and-when-to-pay-it.md

How I Decide When to Pay Technical Debt in a Bootstrapped Product

A pragmatic framework for choosing when to pay down debt versus shipping features in a bootstrapped product.

How I Decide When to Pay Technical Debt in a Bootstrapped Product

Technical debt is a fact of life when you're building alone (or as a tiny team) and trying to get to revenue. I don't aim for a perfect codebase—I aim for a sustainable one that lets me move fast and keep customers happy. This post is the pragmatic framework I use to decide when to pay debt down vs. shipping the next feature.

I've built and maintained three bootstrapped products. I've ignored debt until it caused outages, and I've over-refactored features that never paid back. Both were costly. Over time I arrived at a simple, repeatable process that trades off risk, customer impact, and ROI.

The core idea

Don't treat "technical debt" as a single thing. Break it into measurable dimensions and score it. Prioritize fixes that reduce customer-facing risk or materially speed up delivery. Defer low-impact cleanup until it becomes a blocker.

My decision is driven by three questions:

  • Is this debt hurting customers right now?
  • Is it slowing down development (cycle time) significantly?
  • Does fixing it unlock new revenue or reduce real risk (downtime, security, data loss)?

If the answer is "yes" to any of those, it moves up the queue.

Dimensions I measure

I use six dimensions to score a debt item. Each dimension is 0–5 (higher is worse), then I compute a weighted score.

  • Customer Impact (0–5): Are customers affected? Bugs, slow page loads, broken flows.
  • Frequency (0–5): How often does this pain occur? Once a month vs. every deploy.
  • Developer Pain (0–5): Does it slow me/engineers down? Long debugging, slow tests, merge conflicts.
  • Risk (0–5): Could this cause data loss, security issues, or major outages?
  • Revenue Impact (0–5): Does it block a revenue stream or conversion?
  • Effort to Fix (0–5): Rough estimate of how long fix will take (1 hour vs. 4+ weeks). This one is inverted when computing priority.

I keep weights tuned to my stage. For a solo indie product with small MRR, I care most about Customer Impact and Developer Pain. For a product approaching $10k MRR, Risk and Revenue Impact get heavier weight.

A typical weight vector I use:

  • Customer Impact: 3
  • Frequency: 2
  • Developer Pain: 3
  • Risk: 2
  • Revenue Impact: 2
  • Effort to Fix: 1 (used as a divisor)

Score = (3CI + 2F + 3DP + 2R + 2*RI) / (1 + Effort)

Where Effort is 0–5, but I add 1 to avoid divide-by-zero. Higher score => higher priority.

Simple script I actually use

I keep a tiny Python script in my repo to compute scores during triage. Paste into a scratch file and run while reviewing issues.

# debt_score.py
def score(ci, f, dp, r, ri, effort):
    weights = {'ci':3, 'f':2, 'dp':3, 'r':2, 'ri':2}
    num = weights['ci']*ci + weights['f']*f + weights['dp']*dp + weights['r']*r + weights['ri']*ri
    denom = 1 + effort
    return round(num/denom, 2)

# Example: customer-impact=4, frequency=3, dev-pain=5, risk=2, revenue=1, effort=3
print(score(4,3,5,2,1,3))  # -> priority score

I run this locally when grooming tech-debt issues. For me, any item with score > 6 ends up on the "pay" list this sprint or as a small spike; 4–6 goes into backlog grooming; <4 is deferred.

You can adapt weights depending on your priorities.

Practical thresholds and heuristics

Numbers are just anchors. Here are rules I've found useful:

  • If a bug impacts customers more than once a week, fix it immediately.
  • If a piece of debt makes debugging or deploying take >30% longer than normal, pay it down. I measure this informally: if a deploy used to take 10 minutes and now takes 30, that's a sign.
  • If a security/data-loss risk exists (even if low probability), prioritize it. I don't gamble on this.
  • Cap tech-debt work to 10–25% of your total development time in normal weeks. For me, 15% is the sweet spot.
  • When blocked on a feature because of debt, do a "spike" and fix just enough to unblock (not a full rewrite).
  • Avoid heroic rewrites. If it's more than 3 weeks of work, evaluate whether the business case exists.

These aren't rules of law—more like guardrails that keep me from oscillating between “pay everything” and “never pay anything.”

Triage process I follow (how I actually do it)

  1. Label and quantify: I tag issues with label: tech-debt. Each ticket gets the six-dimension estimates above as checkboxes or comments.
  2. Monthly review: Once a month I run through tech-debt tickets, compute scores, and reorder.
  3. Sprint allocation: I reserve 15% of weekly time for debt. That maps to 1–2 small PRs for me.
  4. Small wins: Prefer a series of small PRs (under 2 hours) over a big merge. Small fixes reduce risk and integrate faster.
  5. When blocked: If a debt item blocks a high-value feature, do a focused 1–3 day spike to remove the blocker, then ship.
  6. Track ROI: After a fix, measure the outcome (fewer incidents, faster deploys, improved conversion). If the fix doesn't move the needle, I stop.

I use GitHub labels, ZenHub (or GitHub projects), and Sentry alerts to surface issues. For build/test speed, I monitor CI duration in GitHub Actions.

Example cases

Case A — High customer impact, low effort

  • Problem: Checkout occasionally times out, affecting 2% of purchases.
  • Score: High CI, high Frequency, low Effort.
  • Action: Fix now. I shipped a retry and optimized the DB query; issue resolved in <4 hours. Revenue recovered immediately.

Case B — Slow tests

  • Problem: Local test suite takes 45 minutes.
  • Score: High Developer Pain, medium Effort (maybe 1–2 days), low immediate Customer Impact.
  • Action: Carve into small tasks: parallelize tests, mock network calls, introduce focused unit tests. Make incremental PRs. Allocate part of the weekly debt budget.

Case C — Legacy framework upgrade (big rewrite)

  • Problem: App uses an end-of-life framework; upgrade is a 6-week project.
  • Score: Medium Risk, low Customer Impact today, high Effort.
  • Action: I deferred the full rewrite. Instead I isolated modules and introduced a compatibility layer to be replaced incrementally. This bought time and reduced risk while avoiding a huge upfront cost.

Tests, monitoring, and automation — pay once, save forever

When I do pay down debt, I aim to automate the protection so we don't regress.

  • Add a regression test for the bug you fixed (unit or integration).
  • Add a Sentry alert or Prometheus metric for the failure mode.
  • Add a CI check (lint, basic static analysis) if the debt was about code style or safety.

Example commands I use:

  • git branch debt/short-description
  • run tests: pytest -q
  • add Sentry alert: create alert for error rate > X%

Automation converts a one-time fix into long-term savings.

When not to pay (or to delay)

  • The fix is purely aesthetic, with no measurable benefit.
  • The effort is large (>3 weeks) and there's no revenue or risk justification.
  • The area is going to be replaced soon (certify that replacement is actually scheduled).
  • You're early and iterating quickly—some debt is a cost of learning.

If you delay, add an explicit "why" and re-evaluate in the next monthly pass. Don't let deferred debt become forgotten debt.

Failure mode: the "shiny rebuild"

I've been tempted to rewrite modules because the code looked ugly. One rewrite consumed 6 weeks and delayed a revenue-generating feature. Worse, the rewrite introduced subtle bugs and regressions.

Lesson: Rewrites are risky. Only rewrite when:

  • You can't ship new features without it, or
  • You will save more developer time than the cost of the rewrite within a reasonable horizon (I use 6 months to 12 months as my ROI horizon).

Otherwise, refactor incrementally.

Tools I use

  • Issue tracking: GitHub issues + labels
  • Errors & monitoring: Sentry (errors), Grafana/Prometheus (metrics), Postgres slow query logs
  • CI: GitHub Actions (measure CI duration)
  • Small script: debt_score.py (above) in repo tools
  • Productivity: a weekly "tech-debt small wins" checklist in my project board

Conclusion / Takeaways

  • Treat debt like a measurable backlog item, not a vague guilt. Score it on Customer Impact, Frequency, Developer Pain, Risk, Revenue Impact, and Effort.
  • Reserve 10–25% of your development time for debt so it doesn't snowball.
  • Prefer small, incremental fixes and automation (tests/alerts) over big rewrites.
  • Prioritize anything that impacts customers, increases incidents, or blocks revenue.
  • Use simple tooling (labels, a tiny scoring script, Sentry) to keep the process lightweight.

If you want my scoring script or a template issue checklist I use, tell me the tools you're using (GitHub, Jira, etc.) and I’ll share a starter repo you can drop into your project.

If you liked this, follow along on Twitter (@fullybootstrapped) for more practical indie-hacker engineering notes.