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)
- Label and quantify: I tag issues with label: tech-debt. Each ticket gets the six-dimension estimates above as checkboxes or comments.
- Monthly review: Once a month I run through tech-debt tickets, compute scores, and reorder.
- Sprint allocation: I reserve 15% of weekly time for debt. That maps to 1–2 small PRs for me.
- Small wins: Prefer a series of small PRs (under 2 hours) over a big merge. Small fixes reduce risk and integrate faster.
- When blocked: If a debt item blocks a high-value feature, do a focused 1–3 day spike to remove the blocker, then ship.
- 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.