Review, Refactor, and Improve Patterns
Establish systematic patterns for reviewing and refactoring AI-generated code to improve quality.
Review, Refactor, and Improve Patterns
You've just watched your AI assistant generate hundreds of lines of code in seconds. It works. Tests pass. You're tempted to merge and move on. But here's the truth: AI-generated code is a first draft, not a finished product. The real skill in vibe coding isn't just getting AI to write code—it's knowing how to review, refactor, and systematically improve what it produces.
This lesson teaches you patterns for turning AI-generated code into production-quality software. You'll learn what to look for, when to refactor, and how to build a reliable review process that catches issues before they become technical debt.
Why AI Code Needs Different Review Patterns
AI assistants don't think like human developers. They optimize for pattern matching and completion, not for your specific codebase's conventions, performance requirements, or long-term maintainability. This creates unique review challenges:
- Over-generic solutions: AI often produces code that works broadly but isn't optimized for your specific use case
- Inconsistent patterns: Different prompts can yield different coding styles, even within the same file
- Hidden complexity: AI might implement features in convoluted ways that are harder to maintain
- Missing context: AI doesn't know your team's conventions, performance requirements, or architectural decisions
As covered in hallucination-detection, AI can also confidently suggest non-existent APIs or outdated approaches. Your review process needs to catch these issues systematically.
The Three-Pass Review Pattern
Instead of trying to catch everything in one review, use a structured three-pass approach. Each pass has a specific focus, making reviews faster and more thorough.
Pass 1: Correctness and Safety
Your first pass focuses on whether the code is correct and safe to run. This is where you catch the critical issues.
What to check:
# AI-generated authentication function
def authenticate_user(username, password):
user = db.query(f"SELECT * FROM users WHERE username = '{username}'")
if user and user.password == password:
return user
return None
Critical issues:
- SQL injection vulnerability (string interpolation)
- Plain text password comparison
- No rate limiting or account lockout
Refactored version:
from werkzeug.security import check_password_hash
import time
def authenticate_user(username: str, password: str) -> Optional[User]:
# Use parameterized queries to prevent SQL injection
user = db.query(
"SELECT * FROM users WHERE username = ?",
(username,)
).first()
if not user:
# Prevent timing attacks
time.sleep(0.1)
return None
# Use proper password hashing
if check_password_hash(user.password_hash, password):
return user
time.sleep(0.1)
return None
First-pass checklist:
- Security vulnerabilities (security-considerations)
- Error handling for edge cases
- Type safety and null checks
- Resource cleanup (files, connections, memory)
- Input validation
Pass 2: Design and Architecture
Once you know the code is safe, evaluate whether it fits your application's architecture and follows good design principles.
// AI-generated user service - works but has design issues
class UserService {
async createUser(userData) {
// Validation
if (!userData.email || !userData.name) {
throw new Error('Missing required fields');
}
// Hash password
const bcrypt = require('bcrypt');
const hashedPassword = await bcrypt.hash(userData.password, 10);
// Save to database
const db = require('./database');
const user = await db.users.create({
...userData,
password: hashedPassword
});
// Send welcome email
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport(/* config */);
await transporter.sendMail({
to: user.email,
subject: 'Welcome!',
text: `Welcome ${user.name}!`
});
// Log the event
console.log(`User created: ${user.id}`);
return user;
}
}
Design problems:
- Violates Single Responsibility Principle
- Tight coupling to specific libraries
- Hard to test
- No dependency injection
- Mixing concerns (validation, persistence, email, logging)
Refactored with better design:
class UserService {
constructor(userRepository, passwordHasher, emailService, logger) {
this.userRepository = userRepository;
this.passwordHasher = passwordHasher;
this.emailService = emailService;
this.logger = logger;
}
async createUser(userData) {
// Validate using dedicated validator
this.validateUserData(userData);
// Hash password through injected service
const hashedPassword = await this.passwordHasher.hash(userData.password);
// Persist through repository pattern
const user = await this.userRepository.create({
...userData,
password: hashedPassword
});
// Async operations don't block return
this.sendWelcomeEmail(user).catch(err =>
this.logger.error('Failed to send welcome email', { userId: user.id, err })
);
this.logger.info('User created', { userId: user.id });
return user;
}
validateUserData(userData) {
const required = ['email', 'name', 'password'];
const missing = required.filter(field => !userData[field]);
if (missing.length > 0) {
throw new ValidationError(`Missing required fields: ${missing.join(', ')}`);
}
}
async sendWelcomeEmail(user) {
await this.emailService.send({
to: user.email,
template: 'welcome',
data: { name: user.name }
});
}
}
Second-pass checklist:
- Follows SOLID principles
- Proper separation of concerns
- Testable design (dependency injection)
- Consistent with existing codebase patterns
- Appropriate abstraction levels
- Clear responsibilities
Pass 3: Readability and Maintainability
The final pass ensures the code is easy to understand and maintain. This is where you polish.
# AI-generated code - functionally correct but hard to read
def process_orders(orders):
result = []
for o in orders:
if o['status'] == 'pending' and o['total'] > 100:
o['priority'] = 'high'
o['discount'] = o['total'] * 0.1 if o['customer']['vip'] else 0
result.append(o)
elif o['status'] == 'pending':
o['priority'] = 'normal'
o['discount'] = 0
result.append(o)
return result
Readability improvements:
from dataclasses import dataclass
from typing import List
from decimal import Decimal
@dataclass
class Order:
order_id: str
status: str
total: Decimal
customer: 'Customer'
priority: str = 'normal'
discount: Decimal = Decimal('0')
class OrderProcessor:
"""Processes pending orders and assigns priority levels."""
HIGH_VALUE_THRESHOLD = Decimal('100')
VIP_DISCOUNT_RATE = Decimal('0.1')
def process_pending_orders(self, orders: List[Order]) -> List[Order]:
"""Filter pending orders and assign priority with applicable discounts.
Args:
orders: List of all orders to process
Returns:
List of pending orders with priority and discount applied
"""
pending_orders = self._filter_pending_orders(orders)
return [self._assign_priority_and_discount(order) for order in pending_orders]
def _filter_pending_orders(self, orders: List[Order]) -> List[Order]:
return [order for order in orders if order.status == 'pending']
def _assign_priority_and_discount(self, order: Order) -> Order:
"""Assign priority level and calculate VIP discount if applicable."""
order.priority = self._calculate_priority(order)
order.discount = self._calculate_discount(order)
return order
def _calculate_priority(self, order: Order) -> str:
"""High priority for orders above threshold, normal otherwise."""
return 'high' if order.total > self.HIGH_VALUE_THRESHOLD else 'normal'
def _calculate_discount(self, order: Order) -> Decimal:
"""Apply VIP discount rate to high-value orders for VIP customers."""
if order.customer.is_vip and order.total > self.HIGH_VALUE_THRESHOLD:
return order.total * self.VIP_DISCOUNT_RATE
return Decimal('0')
Third-pass checklist:
- Clear, descriptive naming
- Proper documentation (doc-generation)
- Magic numbers extracted to constants
- Complex logic broken into named functions
- Consistent formatting
- Removed commented-out code and debug statements
Refactoring Patterns for AI Code
Certain refactoring patterns appear repeatedly when improving AI-generated code. Recognize these opportunities:
Pattern 1: Extract Configuration
AI often hardcodes values that should be configurable.
// AI-generated - hardcoded values
async function fetchUserData(userId: string) {
const response = await fetch(
`https://api.example.com/users/${userId}`,
{ timeout: 5000, retries: 3 }
);
return response.json();
}
// Refactored - externalized configuration
interface ApiConfig {
baseUrl: string;
timeout: number;
maxRetries: number;
}
class UserApiClient {
constructor(private config: ApiConfig) {}
async fetchUser(userId: string) {
const url = `${this.config.baseUrl}/users/${userId}`;
return this.fetchWithRetry(url);
}
private async fetchWithRetry(url: string) {
// Retry logic using this.config.maxRetries
}
}
Pattern 2: Replace Conditionals with Polymorphism
AI loves long if-else chains. Often, these should be polymorphic designs.
# AI-generated conditional mess
def calculate_shipping(order):
if order.type == 'standard':
return order.weight * 0.5
elif order.type == 'express':
return order.weight * 1.5 + 10
elif order.type == 'overnight':
return order.weight * 2.5 + 25
else:
raise ValueError('Unknown order type')
# Refactored with strategy pattern
from abc import ABC, abstractmethod
class ShippingStrategy(ABC):
@abstractmethod
def calculate(self, weight: float) -> float:
pass
class StandardShipping(ShippingStrategy):
def calculate(self, weight: float) -> float:
return weight * 0.5
class ExpressShipping(ShippingStrategy):
def calculate(self, weight: float) -> float:
return weight * 1.5 + 10
class OvernightShipping(ShippingStrategy):
def calculate(self, weight: float) -> float:
return weight * 2.5 + 25
class Order:
def __init__(self, weight: float, shipping: ShippingStrategy):
self.weight = weight
self.shipping = shipping
def calculate_shipping_cost(self) -> float:
return self.shipping.calculate(self.weight)
Pattern 3: Consolidate Duplicate Logic
AI might generate similar code in multiple places because each prompt lacks context from previous generations.
// AI generated these in separate prompts - notice the duplication
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
function validateUserInput(data) {
if (!data.email) return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(data.email);
}
function checkEmailFormat(emailAddress) {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(emailAddress);
}
// Refactored - single source of truth
class Validator {
static EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
static isValidEmail(email) {
return email && this.EMAIL_PATTERN.test(email);
}
}
function validateUserInput(data) {
return Validator.isValidEmail(data.email);
}
Building a Review Checklist
Create a standardized checklist for your team. This ensures consistency and catches common AI-generated code issues. Here's a starter template:
## AI Code Review Checklist
### Security & Safety
- [ ] No SQL injection vulnerabilities (parameterized queries used)
- [ ] Input validation on all external data
- [ ] No hardcoded secrets or credentials
- [ ] Proper authentication and authorization
- [ ] Safe handling of user-uploaded files
- [ ] No eval() or similar dangerous functions
### Correctness
- [ ] Edge cases handled (null, empty, boundary values)
- [ ] Error handling appropriate for context
- [ ] No off-by-one errors in loops
- [ ] Async operations properly awaited
- [ ] Resource cleanup in finally blocks
### Testing
- [ ] Code is testable (no tight coupling)
- [ ] Tests cover happy path and edge cases
- [ ] No reliance on external services in unit tests
- [ ] Tests are deterministic
### Design
- [ ] Single Responsibility Principle followed
- [ ] Dependencies injected, not hardcoded
- [ ] Appropriate abstraction level
- [ ] Consistent with existing patterns
- [ ] No premature optimization
### Maintainability
- [ ] Clear, descriptive names
- [ ] No magic numbers or strings
- [ ] Complex logic documented
- [ ] No commented-out code
- [ ] Consistent code style
Adapt this to your stack and team standards. As discussed in team-workflows, sharing this checklist ensures everyone reviews AI code consistently.
Automated Quality Gates
Don't rely solely on manual reviews. Automate what you can:
# .github/workflows/ai-code-quality.yml
name: AI Code Quality Checks
on: [pull_request]
jobs:
quality-gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Detect common AI code smells
- name: Check for hardcoded secrets
run: |
if grep -r "api_key.*=.*'.*'" src/; then
echo "Hardcoded secrets detected"
exit 1
fi
# Complexity analysis
- name: Cyclomatic complexity
run: |
radon cc src/ -a -nb
radon cc src/ -nc 10 # Fail if complexity > 10
# Security scanning
- name: Security scan
run: bandit -r src/ -ll
# Test coverage minimum
- name: Coverage check
run: |
pytest --cov=src --cov-fail-under=80
As covered in quality-control, automated checks catch issues before human review.
When to Refactor vs. Regenerate
Sometimes it's faster to improve your prompt and regenerate rather than manually refactoring. Use this decision framework:
Refactor when:
- The core logic is sound but needs polish
- It's a small, localized change
- You need to learn the codebase better
- The issue is about naming or structure
Regenerate when:
- The fundamental approach is wrong
- You're fixing the third issue in the same code
- The code doesn't match your architecture
- You can articulate what you need in a better prompt
Example regeneration:
Instead of refactoring messy code:
# Better prompt for regeneration
"Create a UserService class that:
- Uses dependency injection for database and email services
- Validates input using a separate Validator class
- Returns domain objects, not database records
- Handles errors with custom exceptions
- Follows our repository pattern (see src/repositories/BaseRepository.ts)
- Matches the style in src/services/ProductService.ts"
See review-refactor for deeper strategies on improving AI-generated code.
Managing Refactoring Debt
Not every AI-generated code block needs immediate refactoring. Prioritize based on:
- Risk: Security issues and bugs first
- Frequency: Code that changes often needs better structure
- Visibility: Public APIs before internal utilities
- Impact: Core business logic before peripheral features
Tag code for future improvement:
# TODO(ai-refactor): Extract configuration to settings file
# Priority: Medium | Created: 2024-01-15 | Issue: #234
API_ENDPOINT = "https://api.example.com"
TIMEOUT = 5000
As discussed in managing-tech-debt, track these items systematically so they don't accumulate.
Practice Exercise: Review This AI Code
Here's AI-generated code with multiple issues. Practice identifying problems across all three review passes:
def get_user_orders(user_id):
# Get user from database
import sqlite3
conn = sqlite3.connect('app.db')
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
user = cursor.fetchone()
if user:
# Get orders
cursor.execute(f"SELECT * FROM orders WHERE user_id = {user_id}")
orders = cursor.fetchall()
# Calculate total
total = 0
for order in orders:
total = total + order[3] # price column
# Apply discount
if total > 1000:
discount = total * 0.1
else:
discount = 0
result = {
'user': user,
'orders': orders,
'total': total,
'discount': discount,
'final': total - discount
}
return result
Issues to find:
- Pass 1 (Correctness): SQL injection, no connection closing, magic number index
- Pass 2 (Design): Mixed concerns, no error handling, tight coupling
- Pass 3 (Readability): Poor naming, magic numbers, missing documentation
Compare your findings with the checklists above.
Continuous Improvement Loop
The best vibe coders treat each review as a learning opportunity:
- Track patterns: Note recurring issues in AI-generated code
- Update prompts: Build a library of prompts that avoid common problems
- Share learnings: Document team patterns and anti-patterns
- Refine checklists: Add team-specific items based on your stack
- Measure quality: Track metrics like bug rate, review time, and refactoring frequency
This feedback loop, combined with agentic-optimization strategies, continuously improves your AI-assisted development process.
Key Takeaways
Reviewing AI-generated code isn't optional—it's where the real engineering happens:
- Use three-pass reviews: correctness, design, then readability
- Build and maintain team-specific review checklists
- Recognize common refactoring patterns in AI code
- Automate quality gates to catch issues early
- Know when to refactor vs. regenerate with better prompts
- Treat review as a learning opportunity to improve future prompts
The developers who excel at vibe coding aren't those who generate the most code—they're those who most effectively review, refactor, and improve what AI produces. Master these patterns, and you'll transform AI from a code generator into a true productivity multiplier.
Next, explore testing-strategies to learn how to validate AI-generated code systematically, or dive into performance-optimization to make that code production-ready.