Detailed patterns for extracting code into well-named, well-scoped units. Each pattern includes when to apply, when NOT to apply, before/after examples in multiple languages, and common mistakes.
How many inputs does the extracted code need?
│
├─ 0-3 values → Pass as individual parameters
│
├─ 4+ related values → Group into a parameter object / struct
│ └─ { user, permissions, settings } instead of (user, perms, theme, lang, tz)
│
├─ Values come from a shared context → Consider making it a method on that context
│
└─ Mix of config and data → Separate: config as constructor/init, data as method params
calculateShippingCost not loopThroughItemsAndSumvalidate, transform, calculate, fetch, buildisValid, hasPermission, canAccess, shouldRetryprocess, handle, do, run, execute (too vague alone)validateEmail not just validateBefore:
async function processOrder(order: Order) {
// Validate order items
if (order.items.length === 0) {
throw new Error('Order must have at least one item');
}
for (const item of order.items) {
if (item.quantity <= 0) {
throw new Error(`Invalid quantity for item ${item.id}`);
}
if (item.price < 0) {
throw new Error(`Invalid price for item ${item.id}`);
}
}
// Calculate totals
let subtotal = 0;
for (const item of order.items) {
subtotal += item.price * item.quantity;
}
const tax = subtotal * 0.08;
const shipping = subtotal > 100 ? 0 : 9.99;
const total = subtotal + tax + shipping;
// Persist
const savedOrder = await db.orders.create({
...order,
subtotal,
tax,
shipping,
total,
status: 'confirmed',
});
// Notify
await emailService.send(order.customerEmail, 'Order Confirmed', {
orderId: savedOrder.id,
total,
});
return savedOrder;
}
After:
async function processOrder(order: Order): Promise<SavedOrder> {
validateOrderItems(order.items);
const totals = calculateOrderTotals(order.items);
const savedOrder = await persistOrder(order, totals);
await notifyCustomer(order.customerEmail, savedOrder.id, totals.total);
return savedOrder;
}
function validateOrderItems(items: OrderItem[]): void {
if (items.length === 0) {
throw new Error('Order must have at least one item');
}
for (const item of items) {
if (item.quantity <= 0) {
throw new Error(`Invalid quantity for item ${item.id}`);
}
if (item.price < 0) {
throw new Error(`Invalid price for item ${item.id}`);
}
}
}
interface OrderTotals {
subtotal: number;
tax: number;
shipping: number;
total: number;
}
function calculateOrderTotals(items: OrderItem[]): OrderTotals {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * TAX_RATE;
const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_COST;
const total = subtotal + tax + shipping;
return { subtotal, tax, shipping, total };
}
async function persistOrder(order: Order, totals: OrderTotals): Promise<SavedOrder> {
return db.orders.create({ ...order, ...totals, status: 'confirmed' });
}
async function notifyCustomer(email: string, orderId: string, total: number): Promise<void> {
await emailService.send(email, 'Order Confirmed', { orderId, total });
}
Before:
def generate_report(users, start_date, end_date):
# Filter active users in date range
active_users = []
for user in users:
if user.is_active and start_date <= user.created_at <= end_date:
if user.email_verified:
active_users.append(user)
# Calculate statistics
total_revenue = 0
for user in active_users:
for order in user.orders:
if order.status == "completed":
total_revenue += order.total
avg_revenue = total_revenue / len(active_users) if active_users else 0
# Format output
lines = [f"Report: {start_date} to {end_date}"]
lines.append(f"Active Users: {len(active_users)}")
lines.append(f"Total Revenue: ${total_revenue:.2f}")
lines.append(f"Avg Revenue/User: ${avg_revenue:.2f}")
return "\n".join(lines)
After:
def generate_report(users: list[User], start_date: date, end_date: date) -> str:
active_users = filter_active_users(users, start_date, end_date)
revenue = calculate_total_revenue(active_users)
avg_revenue = revenue / len(active_users) if active_users else 0
return format_report(start_date, end_date, len(active_users), revenue, avg_revenue)
def filter_active_users(users: list[User], start: date, end: date) -> list[User]:
return [
u for u in users
if u.is_active and u.email_verified and start <= u.created_at <= end
]
def calculate_total_revenue(users: list[User]) -> float:
return sum(
order.total
for user in users
for order in user.orders
if order.status == "completed"
)
def format_report(start: date, end: date, user_count: int, revenue: float, avg: float) -> str:
return "\n".join([
f"Report: {start} to {end}",
f"Active Users: {user_count}",
f"Total Revenue: ${revenue:.2f}",
f"Avg Revenue/User: ${avg:.2f}",
])
Before:
func HandleUpload(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("document")
if err != nil {
http.Error(w, "Failed to read file", http.StatusBadRequest)
return
}
defer file.Close()
if header.Size > 10*1024*1024 {
http.Error(w, "File too large", http.StatusBadRequest)
return
}
ext := filepath.Ext(header.Filename)
if ext != ".pdf" && ext != ".docx" && ext != ".txt" {
http.Error(w, "Unsupported file type", http.StatusBadRequest)
return
}
data, err := io.ReadAll(file)
if err != nil {
http.Error(w, "Failed to read file", http.StatusInternalServerError)
return
}
hash := sha256.Sum256(data)
filename := fmt.Sprintf("%x%s", hash, ext)
path := filepath.Join("uploads", filename)
if err := os.WriteFile(path, data, 0644); err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]string{"path": path})
}
After:
func HandleUpload(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("document")
if err != nil {
http.Error(w, "Failed to read file", http.StatusBadRequest)
return
}
defer file.Close()
if err := validateUpload(header); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
path, err := saveFile(file, header.Filename)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]string{"path": path})
}
func validateUpload(header *multipart.FileHeader) error {
if header.Size > maxUploadSize {
return fmt.Errorf("file too large (max %d bytes)", maxUploadSize)
}
ext := filepath.Ext(header.Filename)
if !allowedExtensions[ext] {
return fmt.Errorf("unsupported file type: %s", ext)
}
return nil
}
func saveFile(file multipart.File, originalName string) (string, error) {
data, err := io.ReadAll(file)
if err != nil {
return "", fmt.Errorf("reading file: %w", err)
}
hash := sha256.Sum256(data)
ext := filepath.Ext(originalName)
filename := fmt.Sprintf("%x%s", hash, ext)
path := filepath.Join("uploads", filename)
if err := os.WriteFile(path, data, 0644); err != nil {
return "", fmt.Errorf("writing file: %w", err)
}
return path, nil
}
Before:
fn process_csv(path: &str) -> Result<Vec<Record>, Box<dyn Error>> {
let content = fs::read_to_string(path)?;
let mut records = Vec::new();
for (i, line) in content.lines().enumerate() {
if i == 0 { continue; } // skip header
let fields: Vec<&str> = line.split(',').collect();
if fields.len() < 3 {
eprintln!("Skipping malformed line {}: {}", i, line);
continue;
}
let name = fields[0].trim().to_string();
let age: u32 = match fields[1].trim().parse() {
Ok(a) if a > 0 && a < 150 => a,
_ => {
eprintln!("Invalid age on line {}", i);
continue;
}
};
let email = fields[2].trim().to_string();
if !email.contains('@') {
eprintln!("Invalid email on line {}", i);
continue;
}
records.push(Record { name, age, email });
}
Ok(records)
}
After:
fn process_csv(path: &str) -> Result<Vec<Record>, Box<dyn Error>> {
let content = fs::read_to_string(path)?;
let records = content
.lines()
.enumerate()
.skip(1) // skip header
.filter_map(|(i, line)| parse_record(i, line))
.collect();
Ok(records)
}
fn parse_record(line_num: usize, line: &str) -> Option<Record> {
let fields: Vec<&str> = line.split(',').collect();
if fields.len() < 3 {
eprintln!("Skipping malformed line {}: {}", line_num, line);
return None;
}
let name = fields[0].trim().to_string();
let age = parse_age(fields[1].trim(), line_num)?;
let email = parse_email(fields[2].trim(), line_num)?;
Some(Record { name, age, email })
}
fn parse_age(s: &str, line_num: usize) -> Option<u32> {
match s.parse::<u32>() {
Ok(a) if a > 0 && a < 150 => Some(a),
_ => {
eprintln!("Invalid age on line {}", line_num);
None
}
}
}
fn parse_email(s: &str, line_num: usize) -> Option<String> {
if s.contains('@') {
Some(s.to_string())
} else {
eprintln!("Invalid email on line {}", line_num);
None
}
}
| Mistake | Problem | Fix |
|---|---|---|
| Extracting with too many parameters | Function signature is unwieldy, hard to call | Group related params into a struct/object first |
| Naming the function after its implementation | loopAndFilter tells you nothing useful |
Name after the WHAT: filterActiveUsers |
| Extracting one line into a function | Adds indirection without clarity | Only extract if the name adds understanding |
| Not returning a value | Using mutation/side effects when a return value is cleaner | Prefer pure functions that return results |
| Leaving the original code commented out | Clutters the file, confuses future readers | Delete it; git has history |
Before:
function Dashboard({ user }: { user: User }) {
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'date'>('date');
const filteredProjects = useMemo(() => {
return user.projects
.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
.sort((a, b) => sortBy === 'name'
? a.name.localeCompare(b.name)
: b.createdAt.getTime() - a.createdAt.getTime()
);
}, [user.projects, searchTerm, sortBy]);
return (
<div>
<h1>Welcome, {user.name}</h1>
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search projects..."
/>
<select value={sortBy} onChange={e => setSortBy(e.target.value as 'name' | 'date')}>
<option value="date">Sort by Date</option>
<option value="name">Sort by Name</option>
</select>
</div>
<ul>
{filteredProjects.map(project => (
<li key={project.id}>
<h3>{project.name}</h3>
<p>{project.description}</p>
<span>{project.createdAt.toLocaleDateString()}</span>
<span>{project.status}</span>
</li>
))}
</ul>
</div>
);
}
After:
function Dashboard({ user }: { user: User }) {
return (
<div>
<h1>Welcome, {user.name}</h1>
<ProjectList projects={user.projects} />
</div>
);
}
// --- project-list.tsx ---
function ProjectList({ projects }: { projects: Project[] }) {
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<SortField>('date');
const filteredProjects = useFilteredProjects(projects, searchTerm, sortBy);
return (
<div>
<ProjectFilters
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
sortBy={sortBy}
onSortChange={setSortBy}
/>
<ul>
{filteredProjects.map(project => (
<ProjectCard key={project.id} project={project} />
))}
</ul>
</div>
);
}
// --- project-card.tsx ---
function ProjectCard({ project }: { project: Project }) {
return (
<li>
<h3>{project.name}</h3>
<p>{project.description}</p>
<span>{project.createdAt.toLocaleDateString()}</span>
<span>{project.status}</span>
</li>
);
}
Before:
<template>
<div>
<input v-model="search" placeholder="Search..." />
<ul>
<li v-for="item in filteredItems" :key="item.id">
<h3>{{ item.title }}</h3>
<p>{{ item.body }}</p>
<button @click="toggleFavorite(item.id)">
{{ item.isFavorite ? 'Unfavorite' : 'Favorite' }}
</button>
</li>
</ul>
</div>
</template>
After:
<!-- SearchableList.vue -->
<template>
<div>
<SearchInput v-model="search" />
<ItemCard
v-for="item in filteredItems"
:key="item.id"
:item="item"
@toggle-favorite="toggleFavorite"
/>
</div>
</template>
<!-- ItemCard.vue -->
<template>
<li>
<h3>{{ item.title }}</h3>
<p>{{ item.body }}</p>
<button @click="$emit('toggle-favorite', item.id)">
{{ item.isFavorite ? 'Unfavorite' : 'Favorite' }}
</button>
</li>
</template>
Should this be its own component?
│
├─ Does it have its own state? → YES, extract
├─ Is it reused in 2+ places? → YES, extract
├─ Is it > 50 lines of JSX/template? → Probably, extract
├─ Does it have a clear domain name? → YES, extract
├─ Would extracting require > 8 props? → NO, fix the boundary first
└─ Is it pure presentation, < 20 lines? → Probably not worth it
Before:
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
fetchUser(userId, { signal: controller.signal })
.then(setUser)
.catch(err => {
if (!controller.signal.aborted) setError(err);
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [userId]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return null;
return <div>{user.name}</div>;
}
After:
// hooks/use-user.ts
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
fetchUser(userId, { signal: controller.signal })
.then(setUser)
.catch(err => {
if (!controller.signal.aborted) setError(err);
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [userId]);
return { user, loading, error };
}
// components/user-profile.tsx
function UserProfile({ userId }: { userId: string }) {
const { user, loading, error } = useUser(userId);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return null;
return <div>{user.name}</div>;
}
Before:
<script setup>
const items = ref([]);
const loading = ref(false);
const page = ref(1);
const hasMore = ref(true);
async function loadMore() {
if (loading.value || !hasMore.value) return;
loading.value = true;
try {
const result = await fetchItems({ page: page.value });
items.value.push(...result.data);
hasMore.value = result.hasMore;
page.value++;
} finally {
loading.value = false;
}
}
onMounted(loadMore);
</script>
After:
// composables/use-paginated-list.ts
export function usePaginatedList<T>(fetcher: (page: number) => Promise<{ data: T[]; hasMore: boolean }>) {
const items = ref<T[]>([]);
const loading = ref(false);
const page = ref(1);
const hasMore = ref(true);
async function loadMore() {
if (loading.value || !hasMore.value) return;
loading.value = true;
try {
const result = await fetcher(page.value);
items.value.push(...result.data);
hasMore.value = result.hasMore;
page.value++;
} finally {
loading.value = false;
}
}
onMounted(loadMore);
return { items, loading, hasMore, loadMore };
}
<script setup>
const { items, loading, hasMore, loadMore } = usePaginatedList(
(page) => fetchItems({ page })
);
</script>
Large File (500+ lines)
│
├─ Identify responsibility clusters
│ Group functions/classes by what they operate on
│
├─ Check dependency direction
│ Draw arrows: A uses B means A depends on B
│ If A and B depend on each other → shared types module first
│
├─ Create new files, one per cluster
│ Each file exports its public API
│
├─ Create barrel file (index.ts) if needed
│ Re-export public API for backward compatibility
│
└─ Update imports across codebase
One file at a time, running tests after each
// utils.ts (600 lines)
export function formatDate(d: Date): string { /* ... */ }
export function parseDate(s: string): Date { /* ... */ }
export function daysBetween(a: Date, b: Date): number { /* ... */ }
export function formatCurrency(amount: number, currency: string): string { /* ... */ }
export function parseCurrency(s: string): number { /* ... */ }
export function convertCurrency(amount: number, from: string, to: string): number { /* ... */ }
export function validateEmail(email: string): boolean { /* ... */ }
export function validatePhone(phone: string): boolean { /* ... */ }
export function validateUrl(url: string): boolean { /* ... */ }
// utils/date.ts
export function formatDate(d: Date): string { /* ... */ }
export function parseDate(s: string): Date { /* ... */ }
export function daysBetween(a: Date, b: Date): number { /* ... */ }
// utils/currency.ts
export function formatCurrency(amount: number, currency: string): string { /* ... */ }
export function parseCurrency(s: string): number { /* ... */ }
export function convertCurrency(amount: number, from: string, to: string): number { /* ... */ }
// utils/validation.ts
export function validateEmail(email: string): boolean { /* ... */ }
export function validatePhone(phone: string): boolean { /* ... */ }
export function validateUrl(url: string): boolean { /* ... */ }
// utils/index.ts (barrel - backward compatible)
export * from './date';
export * from './currency';
export * from './validation';
Problem: A imports from B, B imports from A
Solution 1: Extract shared types
├─ types.ts (shared interfaces/types)
├─ a.ts (imports from types.ts)
└─ b.ts (imports from types.ts)
Solution 2: Dependency inversion
├─ a.ts (defines interface, imports nothing from B)
├─ b.ts (implements A's interface)
└─ main.ts (wires A and B together)
Solution 3: Merge if truly coupled
└─ ab.ts (if A and B are one responsibility, keep them together)
Before:
class OrderService {
async createOrder(items: CartItem[], customer: Customer): Promise<Order> {
// Validation
if (items.length === 0) throw new Error('Cart is empty');
for (const item of items) {
const product = await this.db.products.findById(item.productId);
if (!product) throw new Error(`Product ${item.productId} not found`);
if (product.stock < item.quantity) throw new Error(`Insufficient stock`);
}
// Price calculation
let subtotal = 0;
for (const item of items) {
const product = await this.db.products.findById(item.productId);
subtotal += product!.price * item.quantity;
}
const discount = customer.tier === 'premium' ? subtotal * 0.1 : 0;
const tax = (subtotal - discount) * 0.08;
const total = subtotal - discount + tax;
// Persistence
const order = await this.db.orders.create({ items, customerId: customer.id, subtotal, discount, tax, total });
// Notification
await this.mailer.send(customer.email, 'Order Confirmed', { orderId: order.id, total });
if (total > 500) {
await this.slack.notify('#high-value-orders', `New order: $${total}`);
}
return order;
}
}
After:
class OrderService {
constructor(
private validator: OrderValidator,
private calculator: PriceCalculator,
private repository: OrderRepository,
private notifier: OrderNotifier,
) {}
async createOrder(items: CartItem[], customer: Customer): Promise<Order> {
await this.validator.validateItems(items);
const pricing = this.calculator.calculate(items, customer);
const order = await this.repository.save(items, customer, pricing);
await this.notifier.orderConfirmed(order, customer);
return order;
}
}
class OrderValidator {
constructor(private productRepo: ProductRepository) {}
async validateItems(items: CartItem[]): Promise<void> {
if (items.length === 0) throw new Error('Cart is empty');
for (const item of items) {
const product = await this.productRepo.findById(item.productId);
if (!product) throw new Error(`Product ${item.productId} not found`);
if (product.stock < item.quantity) throw new Error('Insufficient stock');
}
}
}
class PriceCalculator {
calculate(items: CartItem[], customer: Customer): OrderPricing {
const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const discount = customer.tier === 'premium' ? subtotal * 0.1 : 0;
const tax = (subtotal - discount) * TAX_RATE;
return { subtotal, discount, tax, total: subtotal - discount + tax };
}
}
Before:
class ReportGenerator:
def generate(self, data, format_type, output_path):
# Data processing
cleaned = [row for row in data if row.get("valid")]
grouped = {}
for row in cleaned:
key = row["category"]
grouped.setdefault(key, []).append(row)
# Aggregation
summary = {}
for cat, rows in grouped.items():
summary[cat] = {
"count": len(rows),
"total": sum(r["amount"] for r in rows),
"average": sum(r["amount"] for r in rows) / len(rows),
}
# Formatting
if format_type == "csv":
output = self._to_csv(summary)
elif format_type == "json":
output = json.dumps(summary, indent=2)
elif format_type == "html":
output = self._to_html(summary)
# File I/O
with open(output_path, "w") as f:
f.write(output)
After:
class ReportGenerator:
def __init__(self, processor: DataProcessor, formatter: ReportFormatter, writer: FileWriter):
self.processor = processor
self.formatter = formatter
self.writer = writer
def generate(self, data: list[dict], format_type: str, output_path: str) -> None:
summary = self.processor.summarize(data)
output = self.formatter.format(summary, format_type)
self.writer.write(output, output_path)
class DataProcessor:
def summarize(self, data: list[dict]) -> dict[str, CategorySummary]:
cleaned = [row for row in data if row.get("valid")]
grouped = self._group_by_category(cleaned)
return {cat: self._aggregate(rows) for cat, rows in grouped.items()}
def _group_by_category(self, rows):
grouped = {}
for row in rows:
grouped.setdefault(row["category"], []).append(row)
return grouped
def _aggregate(self, rows):
amounts = [r["amount"] for r in rows]
return CategorySummary(count=len(rows), total=sum(amounts), average=sum(amounts) / len(rows))
async function fetchWithRetry(url: string) {
for (let i = 0; i < 3; i++) {
try {
const response = await fetch(url, { timeout: 5000 });
if (response.status === 429) {
await sleep(1000 * Math.pow(2, i));
continue;
}
return response;
} catch {
if (i === 2) throw new Error('Max retries exceeded');
await sleep(1000 * Math.pow(2, i));
}
}
}
// config/retry.ts
export const RETRY_CONFIG = {
maxAttempts: 3,
baseDelayMs: 1000,
requestTimeoutMs: 5000,
backoffMultiplier: 2,
retryableStatusCodes: [429, 502, 503, 504],
} as const;
// lib/fetch-with-retry.ts
import { RETRY_CONFIG } from '../config/retry';
async function fetchWithRetry(url: string, config = RETRY_CONFIG) {
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
try {
const response = await fetch(url, { timeout: config.requestTimeoutMs });
if (config.retryableStatusCodes.includes(response.status)) {
await sleep(config.baseDelayMs * Math.pow(config.backoffMultiplier, attempt));
continue;
}
return response;
} catch {
if (attempt === config.maxAttempts - 1) throw new Error('Max retries exceeded');
await sleep(config.baseDelayMs * Math.pow(config.backoffMultiplier, attempt));
}
}
}
[ ] Identified all magic numbers and strings
[ ] Grouped related config values into typed objects
[ ] Added sensible defaults (don't require config for common case)
[ ] Made config injectable for testing (parameter with default)
[ ] Documented units in names (timeoutMs, maxRetries, limitBytes)
[ ] Used const assertions or enums for type safety
[ ] Kept environment-specific values in env vars, not code