Skip to content

Before & After Examples

Real-world code transformations showing Claude Code prompts and their results.

These examples show real transformations — what code looked like before, the prompt used, and the result. Use them to understand what effective AI-assisted development looks like in practice.

Example 1: Untested Service to Comprehensive Test Suite

Before

A UserRegistrationService with zero tests. The team has been shipping it for months based on manual testing and code review.

Prompt

Read UserRegistrationService.java. Write comprehensive JUnit 5 tests
covering: valid registration, null inputs, invalid email format,
weak passwords, taken username, taken email, and multiple simultaneous
validation errors. Use Mockito for the UserRepository and AssertJ
assertions. Follow the naming convention: methodName_givenCondition_expectedResult.

After

15 focused tests in under 60 seconds, organized by feature area, covering every branch. The team found 2 bugs they did not know about.

What to Notice

  • Claude followed the project’s naming convention because it read CLAUDE.md
  • Tests are organized with @Nested classes — Claude chose this based on the test count
  • Each test has a descriptive @DisplayName
  • Claude used lenient() for mock setup shared across tests — a pattern many engineers forget

Example 2: SQL Injection to Parameterized Queries

Before

async getOrdersByUser(userId: string): Promise<Order[]> {
  const query = `SELECT * FROM orders WHERE user_id = '${userId}'`;
  const result = await this.db.query(query);
  return result.rows;
}

async searchOrders(status: string, dateRange: string): Promise<Order[]> {
  const query = `SELECT * FROM orders WHERE status = '${status}'
    AND created_at > '${dateRange}'`;
  const result = await this.db.query(query);
  return result.rows;
}

Prompt

Read OrderProcessor.ts and fix all SQL injection vulnerabilities.
Use parameterized queries. Show me each change.

After

async getOrdersByUser(userId: string): Promise<Order[]> {
  const query = 'SELECT * FROM orders WHERE user_id = $1';
  const result = await this.db.query(query, [userId]);
  return result.rows;
}

async searchOrders(status: string, dateRange: string): Promise<Order[]> {
  const query = 'SELECT * FROM orders WHERE status = $1 AND created_at > $2';
  const result = await this.db.query(query, [status, dateRange]);
  return result.rows;
}

What to Notice

  • Claude found ALL instances, not just the first one
  • It used $1, $2 parameterized query syntax (correct for PostgreSQL)
  • The fix is minimal — only the vulnerable parts changed
  • A human reviewer might catch the first instance but miss others in a 500-line file

Example 3: Deeply Nested Conditionals to Guard Clauses

Before

public void processOrder(Order order) {
    if (order != null) {
        if (order.getItems() != null && !order.getItems().isEmpty()) {
            if (order.getPayment() != null) {
                if (order.getPayment().isValid()) {
                    if (inventoryService.hasStock(order.getItems())) {
                        // 5 levels deep — actual business logic
                        paymentService.charge(order.getPayment());
                        inventoryService.reserve(order.getItems());
                        orderRepository.save(order);
                        notificationService.sendConfirmation(order);
                    } else {
                        throw new OutOfStockException("Items not available");
                    }
                } else {
                    throw new PaymentException("Invalid payment method");
                }
            } else {
                throw new PaymentException("No payment provided");
            }
        } else {
            throw new IllegalArgumentException("Order has no items");
        }
    } else {
        throw new IllegalArgumentException("Order cannot be null");
    }
}

Prompt

Flatten the nested conditionals in processOrder using guard clauses.
Keep the same exceptions and messages. Don't change any behavior.

After

public void processOrder(Order order) {
    if (order == null) {
        throw new IllegalArgumentException("Order cannot be null");
    }
    if (order.getItems() == null || order.getItems().isEmpty()) {
        throw new IllegalArgumentException("Order has no items");
    }
    if (order.getPayment() == null) {
        throw new PaymentException("No payment provided");
    }
    if (!order.getPayment().isValid()) {
        throw new PaymentException("Invalid payment method");
    }
    if (!inventoryService.hasStock(order.getItems())) {
        throw new OutOfStockException("Items not available");
    }

    // Business logic at the top level — easy to read
    paymentService.charge(order.getPayment());
    inventoryService.reserve(order.getItems());
    orderRepository.save(order);
    notificationService.sendConfirmation(order);
}

What to Notice

  • 5 levels of nesting reduced to 0 levels of nesting
  • Same behavior, same exceptions, same messages
  • The happy path (actual business logic) is now at the top level
  • Each guard clause is independently readable

Example 4: God Method to Extracted Helpers

Before

def generate_report(type, format, options = {})
  if type == "daily_summary"
    data = []
    @audit_data.each do |audit|
      if audit[:date] == options[:date]
        store = @store_data.find { |s| s[:id] == audit[:store_id] }
        if store
          score = audit[:score]
          status = if score >= 90
                     "Excellent"
                   elsif score >= 75
                     "Good"
                   elsif score >= 60
                     "Needs Improvement"
                   else
                     "Critical"
                   end
          # ... 40 more lines of data processing
        end
      end
    end

    if format == "text"
      # ... 15 lines of text formatting
    elsif format == "csv"
      # ... 10 lines of CSV formatting
    elsif format == "html"
      # ... 20 lines of HTML formatting
    end
  elsif type == "weekly_trends"
    # ... 50 more lines
  end
end
# Total: 180 lines in one method

Prompt

Extract the data processing for each report type into separate
private methods. Then extract the formatting into separate methods.
Keep the generate_report method as a simple dispatcher. Don't change
any output behavior.

After

def generate_report(type, format, options = {})
  data = build_report_data(type, options)
  format_report(data, type, format, options)
end

private

def build_report_data(type, options)
  case type
  when "daily_summary"  then build_daily_summary(options[:date])
  when "weekly_trends"  then build_weekly_trends
  when "store_comparison" then build_store_comparison
  else raise "Unknown report type: #{type}"
  end
end

def format_report(data, type, format, options)
  case format
  when "text" then format_text(data, type, options)
  when "csv"  then format_csv(data, type)
  when "html" then format_html(data, type)
  else raise "Unknown format: #{format}"
  end
end

def score_status(score)
  case score
  when 90.. then "Excellent"
  when 75.. then "Good"
  when 60.. then "Needs Improvement"
  else "Critical"
  end
end

# Each method is 10-15 lines and does one thing

What to Notice

  • 180-line god method became 8 focused methods of 10-15 lines each
  • The main method is now 3 lines — a pure dispatcher
  • Score thresholds are in one place (DRY)
  • Each method has a single responsibility
  • Output is byte-identical to the original

Example 5: Missing Error Handling to Robust Service

Before

async transferStock(from: string, to: string, productId: string, qty: number) {
  const fromStock = await this.db.getStock(from, productId);
  await this.db.updateStock(from, productId, fromStock - qty);
  await this.db.updateStock(to, productId,
    (await this.db.getStock(to, productId)) + qty);
}

Prompt

Review transferStock for error handling gaps. What happens if:
1. From warehouse doesn't have enough stock?
2. The second updateStock fails after the first succeeds?
3. Any of the inputs are null or negative?
Show me a robust version.

After

async transferStock(from: string, to: string, productId: string, qty: number) {
  if (!from || !to || !productId) {
    throw new Error('All parameters are required');
  }
  if (from === to) {
    throw new Error('Cannot transfer to the same warehouse');
  }
  if (qty <= 0) {
    throw new Error('Quantity must be positive');
  }

  const fromStock = await this.db.getStock(from, productId);
  if (fromStock < qty) {
    throw new InsufficientStockError(
      `Need ${qty} of ${productId} in ${from}, have ${fromStock}`
    );
  }

  // Remove from source first
  await this.db.updateStock(from, productId, fromStock - qty);

  try {
    // Add to destination
    const toStock = await this.db.getStock(to, productId);
    await this.db.updateStock(to, productId, toStock + qty);
  } catch (error) {
    // Rollback: restore source warehouse
    await this.db.updateStock(from, productId, fromStock);
    throw new TransferError(`Transfer failed, rolled back: ${error.message}`);
  }
}

What to Notice

  • Input validation catches bad data early
  • Insufficient stock check prevents negative inventory
  • Try/catch with rollback handles partial failure
  • Custom error types make error handling easier for callers
  • The 3-line “happy path” became a 20-line robust method — and that is correct

Example 6: Vague Prompt vs. Specific Prompt

Before (vague prompt)

Review this code

Result: 15 findings of mixed severity, including “consider adding JSDoc comments” and “this variable could be const” alongside “this SQL query is vulnerable to injection.”

After (specific prompt)

Review this code for security vulnerabilities only. For each finding:
1. Show the vulnerable line
2. Explain what an attacker could do
3. Show the fixed code
Categorize as CRITICAL or IMPORTANT. Skip style concerns.

Result: 4 findings, all security-related, each with a clear explanation and fix. The SQL injection is the first item, not buried under style nits.

What to Notice

  • Specific prompts get specific results
  • Asking for categories and fixes makes the output actionable
  • Telling Claude what to skip is as important as telling it what to find