Skip to content

Should You Rewrite or Maintain AI-Generated Code?

Intersecting gravel roads with fog representing a decision crossroads

The Problem

I stared at my AI-generated codebase and made a classic mistake: I decided to rewrite it from scratch.

“After all,” I reasoned, “now I understand what it should do. I can build it properly this time.”

Six weeks later, I had a half-finished rewrite and a deep appreciation for why rewrites fail—even with AI assistance. A Reddit comment from u/Historical_Angle_123 captured exactly what happened to me: “Had to do a ‘month 3 rewrite’ on a project that I mostly wrote myself. Figured I’d easily crack it with AI. Boy was I wrong. Ended up doing 90% of the work myself. And that was with Opus 4.6.”

The rewrite vs maintain decision isn’t obvious. Here’s how to make it correctly.

When to Maintain (Incremental Improvement)

I should have maintained my codebase. Here’s why:

The Architecture Was Sound

My code worked. It had:

  • Clear separation between API, business logic, and database layers
  • Traceable data flow (even if messy in places)
  • No critical security vulnerabilities

The problems were localized—inconsistent naming, duplicated functions, missing tests. These are fixable without a rewrite.

Problems Were Localized

The issues existed in specific modules, not everywhere:

  • Authentication had two different patterns (fixable)
  • Error handling was inconsistent (fixable)
  • Some functions duplicated logic (fixable)

Refactoring one area didn’t cascade into breaking others.

Domain Knowledge Was Embedded

The existing code handled edge cases I had forgotten about:

  • That weird timeout handling for slow APIs
  • The special case for empty user preferences
  • The retry logic for third-party service failures

A rewrite would lose all this working functionality.

When to Rewrite (Start Fresh)

Sometimes a rewrite is the right choice. But rarely.

rewrite-criteria.txt
REWRITE IF:
Architecture is fundamentally broken
├── No separation of concerns
├── Data flow is impossible to trace
└── Security vulnerabilities are systemic
AND:
Codebase is small enough
├── Under 5,000 lines of code
├── Can be rebuilt in 2-4 weeks
└── Domain logic is simple to reimplement
AND:
Cost of understanding > cost of rebuilding
├── You spend more time debugging than adding features
├── Every change breaks something unexpected
└── No one understands how it works

If you can’t check all three boxes, don’t rewrite.

The Decision Matrix

I created this scoring system after my failed rewrite:

decision-matrix.md
# Rewrite Decision Matrix
## Score Each Question (1-5)
### Architecture Health
- [ ] Can you trace data flow from input to output? (1=no idea, 5=trivial)
- [ ] Is there separation of concerns? (1=everything mixed, 5=clear layers)
- [ ] Are there obvious security vulnerabilities? (1=major issues, 5=secure)
- [ ] Can you add a new feature without breaking existing ones? (1=risky, 5=safe)
### Maintainability
- [ ] Can you understand what the code does? (1=foreign language, 5=self-documenting)
- [ ] Are there tests? (1=none, 5=comprehensive)
- [ ] Is there documentation? (1=none, 5=complete)
- [ ] Are patterns consistent? (1=random, 5=standardized)
### Business Value
- [ ] How many active users? (1=none, 5=many)
- [ ] How critical is uptime? (1=doesn't matter, 5=million-dollar cost)
- [ ] How much business logic is encoded? (1=trivial, 5=complex)
## Scoring Guide
- Total < 20: Consider rewrite (with changed process)
- Total 20-30: Strangler pattern (incremental replacement)
- Total > 30: Maintain with active refactoring

My codebase scored 28. I should have used the strangler pattern.

The Strangler Pattern: The Middle Path

u/aginor82’s comment hit the real solution: “Building a good foundation is key, doing constant refactorings while building the app.”

Neither full rewrite nor passive maintenance. Active refactoring.

strangler-pattern-evolution.txt
Phase 1: New code alongside old
┌─────────────────────────────────────────────────────┐
│ OLD CODE (messy, AI-generated) │
│ ├── /api/users → handleUserOld() │
│ ├── /api/orders → handleOrdersOld() │
│ └── /api/products → handleProductsOld() │
│ │
│ NEW CODE (clean, properly structured) │
│ ├── /api/v2/users → handleUserNew() │
│ └── (nothing else yet) │
└─────────────────────────────────────────────────────┘
Phase 2: Gradual migration with feature flags
┌─────────────────────────────────────────────────────┐
│ Router: if (featureFlags.useNewUserHandler(user)) │
│ → handleUserNew() │
│ else │
│ → handleUserOld() │
│ │
│ 10% of users → new code │
│ 90% of users → old code │
└─────────────────────────────────────────────────────┘
Phase 3: Remove old code once migration complete
┌─────────────────────────────────────────────────────┐
│ All users on new code │
│ Old code deleted │
│ /api/users now points to handleUserNew() │
│ │
│ Repeat for orders, products, etc. │
└─────────────────────────────────────────────────────┘

This approach:

  • Preserves working functionality
  • Reduces risk of total failure
  • Allows learning from existing code
  • Can be done incrementally

Why Rewrites Fail Even with AI

I thought AI would make my rewrite easy. I was wrong.

AI Struggles with Greenfield Rewrites

When I asked AI to rebuild my app, it struggled because:

  1. No existing context to work from - AI didn’t know what the old code did
  2. Must make architectural decisions without knowing edge cases - It made different mistakes
  3. Cannot reference working code for behavioral validation - No way to verify correctness
  4. Lacks the “why” behind original decisions - Couldn’t understand tradeoffs

Hidden Costs of Rewrites

The working AI-generated code had things I forgot about:

  • Users with data that needed migration
  • Running processes that couldn’t stop
  • External integrations with undocumented quirks
  • Configured environments with hidden dependencies

Migration was more work than the rewrite itself.

What Active Refactoring Looks Like

Instead of rewriting, I should have improved incrementally:

before-and-after-refactor.js
// BEFORE: AI-generated, works but messy
async function handleUser(req, res) {
const user = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
if (user) {
const orders = await db.query(`SELECT * FROM orders WHERE userId = ${user.id}`);
const prefs = await db.query(`SELECT * FROM preferences WHERE userId = ${user.id}`);
res.json({ user, orders, prefs });
} else {
res.status(404).send('Not found');
}
}
// AFTER: Refactored incrementally (not rewritten)
// Each change made while fixing a related bug or adding a feature
/**
* User profile handler with orders and preferences
* Refactored: 2026-04-01 (added error handling, removed SQL injection)
* TODO: Add caching for preferences (issue #123)
*/
async function handleUser(req, res) {
try {
const userId = validateUserId(req.params.id); // Added validation
// Use parameterized queries (fixed SQL injection)
const user = await userRepository.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Parallel fetch for performance (optimized from sequential)
const [orders, preferences] = await Promise.all([
orderRepository.findByUserId(userId),
preferenceRepository.findByUserId(userId)
]);
res.json({ user, orders, preferences });
} catch (error) {
logger.error('handleUser failed', { error: error.message, userId: req.params.id });
res.status(500).json({ error: 'Internal server error' });
}
}

The “after” version isn’t a rewrite. It’s the original function improved through:

  1. Adding input validation (when fixing a bug)
  2. Fixing SQL injection (when adding a security feature)
  3. Converting to parallel fetch (when optimizing performance)
  4. Adding error handling (when debugging an issue)

Each improvement happened as I touched the code for other reasons.

Common Mistakes

Mistake 1: Rewriting to “Do It Right This Time”

The most common mistake: assuming a rewrite automatically produces better code.

Without changing your process:

  • No tests → new code still has no tests
  • No documentation → new code still undocumented
  • Inconsistent patterns → new code still inconsistent

The rewrite itself doesn’t fix anything. Changing your process does.

Mistake 2: Underestimating Migration Complexity

My working code had:

  • Users with data
  • Running processes
  • External integrations
  • Configured environments

Migration was more work than the rewrite.

Mistake 3: The “Clean Slate” Illusion

A clean slate feels liberating. But:

  • You’ll rediscover the same edge cases
  • You’ll make the same architectural mistakes
  • You’ll have the same testing gaps

Unless you’ve fundamentally changed your process, a clean slate just resets the clock to month 1.

The Real Solution

u/DustInFeel’s comment captures traditional wisdom: “Throw away the proof of concept—like almost every developer does at some point. And then build the damn thing yourself.”

But the key phrase is “after you understand every single function in your codebase.”

The real solution isn’t rewrite OR maintain. It’s:

  1. Document every architectural decision
  2. Refactor incrementally while building
  3. Add tests for every bug you fix
  4. Consolidate patterns as you learn them

This is harder than a rewrite (requires understanding) but more sustainable than passive maintenance.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments