On this page
How to Work With an AI Coding Assistant Without Creating a Mountain of Tech Debt
AI assistants generate code fast — and generate tech debt faster. Duplicated logic, dead code, over-engineered abstractions. Here's the system for catching these before they accumulate: review, deduplicate, simplify, delete.
Quick answer
- AI assistants generate code fast but don’t consider your existing codebase. The result: duplicated logic, dead code, and over-engineered solutions.
- The system: after every AI-generated feature, check for duplication, dead code, and unnecessary complexity — before committing.
- AI-generated tech debt is worse than hand-written tech debt because you don’t understand it. Catch it immediately or it’s permanent.
The problem: AI writes code fast, debt accumulates faster
AI coding assistants have one speed: full throttle. They generate 200 lines for a feature that needed 50. They duplicate a helper function because they can’t see it already exists. They leave dead code from “cleanup” passes that removed the caller but not the function.
And you don’t understand any of it. AI-generated code you’ve reviewed is debt you can manage. AI-generated code you haven’t reviewed is debt that owns you.
The three types of AI tech debt
1. Duplication: the same logic in multiple places
# AI generated in users.py:
def validate_email(email):
return "@" in email and "." in email.split("@")[1]
# AI generated in orders.py (didn't know users.py exists):
def check_email_format(email):
if "@" not in email:
return False
if "." not in email.split("@")[1]:
return False
return TrueTwo implementations of email validation. Different function names, slightly different logic, same purpose. When the validation rule changes, one gets updated and the other doesn’t — a bug waiting to happen.
For a detailed guide, see: How to Stop AI Assistants from Duplicating Code.
2. Dead code: functions with no callers
# AI generated this during a "refactor" pass:
def calculate_discount_legacy(order, rate):
return order.total * rate
# The caller was removed in the next commit. This function lives on forever.Dead code is dangerous because the AI reads it as context. It sees calculate_discount_legacy, assumes it’s important, and generates new code that depends on it — creating a dependency on dead code.
For a full guide, see: How to Remove Dead Code Your AI Left Behind.
3. Over-engineering: complexity the feature doesn’t need
# AI generated for "add user registration":
class UserRegistrationFactory:
def __init__(self, validator, hasher, mailer, logger):
self.validator = validator
self.hasher = hasher
self.mailer = mailer
self.logger = logger
def create_user(self, email, password):
self.validator.validate(email, password)
hashed = self.hasher.hash(password)
user = User(email=email, password_hash=hashed)
self.mailer.send_welcome(user)
self.logger.info(f"user created: {user.id}")
return user
# Needed: one function with 10 lines. Generated: factory pattern with 4 injected dependencies.The AI defaults to patterns it saw in production codebases — patterns designed for scale you don’t have. For a guide on simplifying this, see: How to Simplify Overly Complex AI-Generated Functions.
The review system: catch debt before it’s committed
After every AI-generated feature, before committing, check:
Check 1: Does this exist elsewhere?
# Search for similar function names
grep -r "def validate_email" src/
grep -r "def check_email" src/
grep -r "def.*email.*valid" src/
# Found a duplicate? Extract the shared version and delete the copy.Check 2: Is this code actually used?
# Find all callers of a function
grep -r "calculate_discount_legacy" src/
# No results? Delete it. If tests reference it, those are dead tests — delete them too.Most IDEs can show you callers directly (Ctrl+Click or F12 on the function name). Use it. If nothing calls it, it’s dead.
Check 3: Is this simpler than it looks?
Read the AI’s code and ask: “What does this actually do?” If the answer is simple but the code is complex, simplify it.
# AI generated (complex):
def is_user_allowed(user, resource):
if user.role == "admin":
return True
elif user.role == "manager":
if resource.owner_id == user.id or resource.department == user.department:
return True
elif user.role == "member":
if resource.owner_id == user.id:
return True
return False
# Simplify (same logic, half the lines):
def is_user_allowed(user, resource):
if user.role == "admin":
return True
if user.role == "manager" and resource.department == user.department:
return True
return resource.owner_id == user.idThe AI generates correct but verbose code — it’s trained on code where verbosity was confused with thoroughness.
The preventive measures
Write a .cursorrules or .aidigest file
Give the AI standing instructions about your codebase:
# .aidigest — standing instructions for AI assistants
- Don't duplicate: before writing a function, check if it already exists.
- Don't over-engineer: use the simplest pattern that works. No factory classes
for single-implementation interfaces. No abstract base classes unless there
are at least 2 implementations.
- Delete dead code: when modifying a function, check its callers. If you're
removing the last caller, delete the function.
- Keep functions under 30 lines. If a function grows beyond that, it's doing
too much — suggest splitting it.Review before committing, always
The only reliable defense against AI tech debt is human review. You don’t need to understand every line — but you do need to:
- Read function names — do any look like duplicates?
- Check for functions with no callers
- If the code looks more complex than the problem, it is
The cleanup cadence
Don’t try to fix all debt at once. Do a focused cleanup pass weekly:
- 5 minutes: grep for duplicate function names
- 5 minutes: check for dead code (functions with no callers)
- 10 minutes: simplify the most complex function you touched that week
Over a month, this removes the majority of AI-generated debt before it calcifies.
Where this bites vibecoders
The AI generates 200 lines for a feature. The vibecoder sees it work, commits it. Repeat 20 times. Now there’s 4,000 lines of AI code, ~1,200 of which is duplication or dead code. The AI’s context window is filled with noise, so every new feature generates worse code. The fix is the 3-check system applied to every commit: duplicate? dead? over-complex? Three checks, 60 seconds, prevents the spiral.
Checklist
- After every AI-generated feature: check for duplication, dead code, over-complexity
- Use grep or IDE to find callers before deleting or modifying functions
- Extract shared logic immediately — don’t wait for the “refactor later” that never comes
- Write standing instructions (cursorrules/aidigest) for your AI assistant
- Weekly 20-minute cleanup pass on the codebase
FAQ
How fast does AI-generated tech debt accumulate?
Fast. One feature request = 200 lines of AI code. Five features = 1,000 lines. Without review, ~30% of that is duplication or dead code. After 20 features, you have ~600 lines of unnecessary code that the AI now reads as context for every new feature — making every subsequent feature harder to generate correctly.
Can’t I just ask the AI to not duplicate code?
You can, and it helps, but it’s not reliable. The AI’s context window can’t see your entire codebase, so it doesn’t know a function already exists in another file. The fix isn’t a better prompt — it’s catching duplication in review and extracting it.
What’s the difference between AI tech debt and regular tech debt?
Regular tech debt is a choice you made knowingly (“we’ll refactor after launch”). AI tech debt is accidental — code you didn’t know was generated, patterns you didn’t ask for, abstractions you don’t understand. It’s harder to fix because you didn’t write it and don’t know why it exists.
Related topics
- What Is Technical Debt?
- How to Stop AI Assistants from Duplicating Code
- How to Remove Dead Code Your AI Left Behind
- How to Simplify Overly Complex AI-Generated Functions
- What Is Refactoring (and How Do You Do It Without Breaking Everything)?
- How to Review AI-Generated Code Like a Senior Engineer