Back to Blog
Clean CodeBest PracticesCode QualityMaintainability

Clean Code Principles Every Developer Should Know

Write code that's easy to read, maintain, and extend. Practical clean code principles with real examples.

B
Bootspring Team
Engineering
March 5, 2025
7 min read

Clean code is code that's easy to understand and easy to change. It's not about cleverness—it's about clarity. Here are principles that make code clean.

Meaningful Names#

Use Intention-Revealing Names#

1// ❌ Bad 2const d = new Date(); 3const x = users.filter(u => u.a > 18); 4const list = []; 5 6// ✅ Good 7const currentDate = new Date(); 8const adultUsers = users.filter(user => user.age > 18); 9const activeSubscriptions = [];

Avoid Abbreviations and Single Letters#

1// ❌ Bad 2function calc(p, q, d) { 3 return p * q * (1 - d); 4} 5 6// ✅ Good 7function calculateDiscountedPrice( 8 price: number, 9 quantity: number, 10 discountRate: number, 11): number { 12 return price * quantity * (1 - discountRate); 13}

Use Pronounceable Names#

1// ❌ Bad 2const genymdhms = new Date(); 3const modymdhms = new Date(); 4 5// ✅ Good 6const generationTimestamp = new Date(); 7const modificationTimestamp = new Date();

Functions#

Keep Functions Small#

1// ❌ Bad - does too many things 2async function processOrder(order: Order): Promise<void> { 3 // Validate order 4 if (!order.items.length) throw new Error('No items'); 5 if (!order.customerId) throw new Error('No customer'); 6 const customer = await db.customers.find(order.customerId); 7 if (!customer) throw new Error('Customer not found'); 8 9 // Calculate totals 10 let subtotal = 0; 11 for (const item of order.items) { 12 const product = await db.products.find(item.productId); 13 subtotal += product.price * item.quantity; 14 } 15 const tax = subtotal * 0.1; 16 const total = subtotal + tax; 17 18 // Process payment 19 const payment = await stripe.charges.create({ 20 amount: total * 100, 21 customer: customer.stripeId, 22 }); 23 24 // Save order 25 await db.orders.create({ ...order, total, paymentId: payment.id }); 26 27 // Send email 28 await sendEmail(customer.email, 'Order confirmation', { order }); 29} 30 31// ✅ Good - single responsibility per function 32async function processOrder(order: Order): Promise<void> { 33 await validateOrder(order); 34 const total = await calculateOrderTotal(order); 35 const payment = await processPayment(order.customerId, total); 36 await saveOrder(order, total, payment.id); 37 await sendOrderConfirmation(order); 38} 39 40async function validateOrder(order: Order): Promise<void> { 41 if (!order.items.length) throw new Error('No items'); 42 if (!order.customerId) throw new Error('No customer'); 43 44 const customer = await db.customers.find(order.customerId); 45 if (!customer) throw new Error('Customer not found'); 46} 47 48async function calculateOrderTotal(order: Order): Promise<number> { 49 let subtotal = 0; 50 for (const item of order.items) { 51 const product = await db.products.find(item.productId); 52 subtotal += product.price * item.quantity; 53 } 54 return subtotal * 1.1; // Including tax 55}

Do One Thing#

1// ❌ Bad - multiple responsibilities 2function emailClients(clients: Client[]): void { 3 for (const client of clients) { 4 const record = db.find(client); 5 if (record.isActive()) { 6 email(client); 7 } 8 } 9} 10 11// ✅ Good - each function does one thing 12function getActiveClients(clients: Client[]): Client[] { 13 return clients.filter(client => { 14 const record = db.find(client); 15 return record.isActive(); 16 }); 17} 18 19function emailActiveClients(clients: Client[]): void { 20 const activeClients = getActiveClients(clients); 21 activeClients.forEach(client => email(client)); 22}

Limit Parameters#

1// ❌ Bad - too many parameters 2function createUser( 3 name: string, 4 email: string, 5 age: number, 6 address: string, 7 phone: string, 8 role: string, 9): User { 10 // ... 11} 12 13// ✅ Good - use an object 14interface CreateUserInput { 15 name: string; 16 email: string; 17 age: number; 18 address?: string; 19 phone?: string; 20 role?: string; 21} 22 23function createUser(input: CreateUserInput): User { 24 // ... 25}

Comments#

Code Should Be Self-Documenting#

1// ❌ Bad - comment explains confusing code 2// Check if employee is eligible for benefits 3if (employee.type === 'ft' && employee.yrs > 1) { 4 // ... 5} 6 7// ✅ Good - code is self-explanatory 8const isFullTimeEmployee = employee.type === 'full-time'; 9const hasCompletedProbation = employee.yearsEmployed > 1; 10 11if (isFullTimeEmployee && hasCompletedProbation) { 12 grantBenefits(employee); 13}

Don't Comment Bad Code—Rewrite It#

1// ❌ Bad 2// Loop through array and find users over 18 who have verified 3// email and are not banned, then get their names 4const names = arr.filter(u => u.a >= 18 && u.v && !u.b).map(u => u.n); 5 6// ✅ Good 7const eligibleUsers = users.filter(isEligibleUser); 8const userNames = eligibleUsers.map(user => user.name); 9 10function isEligibleUser(user: User): boolean { 11 return user.age >= 18 && user.emailVerified && !user.isBanned; 12}

Good Comments#

1// Explain why, not what 2// Using insertion sort because the array is nearly sorted (benchmarked) 3insertionSort(items); 4 5// Clarify complex business rules 6// Premium users get 20% discount, but not during sales 7// (per marketing agreement Q2 2024) 8const discount = isPremiumUser && !isSalePeriod ? 0.2 : 0; 9 10// Warn about consequences 11// WARNING: Changing this timeout breaks the payment retry logic 12// See incident #234 for context 13const PAYMENT_TIMEOUT = 30000;

Error Handling#

Use Exceptions, Not Return Codes#

1// ❌ Bad 2function divide(a: number, b: number): number | null { 3 if (b === 0) return null; 4 return a / b; 5} 6 7const result = divide(10, 0); 8if (result === null) { 9 // Handle error 10} 11 12// ✅ Good 13function divide(a: number, b: number): number { 14 if (b === 0) { 15 throw new Error('Cannot divide by zero'); 16 } 17 return a / b; 18} 19 20try { 21 const result = divide(10, 0); 22} catch (error) { 23 // Handle error 24}

Don't Return Null#

1// ❌ Bad - caller must check for null 2function findUser(id: string): User | null { 3 return db.users.find(id) || null; 4} 5 6const user = findUser('123'); 7if (user !== null) { 8 // use user 9} 10 11// ✅ Good - throw or return empty object 12function findUser(id: string): User { 13 const user = db.users.find(id); 14 if (!user) { 15 throw new UserNotFoundError(id); 16 } 17 return user; 18} 19 20// Or use Optional pattern 21function findUser(id: string): User | undefined { 22 return db.users.find(id); 23}

Classes#

Single Responsibility Principle#

1// ❌ Bad - class does too much 2class User { 3 constructor(public name: string, public email: string) {} 4 5 save(): void { 6 db.users.save(this); 7 } 8 9 sendEmail(subject: string, body: string): void { 10 mailer.send(this.email, subject, body); 11 } 12 13 generateReport(): string { 14 return `User Report: ${this.name}...`; 15 } 16} 17 18// ✅ Good - separated concerns 19class User { 20 constructor(public name: string, public email: string) {} 21} 22 23class UserRepository { 24 save(user: User): void { 25 db.users.save(user); 26 } 27} 28 29class UserNotifier { 30 sendEmail(user: User, subject: string, body: string): void { 31 mailer.send(user.email, subject, body); 32 } 33} 34 35class UserReporter { 36 generateReport(user: User): string { 37 return `User Report: ${user.name}...`; 38 } 39}

Keep Classes Small#

1// Classes should have a small number of instance variables 2// Methods should manipulate those variables 3// High cohesion = methods use most instance variables 4 5class OrderProcessor { 6 private order: Order; 7 private customer: Customer; 8 private paymentGateway: PaymentGateway; 9 10 constructor(order: Order, customer: Customer, gateway: PaymentGateway) { 11 this.order = order; 12 this.customer = customer; 13 this.paymentGateway = gateway; 14 } 15 16 process(): ProcessedOrder { 17 this.validateOrder(); 18 const total = this.calculateTotal(); 19 const payment = this.chargeCustomer(total); 20 return this.createProcessedOrder(payment); 21 } 22 23 // All methods work with instance variables 24}

Formatting#

Consistent Style#

1// Pick a style and stick with it 2// Use tools: Prettier, ESLint 3 4// Vertical spacing: related code together 5function processUser(user: User): void { 6 // Validation block 7 validateName(user.name); 8 validateEmail(user.email); 9 validateAge(user.age); 10 11 // Processing block 12 const normalizedUser = normalizeUser(user); 13 const enrichedUser = enrichUser(normalizedUser); 14 15 // Persistence block 16 saveUser(enrichedUser); 17 indexUser(enrichedUser); 18}

Tests#

Clean Tests Follow F.I.R.S.T#

1// Fast - tests should run quickly 2// Independent - tests should not depend on each other 3// Repeatable - tests should work in any environment 4// Self-validating - tests should have boolean output 5// Timely - tests should be written at the right time 6 7describe('OrderCalculator', () => { 8 // Arrange, Act, Assert pattern 9 it('calculates total with discount', () => { 10 // Arrange 11 const items = [ 12 { price: 100, quantity: 2 }, 13 { price: 50, quantity: 1 }, 14 ]; 15 const discount = 0.1; 16 17 // Act 18 const total = calculateTotal(items, discount); 19 20 // Assert 21 expect(total).toBe(225); // (200 + 50) * 0.9 22 }); 23});

The Boy Scout Rule#

"Leave the code cleaner than you found it." Every time you touch code: - Rename a confusing variable - Extract a small function - Remove dead code - Add a clarifying comment Small, continuous improvements compound over time.

Conclusion#

Clean code isn't about following rules mechanically—it's about empathy for the next developer (including future you). Write code that tells a story, that's easy to navigate, and that doesn't require heroics to understand.

Start small: improve one thing in every file you touch. Over time, the codebase transforms.

Share this article

Help spread the word about Bootspring