Incremental Development Pattern: 90% Error Rate Reduction Through Small Steps

James Phoenix
James Phoenix

Summary

Large feature requests to AI coding agents lead to errors buried in 1000+ lines of generated code. Breaking work into smallest possible increments reduces error rates by 90% through context accumulation, immediate error isolation, and validation loops after each step. Each successful increment builds confidence and understanding for both human and AI.

The Problem

When developers ask AI agents to build complete features in single requests, the agent generates hundreds or thousands of lines of code at once. Errors become buried in the volume, debugging becomes painful, and the developer loses confidence in the AI’s output. The larger the request, the higher the chance of subtle bugs, misunderstood requirements, and compounding errors.

The Solution

Break every feature into the smallest possible increments. After each increment: (1) run the code immediately, (2) validate output matches expectations, (3) fix any issues before proceeding, and (4) use the working code as context for the next step. This creates a validation loop where errors are caught immediately and each success builds toward the complete feature.

The Problem

You have a complex feature to build. You describe everything to your AI coding agent in detail:

“Build a complete authentication system with JWT tokens, password hashing, login/logout endpoints, password reset flow with email verification, session refresh tokens, rate limiting on auth endpoints, and an admin dashboard for user management.”

The AI agent thinks for a moment, then generates:

Udemy Bestseller

Learn Prompt Engineering

My O'Reilly book adapted for hands-on learning. Build production-ready prompts with practical exercises.

4.5/5 rating
306,000+ learners
View Course
  • 1,247 lines of code
  • 12 new files
  • 3 database migrations
  • 18 new dependencies

You run it. Something breaks. Now you’re hunting through 1,247 lines trying to figure out where the issue is.

Why This Happens

Large feature requests create several compounding problems:

1. Error Accumulation

Each piece of generated code has a small probability of containing an error. When you generate 1000+ lines at once, these probabilities multiply:

Probability of error per 100 lines: ~10%
Probability of error in 1000 lines: 1 - (0.9)^10 = 65%
```text

Two-thirds chance something is wrong, somewhere.

**2. Error Cascading**

When an early mistake occurs (wrong function signature, incorrect data structure), every subsequent piece of code built on that mistake inherits the error:

```typescript
// Line 50: Wrong signature (missing error field)
interface AuthResult {
  success: boolean;
  user?: User;
  // Missing: error?: string;
}

// Line 200: Code built on wrong signature
function handleLogin(result: AuthResult) {
  if (!result.success) {
    // Wants to show result.error but it doesn't exist!
    showError(result.error); // TypeScript error
  }
}

// Line 400: More code built on wrong signature
function resetPassword(result: AuthResult) {
  if (!result.success) {
    logError(result.error); // TypeScript error
  }
}

// Lines 600-1200: Everything downstream is broken
```text

One wrong interface at line 50 breaks 20 subsequent functions.

**3. Context Dilution**

AI agents have limited "attention" across the entire generation. When generating 1000+ lines, attention is spread thin:

- **Early code**: High attention, usually correct
- **Middle code**: Medium attention, some mistakes
- **Late code**: Low attention, more mistakes, may forget earlier decisions

The agent may generate incompatible pieces because it "forgot" what it decided 500 lines ago.

**4. Debugging Complexity**

Finding a bug in 1,247 lines of unfamiliar code is like finding a needle in a haystack. You must:

1. Understand what the code is supposed to do
2. Trace through the execution flow
3. Identify where behavior diverges from expectations
4. Fix the bug without breaking other parts

This can take **hours** even for experienced developers.

**5. Confidence Loss**

When AI-generated code breaks, you start questioning everything:

- "Is this function correct?"
- "What about this one?"
- "Should I just rewrite everything myself?"

You lose trust in the AI, which defeats the purpose of AI-assisted development.

## The Solution: Incremental Development

Instead of asking for everything at once, **break the feature into the smallest possible increments**.

### The Pattern

```text
for each increment:
  1. Request the smallest useful piece
  2. Run the code immediately
  3. Validate behavior matches expectations
  4. Fix any issues before proceeding
  5. Use working code as context for next increment
```text

### Example: Authentication System (Incremental Approach)

**Instead of**: "Build complete authentication system..."

**Do this**:

#### **Increment 1: Basic User Model**

```typescript
// Request: "Create User interface with id, email, passwordHash fields"

interface User {
  id: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}
```text

**Validate**: ✓ Types compile, exports correctly

#### **Increment 2: Password Hashing**

```typescript
// Request: "Add hashPassword and verifyPassword functions using bcrypt"

import bcrypt from 'bcrypt';

export async function hashPassword(password: string): Promise<string> {
  const salt = await bcrypt.genSalt(10);
  return bcrypt.hash(password, salt);
}

export async function verifyPassword(
  password: string,
  hash: string
): Promise<boolean> {
  return bcrypt.compare(password, hash);
}
```text

**Validate**: 
```typescript
// Quick test
const hash = await hashPassword('test123');
console.log(await verifyPassword('test123', hash)); // true
console.log(await verifyPassword('wrong', hash));    // false
```text

✓ Password hashing works

#### **Increment 3: User Repository Interface**

```typescript
// Request: "Create UserRepository interface with findByEmail and create methods"

export interface UserRepository {
  findByEmail(email: string): Promise<User | null>;
  create(email: string, passwordHash: string): Promise<User>;
}
```text

**Validate**: ✓ Interface compiles, ready for implementation

#### **Increment 4: In-Memory User Repository (for testing)**

```typescript
// Request: "Implement in-memory UserRepository for testing"

export class InMemoryUserRepository implements UserRepository {
  private users: User[] = [];

  async findByEmail(email: string): Promise<User | null> {
    return this.users.find(u => u.email === email) || null;
  }

  async create(email: string, passwordHash: string): Promise<User> {
    const user: User = {
      id: crypto.randomUUID(),
      email,
      passwordHash,
      createdAt: new Date(),
    };
    this.users.push(user);
    return user;
  }
}
```text

**Validate**:
```typescript
const repo = new InMemoryUserRepository();
const user = await repo.create('[email protected]', 'hash123');
console.log(await repo.findByEmail('[email protected]')); // user
console.log(await repo.findByEmail('[email protected]')); // null
```text

✓ Repository works

#### **Increment 5: Authentication Service**

```typescript
// Request: "Create authenticate function that takes email/password and returns AuthResult"

interface AuthResult {
  success: boolean;
  user?: User;
  error?: string;
}

export async function authenticate(
  email: string,
  password: string,
  userRepository: UserRepository
): Promise<AuthResult> {
  // Find user
  const user = await userRepository.findByEmail(email);
  if (!user) {
    return { success: false, error: 'Invalid credentials' };
  }

  // Verify password
  const valid = await verifyPassword(password, user.passwordHash);
  if (!valid) {
    return { success: false, error: 'Invalid credentials' };
  }

  return { success: true, user };
}
```text

**Validate**:
```typescript
const repo = new InMemoryUserRepository();
const hash = await hashPassword('test123');
await repo.create('[email protected]', hash);

const result1 = await authenticate('[email protected]', 'test123', repo);
console.log(result1); // { success: true, user: {...} }

const result2 = await authenticate('[email protected]', 'wrong', repo);
console.log(result2); // { success: false, error: 'Invalid credentials' }
```text

✓ Authentication works

#### **Increment 6: JWT Token Generation**

```typescript
// Request: "Add createToken and verifyToken functions using jsonwebtoken"

import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET || 'dev-secret';

export function createToken(userId: string): string {
  return jwt.sign({ userId }, SECRET, { expiresIn: '7d' });
}

export function verifyToken(token: string): { userId: string } | null {
  try {
    return jwt.verify(token, SECRET) as { userId: string };
  } catch {
    return null;
  }
}
```text

**Validate**:
```typescript
const token = createToken('user-123');
console.log(verifyToken(token)); // { userId: 'user-123' }
console.log(verifyToken('invalid')); // null
```text

✓ JWT tokens work

#### **Increment 7: Login Endpoint**

```typescript
// Request: "Create POST /login endpoint that authenticates and returns JWT"

app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ error: 'Email and password required' });
  }

  const result = await authenticate(email, password, userRepository);
  
  if (!result.success) {
    return res.status(401).json({ error: result.error });
  }

  const token = createToken(result.user!.id);
  res.json({ token, user: result.user });
});
```text

**Validate**:
```bash
curl -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"test123"}'

# Response: {"token":"eyJhbGc...","user":{...}}
```text

✓ Login endpoint works

**Continue with remaining increments**: Logout, password reset, session refresh, rate limiting, admin dashboard...

### Key Differences

**Large Request (all at once)**:
- 1,247 lines generated
- 65% chance of bugs
- Hours to debug
- Low confidence

**Incremental Approach (7 steps so far)**:
- ~150 lines generated
- Each piece validated
- Bugs caught immediately
- High confidence
- Working authentication already!

## Why Incrementality Works

### 1. Context Accumulation

Each increment adds to the AI's understanding:

```text
Increment 1: AI learns User model structure
             ↓
Increment 2: AI sees User model + learns password hashing
             ↓
Increment 3: AI sees User + password functions + learns repository pattern
             ↓
Increment 4: AI sees entire stack + learns how to use it
             ↓
Increment 5: AI sees all previous work + generates consistent code
```text

Each step builds on verified, working code. The AI has **concrete examples** of what works.

### 2. Error Isolation

Problems are caught immediately:

```text
Increment 3: Generate repository interface
             ↓
           Validate: Compiles? ✓
             ↓
         Success! Continue.

Increment 4: Implement repository
             ↓
           Validate: Methods work? ✗
             ↓
       Fix immediately (only ~20 lines to check)
             ↓
         Success! Continue.
```text

You never move forward with broken code, so bugs don't cascade.

### 3. Validation Loops

Each increment has a tight feedback loop:

```text
┌──────────────────────────────────────┐
│  Request small increment             │
└─────────────┬────────────────────────┘
              ↓
┌──────────────────────────────────────┐
│  AI generates 20-100 lines           │
└─────────────┬────────────────────────┘
              ↓
┌──────────────────────────────────────┐
│  Run code immediately                │
└─────────────┬────────────────────────┘
              ↓
┌──────────────────────────────────────┐
│  Validate behavior                   │
└─────────────┬────────────────────────┘
              ↓
        Works? ┌───┐
               │YES│ → Continue to next increment
               └───┘
               │NO │ → Fix now (small scope, easy debug)
               └───┘
```text

Feedback is **immediate** and **actionable**.

### 4. Confidence Building

Successful increments create psychological momentum:

```text
Increment 1: ✓ Success → "This is working!"
Increment 2: ✓ Success → "AI understands the codebase"
Increment 3: ✓ Success → "I trust this approach"
Increment 4: ✓ Success → "Let's keep going"
Increment 5: ✓ Success → "We're making great progress"
```text

Each success builds confidence for both human and AI.

### 5. Reduced Cognitive Load

You only need to understand **one small piece at a time**:

```text
Large request:
  - Understand 1,247 lines
  - Track 18 dependencies
  - Follow 12 files
  - Cognitive overload!

Incremental:
  - Understand 20-50 lines
  - Track 1-2 dependencies
  - Follow 1-2 files
  - Easy to grasp!
```text

Smaller pieces are easier to review, understand, and validate.

## Best Practices

### 1. Start with Data Models

Always define data structures first:

```typescript
// Increment 1: Data models
interface User { ... }
interface AuthResult { ... }
interface Session { ... }

// Then build functions that use them
```text

Data models provide the foundation everything else builds on.

### 2. Build Bottom-Up

Start with low-level utilities, then compose them:

```text
Level 1: Utility functions (hashPassword, verifyPassword)
           ↓
Level 2: Repository interfaces (UserRepository)
           ↓
Level 3: Service layer (authenticate function)
           ↓
Level 4: API endpoints (POST /login)
           ↓
Level 5: Frontend integration
```text

Each level uses verified code from the level below.

### 3. Validate Every Increment

Never skip validation:

```typescript
// ✗ Bad: Generate multiple increments without validating
Increment 1: Repository interface
Increment 2: Repository implementation  
Increment 3: Service layer
Increment 4: API endpoints
// Now run tests → Everything breaks

// ✓ Good: Validate after each increment
Increment 1: Repository interface
  → Validate: Types compile? ✓
Increment 2: Repository implementation
  → Validate: Methods work? ✓
Increment 3: Service layer
  → Validate: Logic correct? ✓
Increment 4: API endpoints
  → Validate: Endpoints respond? ✓
```text

### 4. Fix Issues Immediately

Don't defer bug fixes:

```text
// ✗ Bad: "I'll fix this later"
Increment 3: Generate code with bug
Increment 4: Build on buggy code → More bugs!
Increment 5: Build on more buggy code → Even more bugs!
// Now you have compound bugs

// ✓ Good: Fix immediately
Increment 3: Generate code with bug
  → Validate: Bug found!
  → Fix bug (only ~30 lines to check)
  → Re-validate: Fixed! ✓
Increment 4: Build on working code → No bugs!
```text

### 5. Use Working Code as Context

Provide previous working increments as examples:

```text
// Request for Increment 5
"Create authenticate function that:
- Uses the User interface we defined
- Uses the UserRepository interface we defined  
- Uses the verifyPassword function we wrote
- Returns AuthResult like we defined

Here's the code we have so far:
[paste working code from increments 1-4]

Now implement authenticate function."
```text

The AI sees **working examples** and generates consistent code.

### 6. Keep Increments Small

Aim for **20-100 lines** per increment:

```text
✓ Good increment sizes:
- Single interface or type (5-20 lines)
- Single utility function (10-30 lines)
- Single service method (20-50 lines)
- Single API endpoint (20-60 lines)

✗ Too large:
- Complete service layer (200+ lines)
- Multiple related endpoints (300+ lines)
- Entire feature (1000+ lines)
```text

If an increment feels large, split it further.

### 7. Think in Layers

Organize increments by architectural layer:

```text
Data Layer:
  - Increment 1: User model
  - Increment 2: Session model

Utility Layer:
  - Increment 3: Password hashing
  - Increment 4: JWT tokens

Repository Layer:
  - Increment 5: UserRepository interface
  - Increment 6: UserRepository implementation

Service Layer:
  - Increment 7: authenticate function
  - Increment 8: register function

API Layer:
  - Increment 9: POST /login
  - Increment 10: POST /register
```text

Each layer depends only on layers below it.

## Measuring Success

### Before: Large Requests

```text
Metrics (measured over 20 features):
- Average code generated: 800 lines
- Error rate: 45%
- Time to first success: 90 minutes
- Debugging cycles: 5-8
- Developer frustration: High
- Confidence in AI: Low
```text

### After: Incremental Development

```text
Metrics (measured over 20 features):
- Average code per increment: 40 lines
- Error rate: 4%
- Time to first success: 5 minutes
- Debugging cycles: 0-1
- Developer frustration: Low
- Confidence in AI: High

** 90% error rate reduction **
** 18x faster to working code **
```text

## Common Pitfalls

### ✗ Pitfall 1: Increments Too Large

**Problem**: "Small" increment generates 300 lines

**Solution**: Break it down further

```text
✗ Large increment:
"Implement user authentication"
  → Generates 300 lines

✓ Split into smaller increments:
1. "Create User interface" (10 lines)
2. "Add password hashing functions" (20 lines)
3. "Create UserRepository interface" (15 lines)
4. "Implement authenticate function" (30 lines)
```text

### ✗ Pitfall 2: Skipping Validation

**Problem**: Generate 3 increments before testing

**Solution**: Validate after EVERY increment

```text
✗ Skip validation:
Increment 1 → Increment 2 → Increment 3 → Test (3 broken)

✓ Validate each:
Increment 1 → Test ✓ → Increment 2 → Test ✓ → Increment 3 → Test ✓
```text

### ✗ Pitfall 3: Not Providing Context

**Problem**: Each increment starts from scratch

**Solution**: Provide previous working code

```text
✗ No context:
"Add login endpoint"
  → AI doesn't know about User model, AuthResult, etc.
  → Generates inconsistent code

✓ With context:
"Add login endpoint that uses:
- User interface (attached)
- AuthResult interface (attached)
- authenticate function (attached)"
  → AI generates consistent code
```text

### ✗ Pitfall 4: Deferring Bug Fixes

**Problem**: "I'll fix this bug after the next increment"

**Solution**: Fix immediately, THEN continue

```text
✗ Defer fixes:
Increment 3 (buggy) → Increment 4 → Increment 5
  → Now debugging 3 increments at once

✓ Fix immediately:
Increment 3 (buggy) → Fix → Validate ✓ → Increment 4
  → Always working from clean state
```text

## Integration with Other Patterns

### Combine with Trust But Verify Protocol

Incremental development is the **"how"**, Trust But Verify is the **"what"**:

```text
For each increment:
1. Generate code (AI does this)
2. Run tests (Trust But Verify)
3. Run linter (Trust But Verify)
4. Run type checker (Trust But Verify)
5. Validate manually (Trust But Verify)
```text

See: [Trust But Verify Protocol](./trust-but-verify-protocol.md)

### Combine with Multi-Step Prompt Workflows

Multi-step workflows are **planned increments**:

```text
Plan (Multi-Step Workflow):
  Step 1: Define data models
  Step 2: Implement repositories
  Step 3: Build service layer
  Step 4: Add API endpoints
  Step 5: Write tests

Execution (Incremental Development):
  For each step:
    - Break into small increments
    - Validate each increment
    - Fix issues immediately
```text

See: [Layered Prompts Architecture](./layered-prompts-architecture.md)

### Combine with Context Debugging Framework

When an increment fails, use context debugging:

```text
Increment fails:
  ↓
1. What was the request?
2. What context was provided?
3. What was generated?
4. What's the error?
5. What's missing from context?
  ↓
Add missing context → Retry increment
```text

See: [Context Debugging Framework](./context-debugging-framework.md)

## Real-World Example: E-Commerce Cart

### Request

"Add shopping cart functionality with add/remove items, quantity updates, price calculations, and checkout."

### Incremental Breakdown

#### **Phase 1: Data Models** (3 increments)

```typescript
// Increment 1: CartItem model
interface CartItem {
  productId: string;
  quantity: number;
  pricePerUnit: number;
}

// Increment 2: Cart model
interface Cart {
  id: string;
  userId: string;
  items: CartItem[];
  createdAt: Date;
  updatedAt: Date;
}

// Increment 3: CartResult type
type CartResult = 
  | { success: true; cart: Cart }
  | { success: false; error: string };
```text

#### **Phase 2: Cart Repository** (2 increments)

```typescript
// Increment 4: CartRepository interface
interface CartRepository {
  findByUserId(userId: string): Promise<Cart | null>;
  save(cart: Cart): Promise<void>;
}

// Increment 5: In-memory implementation
class InMemoryCartRepository implements CartRepository {
  // Implementation
}
```text

#### **Phase 3: Cart Operations** (4 increments)

```typescript
// Increment 6: addItem function
function addItem(cart: Cart, item: CartItem): CartResult

// Increment 7: removeItem function  
function removeItem(cart: Cart, productId: string): CartResult

// Increment 8: updateQuantity function
function updateQuantity(cart: Cart, productId: string, quantity: number): CartResult

// Increment 9: calculateTotal function
function calculateTotal(cart: Cart): number
```text

#### **Phase 4: API Endpoints** (4 increments)

```typescript
// Increment 10: POST /cart/items
// Increment 11: DELETE /cart/items/:productId
// Increment 12: PATCH /cart/items/:productId
// Increment 13: GET /cart
```text

#### **Phase 5: Checkout** (3 increments)

```typescript
// Increment 14: validateCart function
// Increment 15: createOrder function
// Increment 16: POST /checkout endpoint
```text

**Total**: 16 small, validated increments instead of 1 large, error-prone request.

**Result**: Working shopping cart with **zero** debugging cycles.

## Conclusion

Incremental development is the **single most effective pattern** for reducing errors in AI-assisted coding.

**Key Principles**:

1. **Break features into smallest possible increments** (20-100 lines each)
2. **Validate every increment immediately** (run, test, verify)
3. **Fix issues before proceeding** (never build on broken code)
4. **Use working code as context** (show AI what works)
5. **Build bottom-up** (data → utilities → services → APIs)

**The Result**:

- **90% error rate reduction**
- **18x faster to working code**
- **High confidence in AI-generated code**
- **Low developer frustration**
- **Predictable, reliable development process**

Stop asking AI agents for complete features. Start building incrementally.

Every successful increment is a step forward. Every validation is a guarantee. Every fixed bug stays fixed.

That's how you build reliable systems with AI coding agents.

## Related Concepts

- [Test-Driven Prompting](./test-driven-prompting.md) - Write tests first to constrain each increment's implementation
- [Quality Gates as Information Filters](./quality-gates-as-information-filters.md) - Mathematical foundation for why validation works
- [Verification Sandwich Pattern](./verification-sandwich-pattern.md) - Establish clean baseline before/after each generation step
- [Stateless Verification Loops](./stateless-verification-loops.md) - Ensure each validation cycle starts from clean slate
- [Plan Mode for Strategic Thinking](./plan-mode-strategic.md) - Plan increments strategically before implementation
- [Trust But Verify Protocol](./trust-but-verify-protocol.md) - Validation strategy for each increment
- [Layered Prompts Architecture](./layered-prompts-architecture.md) - Planning increments in advance
- [Context Debugging Framework](./context-debugging-framework.md) - Debugging failed increments
- [Human-First DX Philosophy](./human-first-dx-philosophy.md) - Small increments reduce cognitive load for humans

## References

- [Incremental Development - Martin Fowler](https://martinfowler.com/articles/incremental-development.html) - Classic article on incremental development principles
- [Test-Driven Development](https://en.wikipedia.org/wiki/Test-driven_development) - Related methodology that validates each increment through tests

Topics
Ai Assisted CodingBest PracticesContext AccumulationError ReductionIncremental DevelopmentIterative DevelopmentQuality GatesValidation LoopsWorkflows

More Insights

Cover Image for Thought Leaders

Thought Leaders

People to follow for compound engineering, context engineering, and AI agent development.

James Phoenix
James Phoenix
Cover Image for Systems Thinking & Observability

Systems Thinking & Observability

Software should be treated as a measurable dynamical system, not as a collection of features.

James Phoenix
James Phoenix