The Byte-sized logo The Byte-sized Indie product studio
Back to Insights
#technical debt #development #quality

Why Every Fix Breaks Something Else: Debug Framework + Root Cause Analysis

Fixing one bug and watching two new ones appear is not bad luck—it's a structural problem. This article explains why AI-built codebases fall into cascading fix loops, and gives you a five-step framework to stop patching symptoms and start solving root causes.

- MVP Journey →
Why Every Fix Breaks Something Else: Debug Framework + Root Cause Analysis

You fix one bug. Two new ones appear.

This is the most frustrating pattern in AI-built MVPs—and it’s not random.

It happens because:

  • Business rules are scattered (validation in 5 places)
  • Logic is duplicated (same code copy-pasted)
  • Dependencies are unclear (change A breaks B, no one knows why)

This article explains why it happens and how to stop it.

For broader context:


Why Fixes Break Things: The 3 Root Causes

1. Scattered Business Rules

Example: Payment validation logic.

In a healthy codebase:

  • One place: rules/payments.ts → all payment logic here.

In AI-built MVP:

  • Five places: checkout page, payment API, webhook handler, admin panel, email template.
  • Fix validation in checkout → breaks webhook (different rule).

Result: Fix one bug, create regression.


2. Duplicated Logic

Example: User role checks.

Healthy:

// utils/permissions.ts
export function canEditProject(user, project) {
  return user.role === 'admin' || user.id === project.owner_id;
}

AI-built MVP:

// 5 different files, 5 slightly different checks
if (user.role === 'admin' || user.id === project.owner) { ... }
if (user.role === 'admin' || user.userId === project.ownerId) { ... }
if (user.isAdmin || user.id === project.owner_id) { ... }

Result: Fix one check → 4 others still broken.


3. Unclear Dependencies

Example: Order state machine.

Healthy (explicit dependencies):

  • Order created → Email sent → Inventory updated → Payment charged.
  • Each step documented, tested.

AI-built MVP (implicit dependencies):

  • Order created → ???
  • Sometimes email sent before payment (bug: email says “paid” but payment fails).
  • Sometimes inventory updated before payment (bug: item reserved but payment declined).

Result: Fix email timing → breaks inventory logic.


Real Example: The Checkout Bug Loop

Week 1: User reports: “Checkout button doesn’t work.”

Fix: Add onClick handler. Deploy.

Week 2: User reports: “Checkout charges twice.”

Fix: Add idempotency key. Deploy.

Week 3: User reports: “Order confirmation email not sent.”

Fix: Add email trigger after payment. Deploy.

Week 4: User reports: “Email sent even if payment fails.”

Fix: Move email trigger after payment success check. Deploy.

Week 5: User reports: “Inventory not updated.”

Fix: Add inventory update after payment. Deploy.

Week 6: User reports: “Inventory updated even if order canceled.”

Result: 6 weeks, 6 bugs, each fix creates new regression. Original issue (checkout button) still fragile.

Root cause: No centralized order workflow. Each fix = patch on patch.


5-Step Debugging Framework (Stop the Loop)

Step 1: Isolate the Bug

Don’t: Prompt AI: “Fix checkout bug.”

Do: Reproduce bug manually. Document exact steps.

Example:

  1. Login as user X.
  2. Add item Y to cart.
  3. Click checkout.
  4. Enter card 4242 4242 4242 4242.
  5. Click “Pay.”
  6. Observe: Charged twice.

Output: Reproducible test case.


Step 2: Test First (Before Fixing)

Write test that fails:

test('checkout charges once', async () => {
  const order = await createOrder({ userId: 1, itemId: 5 });
  const charges = await getCharges(order.id);
  expect(charges.length).toBe(1); // Fails (currently 2)
});

Why: Test ensures fix actually works (not just “looks fixed”).


Step 3: Centralize Logic

Before (scattered):

  • Checkout page: calls Stripe API.
  • Webhook handler: also calls Stripe API (different params).
  • Admin refund: also calls Stripe API (third variation).

After (centralized):

// services/payments.ts
export async function chargeCustomer(userId, amount, orderId) {
  // Single source of truth for payment logic
  const idempotencyKey = `order_${orderId}`;
  return stripe.charges.create({ amount, idempotencyKey });
}

Result: Fix once, applies everywhere.


Step 4: Document Dependencies

Create order workflow doc:

Order State Machine:
1. Created → send email ("order received")
2. Payment charged → update inventory
3. Payment success → send email ("order confirmed")
4. Payment failed → send email ("payment issue") + release inventory
5. Order canceled → refund + release inventory

Why: Explicit dependencies prevent “fix A breaks B” surprises.


Step 5: Prevent (Not Just Fix)

Add guard rails:

  • Linting: ESLint rule: “No direct Stripe calls outside services/payments.ts.”
  • Tests: Every fix = new test (prevent regression).
  • Code review: Human checks AI-generated fixes (don’t merge blindly).

Result: Future bugs caught before production.


Cost of Broken Fixes vs Prevention

ApproachTimelineCostOutcome
Patch-on-patch (no framework)6-12 weeks firefighting€3k-€8k (100-200h debugging)Velocity drops, bugs multiply
5-step framework (prevention)1-2 weeks setup€1k-€2k (15-30h one-time)Bugs decrease, velocity stable
Rescue later (rebuild parts)8-12 weeks€8k-€15k (rebuild checkout flow)Clean slate, but lost time

Insight: Prevention (€1k-€2k upfront) saves €2k-€13k vs patch-on-patch or rescue.


How AI Makes This Worse

AI optimizes for:

  • Fast output (generate code quickly).
  • Happy path (user does everything right).

AI does NOT optimize for:

  • Centralized logic (each prompt = isolated solution).
  • Edge cases (“what if payment fails mid-process?”).
  • Dependencies (AI doesn’t know your full codebase).

Example: Prompt: “Add email after payment.”

AI generates:

await stripe.charges.create(...);
await sendEmail('order_confirmed', user.email); // Bug: no error handling

Problem: If email fails, user charged but no confirmation. AI doesn’t know to:

  1. Check payment success first.
  2. Handle email failure gracefully.
  3. Log for debugging.

The fix is not to stop using AI—it’s to use AI as an accelerator with human guardrails, not as an autopilot.


When to Centralize Logic

Trigger: When you fix same bug 2+ times.

Signs you need centralization:

  • Same validation logic in 3+ files.
  • Copy-paste code (with small variations).
  • “I fixed this last week, why is it broken again?”

Action: Stop adding features. Spend 1-2 weeks refactoring.

Cost: €1k-€2k (15-30h).

Payoff: Velocity increases (fixes work first try). The prototype-to-product hardening checklist gives you the full priority matrix for this kind of stabilization work.


Conclusion: Fix the System, Not Just the Bug

Every fix breaks something when the system is fragile.

The solution is not “be more careful.” It’s:

  1. Isolate bugs (reproduce reliably).
  2. Test first (write failing test).
  3. Centralize logic (one source of truth).
  4. Document dependencies (explicit workflows).
  5. Prevent future bugs (linting, tests, review).

Remember: Patch-on-patch costs €3k-€8k + 6-12 weeks. Prevention costs €1k-€2k + 1-2 weeks (saves €2k-€6k).


Next reads