By Dave Fuller, Managing Director, Accent Design
Technical debt is the future cost you take on when you choose a quicker software solution now over a more durable one later. Like a loan, it gets you something faster today, and you pay interest on it every time you work in that part of the system afterwards.
That definition does a lot of quiet work, so it is worth slowing down on it. Technical debt is not a synonym for bad code, and it is not always a mistake. Some of it is sensible borrowing, taken on deliberately to ship something time-critical. Some of it is reckless, accrued through neglect or rushed decisions nobody revisited. The difference between a healthy codebase and a struggling one is rarely whether it carries debt. It is whether the team knows the debt is there, knows what it is costing, and is paying it down on purpose.
This piece explains what technical debt actually is, how it builds up, what it costs you in practice, and what to do about it. It is written for developers and technical leads who want the concept properly, and for product owners who need to understand why a codebase they cannot see is slowing down the roadmap they can.
The short answer
Technical debt is the accumulated cost of decisions, deliberate or accidental, that made software easier to ship in the short term at the expense of changing it later.
The most common confusion is between technical debt and bad code, so it is worth separating them cleanly. Bad code is code that is wrong: it has bugs, it is badly written, it should not have been merged. Technical debt is different. The code can be perfectly correct and still represent debt, because debt is about the gap between the solution you have and the solution the system now needs. A simple approach that was exactly right for last year's product can become debt this year, not because anyone wrote it badly, but because the product grew past it.
That is also why some debt is a reasonable trade rather than a failure. Choosing the quick path to hit a deadline can be the correct engineering decision, provided you know you are doing it and you intend to deal with it later. The problems start when debt is invisible, unacknowledged, or never repaid.
Where the analogy holds, and where it breaks
The borrowing model is useful because the mechanics genuinely match. You take a shortcut and get the benefit immediately. You then pay interest on it: every time you work near that shortcut, the work is a little slower, a little riskier, a little more manual than it should be. The principal is what it would cost to go back and do the thing properly. You can carry the debt indefinitely and just keep paying interest, or you can pay down the principal and stop the bleeding.
A real example from our own work shows how a sensible loan looks. We were building a high-performance platform to replace a crumbling legacy system, and a core requirement was a complex two-way sync between the database and an external API. Building that sync properly, with queue runners, atomic transactions and automated conflict resolution, was around three weeks of work. The client had an immovable deadline: a showcase to major investors. So we made a deliberate call. We paused the proper sync and wrote a lean, one-way script that pulled the data in manually, triggered by a developer from the terminal. No error handling, no automated logging.
It did exactly its job. The platform was fast and stable for the showcase, and the client secured their funding. Then we started paying the interest: for a few weeks, every data change meant a developer stopping work to run the script and check nothing had broken, and the occasional network glitch meant billable hours cleaning up the database by hand. Once the launch window passed, we paid down the principal in a post-launch sprint: we ripped out the manual script and built the proper queue-based architecture with automated retries and webhook listeners. The manual overhead dropped to zero and the debt was settled. That is debt working as intended: borrowed knowingly, used to clear a real hurdle, repaid on purpose.
Where the analogy misleads is just as important. With financial debt you know the balance, the interest rate and the due date, because someone sends you a statement. Technical debt sends no statement. You usually cannot see the balance, nobody tells you the rate, and there is no due date forcing the conversation. That invisibility is exactly what makes it dangerous: it accrues quietly, and most teams only discover how much they owe when the interest payments have already become crippling.
How technical debt accumulates
Debt rarely arrives in one big decision. It builds up from a handful of ordinary sources, most of which felt reasonable at the time.
Deliberate shortcuts under deadline pressure. The sync script above is the clean version of this: a known trade, taken to hit a date. This is the most defensible kind, as long as it gets repaid.
Decisions that were right at the time but outgrown by the product. We inherited a content and e-commerce platform built in the mid-2010s on a then-popular monolithic framework, with the automated test suite skipped to hit the original launch. For the first couple of years it ran beautifully. But as the business scaled and requirements changed, the absence of tests turned every new feature and security patch into an anxious exercise, because nobody could refactor old code without knowing what might break downstream. The original choices were not foolish; the product simply grew past them.
Absent or eroded tests and documentation. The same platform shows the interest this carries. With no automated tests, every deploy needed hours of manual click-testing by a QA engineer. A change that should have been a two-hour feature update became a two-day ordeal, which is development velocity and budget paid straight out as interest.
Inconsistency as a team grows or changes. We took on a high-traffic client portal that had passed through a succession of freelancers and agencies over five years. Each had a different philosophy: a custom routing script added here, a different database abstraction library there, business logic hardcoded into UI templates to save time one Friday afternoon. The result was a "Frankenstein" codebase where a 30-minute bug fix needed three hours of forensic work just to trace how data moved through the layers. The code was actively resisting modification.
Dependencies left to age. Libraries and frameworks that were current at the start drift out of support. Each skipped upgrade is small; the gap between where you are and where the ecosystem has moved is what eventually forces a painful, all-at-once migration.
These cases are patterns we see repeatedly rather than one-off disasters, which is the point. The danger is that debt compounds. Messy code breeds more messy code: a developer asked to add a feature to a degraded module is not going to spend three unpaid days rewriting it first, so they append another shortcut to the pile. Left long enough, a system can reach a kind of technical bankruptcy, where adding even minor features is so risky and slow that scrapping the codebase and rebuilding becomes the cheaper option. That is the far end of the scale, not the normal outcome, but it is where unmanaged debt points.
The AI-generated wrinkle
There is a newer source of debt worth naming on its own. AI build tools, such as Lovable and Cursor, let people produce working-looking software remarkably fast. That speed is real, and for getting an idea in front of users it is genuinely valuable. But it front-loads the borrowing and hides the interest in a particular way: the person who now owns the code often did not write it and does not fully understand it. The result can look finished while carrying significant, invisible debt under the surface, waiting for the first serious change or security review to surface it.
This is its own topic, and we cover the specifics, what tends to break and why, in a separate piece on the hidden cost of building with AI tools. The point to hold here is simply that AI-generated code is not exempt from technical debt. If anything it concentrates it, because the usual signal that debt exists, a developer who remembers the shortcut they took, is missing.
What technical debt actually costs
For an engineer the cost of debt is obvious, because they feel it daily. For a product owner it is easy to dismiss as an engineering preference until it is reframed in terms of the things they actually own: the roadmap, the budget and the risk register. The bill arrives in several forms.
Slower delivery. Features take longer because the system fights every change. The two-hour update that becomes a two-day ordeal is the same story repeated across the whole backlog.
More bugs and firefighting. Fragile, untested code breaks more often and in less predictable places, so engineering time drains into reactive fixes instead of planned work.
Security and compliance risk. Ageing dependencies and code nobody fully understands are where vulnerabilities hide and where audits stall.
Knowledge concentrated in too few heads. When only one or two people understand how a tangled system holds together, every holiday, illness or resignation becomes a delivery risk.
Morale and hiring cost. Good engineers do not enjoy working in a codebase they dread, and they leave. Replacing them is slow and expensive, and onboarding into a "Frankenstein" system is slower still.
The thread through all of these is that the cost is paid as interest, in small daily increments, rather than as a single visible bill. That is precisely why it stays ignorable until it is large. A useful exercise is to make it concrete: estimate the hours your team loses each sprint to firefighting, manual testing and working around known-bad areas, then multiply that out across a year. The number is usually far larger than anyone expects, because nobody had ever added it up.
If debt were always bad, the advice would be simple: never take any. It is not, which is where judgement comes in.
Good debt is taken on knowingly, for a clear reason, with an intention to repay. The sync script was good debt: a deliberate trade to clear a hard deadline, repaid as soon as the pressure lifted. Shipping a deliberately simple first version to learn whether anyone wants the feature is good debt. In both cases the team chose the shortcut with eyes open and a plan to deal with it.
Bad debt is accrued through neglect or lack of skill, and left to compound. The skipped test suite that nobody ever returned to, the hardcoded logic added to save a Friday afternoon, the dependency upgrades quietly skipped year after year. None of these were chosen as trades; they were defaults that nobody owned.
So the real failure is not having debt. Every working codebase carries some. The failure is not knowing you have it, not tracking it, and not choosing it deliberately. A team that can point to its debt, explain why each piece is there, and say which parts it is servicing and which it is repaying is in a fundamentally healthier position than a team with less debt that has no idea where any of it is.
What to do about it
Managing technical debt is mostly unglamorous and entirely learnable. Four things matter.
Make it visible. You cannot manage debt you are pretending is not there. Name it, write it down, and track it alongside features rather than in the heads of the people who created it. The act of listing it is often the most valuable step, because it converts a vague sense of dread into a finite, discussable list.
Decide what to service and what to let ride. Not all debt is worth repaying. Debt in a stable, rarely-touched corner of the system can be left to sit, paying its small interest quietly. Debt in the areas you are actively building on is where repayment earns its keep. Triage deliberately rather than trying to fix everything or ignoring all of it.
Pay it down where you are already working. Large, separate "rewrite everything" projects are risky and tend to get cancelled when the business needs features instead. It is usually safer and more durable to repay debt incrementally, improving each area as you touch it for other reasons, so repayment rides along with work that is already justified.
Get an outside read when you need one. The people who built a system are often the least able to see its debt clearly, because every shortcut made sense to them at the time. An independent code audit, by someone whose job is to look for exactly this, turns the invisible balance into a documented one you can act on. This is the core of what our AI Prototype Rescue and Code Audit service does: assess an existing or inherited codebase, surface the debt, and give you a prioritised plan for dealing with it.
The earlier and more deliberately you build for maintainability, of course, the less of this you accumulate in the first place. That is the principle behind how we approach bespoke software development: treating reliability and long-term maintainability as the default, so debt builds slowly and stays serviceable, rather than something to be excavated later.
When to bring in help
Most teams can manage their own debt for a long time. The signals that it has passed the point of comfortable self-service are fairly consistent:
- Delivery keeps slowing despite the team working as hard as ever.
- There are areas of the system nobody wants to touch, and changes get routed around them.
- You have inherited a codebase, or generated one with AI tools, that nobody on the team fully understands.
- Bug fixes routinely take far longer than the size of the fix would suggest.
If those sound familiar, an outside audit is usually the fastest way to turn the problem from a vague worry into a clear plan. Our AI Prototype Rescue and Code Audit service is built for exactly this, particularly where the codebase was inherited or AI-generated. Where the need is broader, building or rebuilding software to last, that is the remit of our bespoke software development work.
FAQ
Is technical debt always bad? No. Some debt is a deliberate, sensible trade, taken knowingly to ship something time-critical and repaid afterwards. Debt only becomes a serious problem when it is invisible, unacknowledged, or left to compound through neglect.
What is the difference between technical debt and bad code? Bad code is incorrect or poorly written code that should not have shipped. Technical debt can be perfectly correct code that no longer fits what the system needs, usually because the product has grown past a decision that was reasonable when it was made. The two often overlap, but they are not the same thing.
How do you measure technical debt? There is no single universal metric, but you can get a practical estimate by tracking the time lost to it: hours spent firefighting, manually testing, and working around known-bad areas, totalled across a period. Some teams also track it qualitatively, by keeping an explicit list of known debt and its likely cost to fix.
Can you ever pay it off completely? Not entirely, and that is fine. Every actively developed codebase accumulates some new debt as it changes and grows. The realistic goal is not zero debt but managed debt: known, tracked, and serviced deliberately rather than allowed to compound unseen.
Is AI-generated code more prone to technical debt? It can be, because the speed of AI tools front-loads the borrowing while hiding the interest, and the person who owns the resulting code often did not write it and does not fully understand it. The code is not automatically worse, but the usual signals that debt exists are weaker, so it is easier for it to go unnoticed.
Dave Fuller is Managing Director of Accent, based at the Enterprise Centre, University of East Anglia. He has worked in web development for 27 years, holds qualifications in web accessibility and design, and was previously Technical Director at Accent.