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
@Nestedclasses — 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,$2parameterized 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