Skip to main content

The Developer's Guide to Technical Debt

Last updated . Sources are named and dated inline - how we source claims.

Practical strategies for recognizing, preventing, and fixing tech debt in your daily work - whether you are two months in or twenty years deep

Technical debt is not just a management problem. Every developer creates it, inherits it, and fights it. This hub gives you a curated reading path from the basics through advanced strategies, plus actionable checklists you can use today.

Choose Your Path

What Debt Looks Like From Inside the Code

You will not find a document describing your codebase's debt. You find symptoms, one at a time, while trying to do something else. Each is evidence about how the code got this way.

The file everyone routes around

You ask where a change belongs and someone says "not in there, put it in the new service." The old file still runs and still takes bug fixes, so logic that belongs together now lives in two places, and you pay a second read every time. Somebody stopped maintaining it without ever deciding to retire it.

The signature that grew a fourth boolean

What began as applyDiscount(order) is now applyDiscount(order, skipCache, dryRun, legacyRules, quiet). Nobody can say which combinations were ever exercised, so calling it safely means reading the whole body first. Each requirement was bolted onto the nearest existing function because that was the smallest diff at the time.

The test that never came back

It went flaky during a release, someone commented it out to unblock the deploy, and the restore ticket was never filed. The code it covered has changed several times since, so your green suite means less than it looks like. The team can borrow under pressure but has no ritual for paying it back.

The comment that explains only the workaround

// have to call this twice or the totals come out short. Useful as far as it goes, but why do they come out short? That was never written down, so nobody can safely remove the second call and the real defect stays unfixable. This is the exact point where someone ran out of time to investigate.

Three copies that have quietly diverged

The same validation was copied into a second service, then a third, and only two of them trim whitespace, because a bug got fixed where it was found rather than everywhere it lived. You pay with a defect that reproduces in one environment and refuses to in another. Copying was faster than agreeing where the logic should live.

The things nobody can prove are safe to touch

A config flag in every environment file, with one reference left inside a branch that may or may not be reachable. A dependency pinned a major version back because the upgrade broke something once and the person who tried has left. Neither can be removed without an afternoon of proof nobody wants to fund, so both stay, and every advisory for that package now needs a manual read. Rollouts and upgrades had owners; the cleanups never did.

Why you stop seeing all of it

Notice how many of these are obvious on your third day and invisible by your sixth month. That is adaptation, not carelessness: you learn the workaround, it becomes muscle memory, and the friction stops registering. So the newest person on your team is your best detector, and the least likely to speak up. Ask what confused them this week, and write it down.

The Daily Decisions That Create It or Prevent It

Most debt does not arrive in a single architectural mistake. It accumulates through ordinary choices made under mild time pressure, each of which looked reasonable on its own morning. These are the ones that get made inside a normal workday.

How much one pull request carries

A pull request that changes one thing gets read. One that also carries a rename, a dependency bump, and an opportunistic refactor gets skimmed and approved. Splitting costs you now and pays everyone else later, which is exactly why it gets skipped.

Whether the shortcut gets written down or just taken

Shipping a deliberate shortcut is a legitimate engineering move. It becomes debt at the moment nobody records the trade: what you chose, what you gave up, and the condition that should bring someone back. Three lines in the PR description are enough.

Whether a TODO carries an owner and a condition

// TODO: clean this up is a wish. // TODO(priya): replace with the batch endpoint once the new billing API ships is something a person can act on and another can chase. Name an owner and a trigger, or you are hiding the work rather than deferring it.

Deleting dead code instead of leaving it in case

Unreachable code kept "in case we need it" means every reader has to judge whether it matters, every refactor has to keep it compiling, and every search returns it. Version control already remembers it.

Writing the test that would have caught the bug you just fixed

The minutes after a fix are the only time you will understand that failure this well, and the only time the test is nearly free. Skip it and the same shape of bug is free to come back, except next time nobody will remember the shape.

Naming things for what they do now

A function called syncLegacyUsers that stopped touching the legacy system when the migration finished hands every new reader a wrong assumption to unlearn. Renaming is the cheapest refactor there is and the easiest to postpone, because the person it confuses has not been hired yet.

None of these needs a sprint or anyone's approval. They need doing while you are already in the file, because coming back later is the whole problem.

How to Raise It Without Sounding Like You Are Complaining

Two sentences describe the same file. "This code is bad" asks your lead to agree with your taste. "Getting the discount change in cost me a day" asks them to look at time already spent. Only one of those is an argument.

1

Ask about intent before you judge

There is usually a reason, and usually a constraint you have not met yet: a customer still on the old behaviour, a migration that stalled, a deadline that was real at the time. Ask what it was working around before you decide it is wrong.

2

Bring the cost in time you personally lost

Not a projection of what the team might save someday. What you actually spent, on what, this week. That is defensible under questioning and specific enough that someone can go and check it.

3

Propose something small enough to fit a sprint

"We should rewrite the billing module" has no path to yes, because nobody can say what it costs or when it ends. "Give me two days to pull the tax rules into one place so we stop editing three files per rate change" has a size, a finish line, and a benefit.

4

Attach the fix to work already scheduled

If a feature is landing in that area next month, that is the cheapest moment the cleanup will ever have: context loaded, tests running anyway, risk already accepted. You are asking to spend committed time differently, not asking for new time.

5

Pick the moment

Retro and planning exist for this conversation. Mid-incident does not, however vindicated you feel. The person you would be talking to is holding an outage, and will remember only that you chose that moment to be right.

6

Take "not now" well, then ask what would change it

Sometimes the answer is genuinely no, not this quarter. Ask what would have to be true for it to become a yes - a customer complaint, a missed date, a second bug in that area - then watch for it. Brought back with a trigger beats brought back louder.

The complaint framing fails structurally, not politically. It asks someone to agree with you about quality, which is a matter of judgement, and your lead has no obligation to share yours. The cost framing asks them to look at time already spent, which is a matter of record. Same file, same fix, different conversation. When it has to go past your lead, selling to management covers how the same argument is built for a budget holder.

Quick Actions

You do not need to read everything first. Pick one action and do it today.

Your Reading Path

Three guides sit under this page, and the order matters more than their labels suggest. They are not beginner, intermediate and advanced versions of one article - each answers a different question.

Which guide, and in what order

The junior guide is written for a situation rather than a seniority level: being new to a codebase and not yet able to tell a strange decision apart from a wrong one. You come away able to name what you are looking at, keep a list that is useful to your team rather than irritating to it, and ask about code without putting its author on the defensive. If you have twenty years behind you and started somewhere new last month, it is still your guide: the first ninety days on unfamiliar code is the junior situation whatever your title says.

The senior playbook assumes you can already spot debt and picks up at the harder part: getting something done about it when you do not own the roadmap. It covers sizing a refactor so it can be scheduled, sequencing the work so the team keeps shipping, and building a case that survives a planning meeting. Read it with one specific piece of debt in mind; in the abstract it is advice, and with a real file attached it is a plan.

Daily habits is the shortest of the three and the one worth returning to. It takes the small choices described above and turns them into defaults you stop having to decide about. Take it last, because a habit sticks only once you have felt the problem it prevents.

Do not read all three in one sitting. After each, pick one thing and use it on real work for a week: one question asked instead of an assumption made, one proposal sized to fit a sprint, one pull request kept to a single idea. Moving on before you have practised anything gives you more vocabulary and no more leverage. The five steps below take the same ideas outward across the rest of the site; each builds on the one before it.

Downloads for Developers

Printable checklists and templates you can start using immediately. All free, no signup required.

Code Review Checklist

PDF - 4 pages

A printable desk reference covering correctness, design, AI-generated code, security, performance, testing, and maintainability. Includes a review verdict section.

Download PDF

Assessment Checklist

PDF - 6 pages

Six-page checklist covering code quality, architecture, dependencies, testing, documentation, infrastructure, and process. Scoring matrix with 117-point scale.

Download PDF

Sprint Planning Template

XLSX - 4 sheets

Sprint backlog with example debt stories, capacity planning with 20/80 debt allocation, debt inventory with risk scores, and sprint metrics with debt ratio calculations.

Download XLSX

Frequently Asked Questions

Yes, but do it constructively. When you spot something that looks like debt, ask questions rather than making accusations. "I noticed this pattern - is there a reason it's done this way?" works far better than "This code is terrible." Keep a running list of things that confuse you or slow you down. That list is valuable data your team needs, and fresh eyes catch things veterans have stopped seeing. If your team has a debt backlog, add items there. If they do not, that is itself a conversation worth starting.

Start with tests. If the code you want to refactor does not have tests, write characterization tests first - tests that document what the code actually does right now, not what it should do. Then refactor in small steps, running tests after each change. Never mix refactoring with feature work in the same commit. Use feature flags if the refactoring is large enough that it cannot be completed in a single day. The key principle: make the change easy, then make the easy change. See our techniques guide for specific refactoring patterns.

Apply the boy scout rule: leave code a little better than you found it. When you touch a file for a feature, clean up one thing while you are there - rename a confusing variable, extract a method, add a missing test. These micro-improvements add up fast and do not require separate tickets or permission. Also, start tracking the time you lose to debt - "I spent 3 hours working around the payment module's limitations" is concrete data your lead needs. When you can show that debt is already costing time, the "we don't have time" argument flips. See selling to management for how to build that case.

It depends on the scope. Small, localized improvements - renaming, extracting methods, adding types - absolutely do it in the same PR. For larger structural changes, create a separate ticket and PR. Mixing big refactors with feature work makes code review harder, makes rollbacks riskier, and muddies the commit history. A good rule of thumb: if the refactoring touches files your feature does not need, it belongs in its own PR. If it only touches files you are already changing, include it but keep the refactoring in separate commits.

Stop talking about code quality and start talking about delivery speed. Track concrete metrics: how long features take in debt-heavy areas vs clean areas, how many bugs originate from known debt, how much time is spent on workarounds. Present a specific proposal - not "we need to fix everything" but "if we spend 2 sprints on the payment module, feature delivery in that area will go from 3 weeks to 1 week." Make it about outcomes, not aesthetics. Our measuring guide and selling to management page have frameworks for this.

Tech debt is a deliberate or inadvertent trade-off where you accept a suboptimal solution now, knowing it will cost more to change later. Bad code is just bad code - no trade-off was made, no value was gained. The distinction matters because tech debt implies a rational decision (ship now, fix later) while bad code implies a skill or process gap. In practice, both slow you down, but the fix is different: debt needs prioritization and scheduling, while bad code needs better reviews, mentoring, and standards. The original debt metaphor from Ward Cunningham was specifically about shipping a first version to learn from users - not about writing sloppy code. See what is tech debt for the full breakdown.

Related Resources

Ready to Go Deeper?

See how real teams tackled their tech debt, or explore how AI is changing the game for developers.