What is technical debt? A junior developer's guide

Every codebase you'll touch as an intern has technical debt. The senior engineers will gripe about it. Your manager will mention it in 1:1s. There's even a chance you'll cause some accidentally. It's not a personal failing or a sign of bad engineering, it's a tool that gets misused. Here's the honest version, with the four kinds you'll actually meet and how to talk about it without sounding green.

The original metaphor

Technical debt was coined by Ward Cunningham in 1992. The metaphor: writing quick, imperfect code is like taking a financial loan. You ship faster now, but you'll pay interest later in slower future development, more bugs, and harder onboarding. Eventually the interest gets bad enough that you have to pay down the principal (refactor) or the team grinds to a halt.

Most working engineers have a love-hate relationship with the metaphor. It's useful as shorthand. It's misused as an excuse for everything from "we wrote this in a hurry" to "I don't like this code."

The four kinds you'll actually meet

Martin Fowler's framing is the cleanest. Technical debt has two axes: was it deliberate, and was it prudent? That gives four quadrants.

1. Deliberate and prudent ("we know, and we'd do it again")

The team consciously chose to ship something imperfect because shipping fast was worth more than shipping clean. Example: "We hardcoded the list of supported countries because we needed to launch the feature in two weeks. The plan is to move it to a database table next quarter when we add admin tooling."

This is the kind of debt nobody minds. It's documented, time-bounded, and was the right call at the moment.

2. Deliberate and reckless ("we know, and we shipped it anyway")

The team was aware of the proper approach but didn't do it. Example: "We didn't write tests because we ran out of time, and we're going to keep not writing them." Or: "We know the auth code is messy, but nobody's going to clean it up."

This is the dangerous kind. It compounds because the team has accepted lower standards as normal.

3. Inadvertent and prudent ("we did our best, now we know better")

You designed the code well, and then six months later you understood the problem better and realized your original design was wrong. Example: "We modeled User and Account as the same entity, but now that we've built billing it's clear they should have been separate."

This kind is unavoidable. You can't predict everything. The mark of a healthy team is that they revisit and refactor when their understanding catches up.

4. Inadvertent and reckless ("we didn't know what we were doing")

The team shipped without enough understanding to make a good design decision, and didn't realize. Example: "We wrote our own auth system from scratch, didn't know about JWT, and now we have a homegrown thing that's both insecure and hard to extend."

This is what most legacy spaghetti is, accreted over years from teams that didn't know any better at the time.

Why teams take on debt deliberately

Five legitimate reasons:

  1. Time-to-market. Shipping a feature in two weeks vs. eight is sometimes worth a lot of cleanup-debt later, especially if the feature might not even survive customer feedback.
  2. Learning the problem. The first version of any feature is partly a research project. Building it cleanly before you know what it needs to do is wasted effort.
  3. Team capacity. If only one person knows the proper way and they're swamped, it can be better to ship the imperfect version than block.
  4. Competition / strategy. Sometimes shipping anything in a race window matters more than shipping it well.
  5. Investment uncertainty. Why build a clean abstraction for a feature you might cut next quarter?

The unprofitable version of this list is "because we were sloppy." That's the difference between debt and waste.

What technical debt costs you

The interest payments, in concrete form:

Most of these costs are invisible to the people writing the original feature. They show up months later, hit different people, and get blamed on "the codebase" instead of the original decision.

Practice navigating real-shaped debt before your first internship

InternQuest's missions include real intern-grade messes: hardcoded secrets, off-by-one bugs in old code, missing rate limits, broken middleware. Free virtual software engineering internship simulator. Fix the debt, learn the patterns.

Try a mission →

When paying down debt is worth it

Three signals that say "yes, refactor now":

  1. The same area causes the same kind of bug repeatedly. If you've fixed three bugs in the auth module that all stem from the same coupling, refactoring the coupling is now cheaper than the next three bug fixes.
  2. You're about to add new features that touch the messy area heavily. Better to clean before, then build clean. Don't refactor for the sake of refactoring; refactor because the next feature will pay it back.
  3. The mess is actively scaring people away from touching the file. When engineers are too afraid to fix obvious bugs because "you don't want to mess with that file," you're paying interest in slow bug fixes whether you see it or not.

Three signals that say "no, leave it":

  1. It's stable and rarely changes. Old code that works and isn't being touched isn't costing anything. Don't dig it up.
  2. The product strategy might change. Refactoring code that's about to be deleted is wasted effort.
  3. You can't articulate a concrete benefit. "It would be cleaner" is not a benefit. "We'd cut deploy time by 30%" is.

Talking about technical debt as an intern

Three rules.

1. Don't trash the codebase out loud

You will think the codebase is bad. Some of it will be objectively bad. Saying so loudly in your first month is a bad look. The senior engineers know. They wrote some of it. They're often planning to fix it. Loud commentary makes you look like you don't understand the constraints they were under.

Better: ask. "I noticed we're not using async in the user lookup, was that intentional or just historical?" You'll learn the real reason and not embarrass yourself.

2. Distinguish "I don't like it" from "this is debt"

Code can be ugly without being debt. Debt has a specific meaning: it's costing the team something measurable. If the only argument against a piece of code is "it's not how I'd write it," that's not debt, that's preference.

Real debt has consequences you can name: more bugs, slower changes, harder onboarding, real outages.

3. Propose, don't lecture

If you do see debt worth flagging, frame it as a proposal:

"I noticed the email-sending logic is duplicated in three places, with slightly different retry behavior. I'd like to consolidate it into one helper next sprint. Estimated 2 days. The benefit is fixing the inconsistency in retry behavior, which I think is causing the duplicate-email bug we saw last week."

That sentence has the problem, the proposal, the cost, and the benefit. It's hard to argue with. Compare to: "This codebase is a mess, we should refactor everything."

Refactoring as debt repayment

Most debt repayment isn't "we set aside a sprint to clean up." It's the boy scout rule: leave the code a little better than you found it.

Practical version: when you're already touching a file for a feature or bugfix, you can:

Don't do all of these in the same PR. Pick one. Mention it in your PR description: "Also renamed `tmp` to `pendingInvoices` while I was here." Reviewers love these. Quietly accumulating these is how teams pay down debt over time without ever declaring a "refactor sprint."

The trap: gold-plating in the name of debt

The opposite mistake is real too. Some engineers (often senior, sometimes junior) use "preventing tech debt" as license to over-engineer everything. Adding three layers of abstraction for hypothetical future use cases. Refactoring code that was fine. Insisting on patterns that don't pay back.

This is debt of a different kind: over-abstraction debt. The cost is the same: harder to read, harder to change, harder to onboard. Don't be that engineer either.

Heuristic: three uses, then abstract. If you've copy-pasted similar logic three times and you can see the pattern, abstract it. Doing it the first time is premature.

What gets graded as a junior

Your reviewer is probably not measuring how clean your code is in absolute terms. They're measuring:

The good juniors are remembered as "they consistently leave the code cleaner than they found it." Not "they tried to refactor everything in their first month."

The mental model

Technical debt isn't moral. It's just a tool. Used well, it lets you ship things you couldn't otherwise ship. Used badly, it crushes you slowly. The job of any engineer, including you as an intern, is to know which kind you're taking on each time and to leave a paper trail when you take some.

"We're skipping tests on this PR because the deadline is Friday and we'll add them in a follow-up next week" is fine, written down somewhere, with the follow-up scheduled. "We don't write tests" is not fine, even if the result looks the same in the short term.

Train the muscle of leaving things better than you found them

InternQuest's missions are intentionally a little messy, like real intern code. The grader rewards clean fixes that respect the existing codebase, not gold-plated rewrites. Free virtual SWE internship simulator.

Try a mission →