Managing Technical Debt from AI Code

12 minpublished

Prevent and address technical debt that can accumulate from uncritical acceptance of AI-generated code.

Managing Technical Debt from AI Code

AI coding assistants are incredibly powerful, but they come with a hidden cost: they can accelerate technical debt accumulation faster than traditional coding. In this lesson, we'll explore how AI-generated code creates specific types of technical debt and, more importantly, how to manage it effectively.

Understanding AI-Specific Technical Debt

Technical debt from AI code isn't fundamentally different from traditional technical debt, but it accumulates in unique patterns. AI assistants excel at generating code that works but may not align with your architecture, standards, or long-term maintainability goals.

The core issue? AI tools optimize for immediate functionality, not sustainable design. They don't understand your codebase's evolution, your team's conventions, or the architectural decisions made six months ago.

The Speed-Quality Paradox

When you generate code 5-10x faster with AI, you also make architectural decisions 5-10x faster. Not all of those decisions will be good ones. This creates a paradox where increased velocity can lead to decreased quality if you're not intentional about your process.

# AI might generate this quickly:
def process_user_data(user_id):
    # Direct database access
    conn = psycopg2.connect("dbname=mydb user=postgres")
    cursor = conn.cursor()
    cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
    user = cursor.fetchone()
    
    # Inline business logic
    if user[4] == 'premium':
        discount = 0.2
    else:
        discount = 0.1
    
    # Mixed concerns
    send_email(user[2], f"Your discount is {discount * 100}%")
    return user

# What you actually want:
class UserService:
    def __init__(self, db_repository, notification_service):
        self.db = db_repository
        self.notifications = notification_service
    
    def get_user_discount(self, user_id: int) -> float:
        user = self.db.get_user_by_id(user_id)
        return self._calculate_discount(user)
    
    def _calculate_discount(self, user: User) -> float:
        return 0.2 if user.is_premium else 0.1

The first version "works," but it violates separation of concerns, has SQL injection vulnerabilities, and would be painful to test or modify.

Common Patterns of AI-Generated Technical Debt

1. Over-Abstraction Without Justification

AI assistants love creating abstractions. Sometimes these are helpful, but often they're premature or unnecessary.

// AI-generated over-abstraction
class DataTransformer {
  constructor(strategy) {
    this.strategy = strategy;
  }
  
  transform(data) {
    return this.strategy.execute(data);
  }
}

class UppercaseStrategy {
  execute(data) {
    return data.toUpperCase();
  }
}

// Usage for simple string transformation
const transformer = new DataTransformer(new UppercaseStrategy());
const result = transformer.transform("hello");

// What you actually need:
const result = "hello".toUpperCase();

Red flag: When the abstraction has more lines than the actual logic it encapsulates.

2. Inconsistent Error Handling

AI often generates error handling that looks good in isolation but creates inconsistency across your codebase.

// Function 1 - AI generates this style
func GetUser(id int) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        log.Println("Error getting user:", err)
        return nil, err
    }
    return user, nil
}

// Function 2 - AI generates different style in another session
func GetOrder(id int) (*Order, error) {
    order, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("failed to get order %d: %w", id, err)
    }
    return order, nil
}

// Function 3 - Yet another pattern
func GetProduct(id int) (*Product, error) {
    product, err := db.Query(id)
    if err != nil {
        panic(err) // Definitely wrong!
    }
    return product, nil
}

Each AI session might produce different error handling patterns, creating maintenance nightmares.

3. Copy-Paste Syndrome at Scale

AI can replicate patterns across your codebase quickly—including bad patterns.

// Original function (with a subtle bug)
async function fetchUserOrders(userId: string) {
  const orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [userId]);
  // Bug: doesn't handle null/undefined userId
  return orders.map(o => ({
    ...o,
    total: o.total / 100 // Convert cents to dollars
  }));
}

// AI replicates the pattern (and the bug) everywhere
async function fetchUserInvoices(userId: string) {
  const invoices = await db.query('SELECT * FROM invoices WHERE user_id = ?', [userId]);
  // Same bug propagated
  return invoices.map(i => ({
    ...i,
    total: i.total / 100
  }));
}

async function fetchUserSubscriptions(userId: string) {
  const subs = await db.query('SELECT * FROM subscriptions WHERE user_id = ?', [userId]);
  // And again...
  return subs.map(s => ({
    ...s,
    total: s.total / 100
  }));
}

Now you have the same bug in three places. When discovered, you have to fix it three times.

Proactive Debt Prevention Strategies

Strategy 1: Establish AI Guardrails

Create a prompting framework that enforces your standards. This is explored in depth in our managing-tech-debt lesson.

## Code Generation Rules

When generating code, always:
1. Use our UserRepository class for database access
2. Implement error handling using our AppError classes
3. Include input validation using our validator library
4. Follow the Service-Repository-Controller pattern
5. Write functions no longer than 20 lines

Example:
[paste example from your codebase]

Keep this in a file and include it in your AI prompts or IDE configuration.

Strategy 2: The Two-Pass Review System

Never merge AI-generated code after a single review. Use two distinct passes:

Pass 1: Functionality Review

  • Does it solve the problem?
  • Are there obvious bugs?
  • Does it handle edge cases?

Pass 2: Architecture Review (24 hours later)

  • Does it fit our architecture?
  • Is it consistent with existing code?
  • Will we regret this in 6 months?

The 24-hour gap is crucial. It breaks the "it works, ship it!" momentum and gives you fresh eyes.

Strategy 3: Automated Consistency Checks

Use linting and static analysis tools configured for your standards, not generic rules.

# .eslintrc.yml - Custom rules for AI code review
rules:
  # Prevent AI from creating too-small functions
  max-lines-per-function: [error, { max: 50, min: 3 }]
  
  # Enforce consistent import patterns
  no-restricted-imports:
    - error
    - patterns:
      - message: "Use @/lib/* for shared utilities"
        pattern: "../../../lib/*"
  
  # Prevent inline SQL
  no-restricted-syntax:
    - error
    - selector: "TemplateLiteral[expressions.length>0] > TemplateElement[value.raw=/SELECT|INSERT|UPDATE|DELETE/i]"
      message: "Use repository classes instead of inline SQL"

This catches AI deviations immediately, before they reach code review.

Reactive Debt Management

Even with prevention, AI will generate technical debt. Here's how to manage it.

Debt Tagging System

Mark AI-generated debt explicitly in your code:

def calculate_shipping(items, address):
    # AI_DEBT: This uses hardcoded rates. Should integrate with ShippingService
    # Priority: Medium | Created: 2024-01-15 | Ticket: SHIP-123
    total = 0
    for item in items:
        if address.country == 'US':
            total += 5.99
        else:
            total += 12.99
    return total

This creates visibility and accountability. You can even write scripts to track these tags:

# Count AI debt by priority
grep -r "AI_DEBT" --include="*.py" | grep "Priority: High" | wc -l

The Refactor Budget

Allocate 20% of your development time to refactoring AI-generated code. This isn't waste—it's investment.

Track this using a simple metric:

Refactor Ratio = (Lines refactored) / (Lines AI-generated)

Aim for at least 0.15 (refactoring 15% of AI code). If it drops below 0.10, you're accumulating debt too fast.

Pattern Migration Scripts

When you identify a bad pattern that AI has replicated, write migration scripts:

// migrate-error-handling.js
const fs = require('fs');
const glob = require('glob');

// Find all files with old error pattern
const files = glob.sync('src/**/*.js');

files.forEach(file => {
  let content = fs.readFileSync(file, 'utf8');
  
  // Replace old pattern with new
  const updated = content.replace(
    /catch\s*\(err\)\s*{\s*console\.log\(/g,
    'catch (err) {\n    logger.error('
  );
  
  if (updated !== content) {
    fs.writeFileSync(file, updated);
    console.log(`Updated ${file}`);
  }
});

This is faster than manual refactoring and ensures consistency.

Security Debt: The Hidden Danger

AI-generated security vulnerabilities are particularly insidious because the code looks professional. As covered in our when-not-to-use-ai lesson, security-critical code requires extra scrutiny.

Common AI Security Debt

# AI generates seemingly reasonable auth code
def check_user_permission(user_id, resource_id):
    user = get_user(user_id)
    resource = get_resource(resource_id)
    
    # Looks fine, but has a critical flaw
    if user.role == 'admin':
        return True
    
    if resource.owner_id == user.id:
        return True
    
    # What about shared resources? Delegated permissions?
    # Time-based access? This is incomplete!
    return False

# Better approach - security logic should be explicit
def check_user_permission(user_id, resource_id, permission_type):
    # Use existing authorization service
    return AuthorizationService.check(
        user_id=user_id,
        resource_id=resource_id,
        permission=permission_type,
        # Handles all edge cases, roles, delegations, etc.
    )

Golden rule: Never let AI write authorization logic from scratch. Always integrate with your existing security framework.

Measuring and Tracking Technical Debt

You can't manage what you don't measure. Here are practical metrics for AI-generated debt:

1. Code Churn Rate

Track how often AI-generated code gets modified:

-- Query your git history
SELECT 
  file_path,
  COUNT(*) as modification_count,
  MAX(commit_date) as last_modified
FROM commits
WHERE commit_message LIKE '%AI-generated%' 
  OR author = 'copilot'
GROUP BY file_path
HAVING COUNT(*) > 3
ORDER BY modification_count DESC;

High churn indicates the AI didn't get it right the first time.

2. Test Coverage Gaps

AI often generates code without comprehensive tests:

# Compare test coverage for AI vs human code
git log --all --pretty=format: --name-only --diff-filter=A \
  | sort -u \
  | xargs -I {} sh -c 'git log -1 --format="%an" -- {} | grep -q "copilot" && echo {}'
  | xargs coverage report --include=

3. Integration Debt Score

How well does AI code integrate with your existing architecture?

Integration Score = (
  (Reused components / Total components) * 0.4 +
  (Follows patterns / Total patterns) * 0.4 +
  (Uses shared utilities / Total utilities) * 0.2
)

Scores below 0.6 indicate the AI is creating isolated islands of code.

The Refactoring Priority Matrix

Not all technical debt is equal. Prioritize using this framework:

Priority = (Usage Frequency * Maintenance Cost * Risk Factor)

- Usage Frequency: 1-10 (how often the code runs)
- Maintenance Cost: 1-10 (how hard to understand/modify)
- Risk Factor: 1-10 (security/correctness criticality)

Focus on high scores first:

  • Score > 500: Refactor this sprint
  • Score 200-500: Schedule for next month
  • Score < 200: Document and defer

Conclusion: Making AI Technical Debt Manageable

AI coding assistants will generate technical debt—this is inevitable when you're moving fast. The key is making that debt intentional and managed rather than accidental and forgotten.

Remember these principles:

  1. Prevention is cheaper than remediation: Set up guardrails before they're needed
  2. Visibility enables action: Tag, track, and measure AI-generated debt
  3. Budget for refactoring: Allocate time to pay down debt continuously
  4. Security is non-negotiable: Never compromise on security for velocity

The goal isn't to eliminate technical debt from AI code—it's to ensure the debt you take on has a reasonable interest rate and a clear payoff plan.

For more on avoiding common pitfalls with AI coding tools, check out our top-mistakes and over-reliance lessons. These complement this lesson by showing you what not to do when working with AI assistants.

Now, take a look at your most recent AI-generated code. Can you identify technical debt using the patterns we've discussed? Start there, and remember: every piece of debt you pay down today is compounding interest you won't pay tomorrow.