Guide: from prototype to production

Spotting technical debt in AI-generated code

Technical debt is a shortcut that saves time today and charges interest on every change later. Generated code produces certain kinds of it especially easily, because a language model solves the task in front of it, not the application's next few years. Six typical kinds of debt are set out here, each with symptoms visible even without programming knowledge.

A tall tower of white modular blocks, patched at the base with cardboard and tape, orange light shining through a gap at its footAI-GENERATED
UPDATED
12 September 2026
READING TIME
13 min

Short answer

Six kinds of debt are typical in AI-generated code: duplication instead of reuse, missing abstraction layers, a data model with no migration history, configuration baked into code, missing tests, and dependencies with no maintenance path. They show up as symptoms such as bugs that need fixing separately in several places, or updates nobody dares to install.

01

What is technical debt, and why does it arise differently in AI code?

Technical debt consists of structural decisions that make one change faster today and every later one more costly. In AI-generated code, it often arises incidentally: each prompt solves a task on its own, the code runs and gets accepted, and the next task builds beside it rather than on it. The debt only becomes visible once something has to change.

The series From prototype to production-ready application covers the broader path; here, the question is not whether an application is secure today, but what it will cost in changes over time.

When generating code, a language model only sees the slice of the application that sits in its context. If a matching function already exists in another file, it may well write a new one anyway. The result works, so it goes unnoticed that the application gets harder to change with every iteration. A GitClear analysis shows a matching trend: how often newly written code calls a function defined elsewhere fell 35% since 2023, from 343 to 223 calls per thousand changed lines (GitClear, 2026). That is a correlation, not proof of a cause. What the studies measure in detail is covered in What studies show about the quality of AI-generated code.

How a prompt turns into debt
  1. 01Promptsolves a task
  2. 02Code runsgets accepted
  3. 03Next promptbuilds beside it, not on it
  4. 04Changehits several places
  5. 05Interestevery change gets pricier

02

Which kinds of debt are typical in generated code?

Six kinds of debt that generated code easily produces are set out here, each arising from individually correct local solutions. Each has a symptom you can observe without knowing the code, and a cost driver that determines how expensive the debt becomes in operation. Each kind of debt gets a closer look afterwards.

Kind of debtVisible symptomCost driver
Duplication instead of reusea fixed bug reappears somewhere elseevery rule change has to be multiplied
Missing abstraction layersa vendor switch touches half the applicationlock-in to a vendor and a data structure
Data model with no migration historynobody knows how the production database came to berisky schema changes, no identical test environment
Configuration baked into codecode has to change for a different environmentevery switch turns into a release
Missing testssomeone clicks through everything after each changemanual checking grows with every feature
Dependencies with no maintenance pathupdates pile up unappliedsecurity updates back up
Six kinds of debt at a glance

03

How do you spot duplication instead of reuse?

Most clearly, when a bug gets fixed in one place and reappears in the same form somewhere else, or when two pages calculate the same figure differently. The cause: the same logic exists more than once, because requests to the model produced their own solutions instead of reusing an existing one.

Symptom: the sales tax shown in the cart differs from the one on the invoice. A phone number check works on the sign-up form but not on the profile page. A changed error message still appears in its old wording on three other pages.

Cost driver: every change to a business rule has to be implemented and checked as many times as the rule was copied, and every forgotten copy is a new bug. As the application grows, the odds of finding every instance fall. Duplicate-detection tools such as jscpd make the scale visible before you decide on a refactor.

bash
npx jscpd src
Finding copied code blocks in the source

04

What does it mean when abstraction layers are missing?

It means the interface, the business logic, and access to the database or external services are not kept separate. A page queries the database directly, a button calls the language model directly. That works until something has to change: then a switch of vendor or data structure touches every file that references it directly.

Symptom: ask whether the mail service or the language model can be swapped out, and the answer is "it's baked in everywhere." A new column in the database drags changes through many interface components. Business rules can only be tested by running the live interface.

ts
// Without a layer: every component knows the table, columns and filter
const { data } = await supabase
  .from("invoices")
  .select("id, amount, due_date")
  .eq("status", "open");

// With a layer: the component only knows the business question
const openInvoices = await invoices.listOpen();
Direct access in the component, and the same query behind a layer (simplified)

Cost driver: lock-in to a vendor or a data structure grows with every file that uses it directly. A switch, say from a hosted to a locally run language model, then turns from a configuration step into a rebuild. Logic with no layer of its own can also only be tested through the interface, which makes tests slow and brittle.

05

Why is a data model with no migration history a form of debt?

Because then the state of the database only exists in live production. Nobody can trace when or why a column appeared, no second environment can be built identically, and no change can be rolled back in an orderly way. Every further schema change then becomes an operation on the one and only copy.

With and without migration history

CriterionWithout historyWith history
Setting up a new environmentrebuilt by hand, with discrepanciesreproducible from the files
Tracing a changerelies on individual memorya dated file in the repository
Rolling back a changeimprovised, with data at riskplanned and tested beforehand
Handover to another teamneeds a stocktake firstthe schema is documented

Symptom: phrases such as "we changed that in the dashboard once" or "the table looks different on the test system." Bugs that only show up in production because the test database has a different structure.

Cost driver: every schema change needs a stocktake beforehand and a manual check afterwards, and the risk of data loss grows with the amount of data. Supabase therefore recommends running every schema change, even small ones, through versioned migration files. How to bring an existing Supabase schema into migrations after the fact is shown in Built with Lovable, Bolt or Replit.

06

How do you spot configuration baked into code?

By values that should differ between test and production, but can only be switched through a code change: database addresses, credentials, sender addresses, model names, threshold values. The Twelve-Factor App calls for a strict separation of configuration and code, and offers a simple test question.

The test question is: could the code be published as open source at any time without exposing credentials? Configuration there covers everything that varies between environments, and belongs in environment variables rather than in constants or versioned configuration files (The Twelve-Factor App).

Symptom: for a test run, someone swaps an address in the code and resets it before going live. A test environment sends emails to real addresses or writes to the production database. An access key cannot be rotated without shipping a new version.

Cost driver: every switch turns into a release that needs a developer, swapping a key becomes a small project of its own, and mix-ups between environments hit real data. The more environments an application needs, say test, staging and production, the faster this driver grows.

07

Why do missing tests slow down every change?

Without automated tests, nobody knows after a change whether the rest of the application still works. The result is caution: changes get bundled, postponed, or clicked through by hand. With generated code, there is an added risk: a model can touch parts of the code during a change that nobody asked it to touch.

Symptom: someone clicks through the main workflows by hand after every change. Fixed bugs come back. The only safeguard against side effects is the line "don't change anything else" in the prompt. Or there are tests that stay green even though the feature is broken, because they only check what they simulated themselves beforehand.

Cost driver: the manual testing effort grows with every feature, and postponed changes back up, security updates among them. Without tests, there is also no way to check what an AI assistant has changed in existing code. The biggest lever of these tools, fast changes, then becomes the biggest risk.

08

Which dependencies have no maintenance path?

Ones where nobody knows why they were added, that have not had a new version in a long time, or whose next version demands a rebuild. Generated code pulls in packages readily, and each one is a bet that someone maintains it. Some suggested packages do not even exist.

576,000code examples in Python and JavaScript from 16 language modelsUSENIX Security, 2025
21.7%hallucinated packages on average for open models, at least 5.2% for commercial onesUSENIX Security, 2025
205,474distinct hallucinated package namesUSENIX Security, 2025

Anyone who registers a hallucinated package name can smuggle malicious code into projects that accept the suggestion unreviewed. Symptom: a long list in package.json that nobody can explain, ignored warnings during install, an update that stalls because of a big version jump.

bash
npm outdated      # installed, wanted and latest version per package
npm audit         # known vulnerabilities in the dependencies
npm ls <package>    # which dependency pulled a package into the project
Checking the state of dependencies (npm)

Cost driver: npm outdated shows, in the Wanted column, the highest version that fits the specified version range, and in Latest, the current one. If Latest falls outside that range, a bigger version jump is usually due. The more such jumps pile up, the more likely a security update turns into a rebuild, and an abandoned package has to be replaced entirely.

09

How do you assess how serious the debt is?

Not every debt has to be paid down. Three questions decide it: will the application keep changing, does it process sensitive or business-critical data, and is the foundation of migrations and tests missing? A checklist shows the finding, and a decision guide shows what to discuss next.

Checklist

Quick test without code knowledge

0 of7

Every tick is a sign of debt worth a closer look.

Decision path

Pay down the debt, or carry it deliberately?

    All questions and results as a list
    • Will the application keep being changed?
      • Yes, it keeps evolving, continue with: Can the data model be built from migration files?
      • No, it stays largely as it is, continue with: Does it process personal or business-critical data?
    • Can the data model be built from migration files?
      • Yes, continue with: Does a test fail if you deliberately break a central rule?
      • No, Result: The foundation comes first
    • Does it process personal or business-critical data?
      • Yes, Result: Dependencies and configuration first
      • No, Result: Carry it deliberately
    • Does a test fail if you deliberately break a central rule?
      • Yes, Result: Pay down debt exactly where you are changing things
      • No, Result: Tests before new features
    • Result: The foundation comes firstWorth discussing: capturing today's database state as a starting migration and building a test environment from it, before new features get added.
    • Result: Tests before new featuresWorth discussing: which business rules are critical and how tests can secure exactly those, before further changes build on top.
    • Result: Pay down debt exactly where you are changing thingsWorth discussing: tackling duplication and missing layers in the parts that get changed next, rather than rebuilding everything at once.
    • Result: Dependencies and configuration firstWorth discussing: closing known vulnerabilities in dependencies, getting credentials out of the code, and clarifying who installs security updates.
    • Result: Carry it deliberatelyWorth discussing: which debts are known, where they sit, and what event would trigger a fresh assessment, say new data or new user groups.

    10

    What determines how much effort paying it down takes?

    There is no credible blanket figure, because the effort depends on the specific application. What determines it is the scope of data storage, the number of interfaces, the data's protection needs, the planned pace of change, and whether migrations and tests already exist. Knowing these points lets you compare different estimates.

    • Scope of data storage: every table with no migration history and every copied rule adds to the stocktake.
    • Interfaces: every directly wired-in service with no layer of its own is a point where a switch bites.
    • Protection needs: personal or business-critical data raises the requirements for tests, logs and updates.
    • Pace of change: an application that keeps evolving pays the interest with every change; a stable one barely does.
    • Existing foundation: where migrations and meaningful tests already exist, the remaining debt can be paid down step by step.

    Debt becomes especially visible when an application hands over from one team to another. Which artefacts a handover into regular operation needs is covered in From proof of concept to regular operation.

    iiterate Technologies GmbH, based in Adenau, builds custom software and AI applications from architecture through implementation to operation, and hands over source code and documentation. To talk about your application: Contact.

    Frequently asked questions

    Does all AI-generated code carry technical debt?

    All code carries debt, hand-written code included. With generated code, certain kinds arise more easily, because every request gets solved locally and working code gets accepted quickly. Whether that becomes a problem depends on how long the application lives and how often it changes. For a short-lived prototype, debt can be the right choice.

    Can an AI assistant pay down the debt itself?

    Partly. Assistants can merge duplicates, introduce layers and write tests, if the task is clearly described. Without tests, though, there is no way to check whether behaviour got lost during a rebuild, and an assistant can create new duplicates in the process. The sensible order is therefore: tests for the critical rules first, then the rebuild with review.

    How can management tell that technical debt is the problem?

    By recurring patterns rather than individual bugs: changes keep taking longer, fixed bugs come back, updates pile up, and only one person still dares touch certain parts. When such patterns show up together, the cause sits more in the structure of the code than with individual people. A quick self-test can help place it.

    What does it cost to pay down technical debt?

    A blanket answer would not be credible, because the effort depends on the application. The drivers can be named, though: the scope of data storage, the number of interfaces, the data's protection needs, the planned pace of change, and whether migrations and tests already exist. These points make it possible to compare estimates from different sources and set priorities.

    Is technical debt always bad?

    No. A debt taken on deliberately can be the right call, say to test an idea quickly or hit a deadline. What is a problem is debt nobody knows about, and debt in applications that keep growing. That is why it pays to name it, place it, and decide which debts to carry and which to pay down.

    Read on

    Sources

    1. 01 The Maintainability Gap: 2026 AI Code Quality Research GitClear, 2026 · gitclear.com
    2. 02 We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs (USENIX Security 2025) arXiv, 2025 · arxiv.org
    3. 03 III. Config The Twelve-Factor App, o. J. · 12factor.net
    4. 04 Database Migrations Supabase Docs, 2026 · supabase.com
    5. 05 npm outdated npm Docs, 2026 · docs.npmjs.com
    6. 06 npm audit npm Docs, 2026 · docs.npmjs.com

    Let us talk about your project

    Whether it is a prototype, an internal tool or an AI application: describe briefly what you are building or want to take into production.

    Arthur C. Clarke

    “Any sufficiently advanced technology is indistinguishable from magic.”