Best Practices
Clean Code Principles: Writing Maintainable, Testable, and Scalable Software
12 min
Clean CodeBest PracticesSoftware DesignSOLIDTesting
Learn clean code principles that make software maintainable, testable, and scalable. Covers naming conventions, function design, SOLID principles, and refactoring patterns with real examples.
Clean code isn't just about working code—it's about code that humans can understand, maintain, and extend. This guide distills decades of software engineering wisdom into practical patterns you can apply today.
Meaningful Names
1// ❌ Bad: Unclear names
2function calc(a, b) {
3 return a * b * 0.2;
4}
5const d = new Date();
6const t = 86400000;
7
8// ✅ Good: Intention-revealing names
9function calculateOrderDiscount(orderTotal, itemCount) {
10 const DISCOUNT_RATE = 0.2;
11 return orderTotal * itemCount * DISCOUNT_RATE;
12}
13const currentDate = new Date();
14const MILLISECONDS_PER_DAY = 86400000;
15
16// Naming conventions:
17// - Use pronounceable names (getUserProfile vs getUsrPrfl)
18// - Use searchable names (MAX_RETRIES vs 3)
19// - Avoid mental mapping (i, j, k only in short loops)
20// - Class names: nouns (User, Order, PaymentProcessor)
21// - Function names: verbs (getUser, processPayment, validateEmail)
22// - Boolean names: is/has/can (isActive, hasPermission, canDelete)
23
24// ❌ Bad: Inconsistent naming
25function getUser() {}
26function retrieveProfile() {}
27function fetchAccount() {}
28
29// ✅ Good: Consistent naming
30function getUser() {}
31function getProfile() {}
32function getAccount() {}
Functions: Do One Thing Well
1// ❌ Bad: Function does too many things
2function processUserOrder(userId, items) {
3 // Validate user
4 const user = database.query('SELECT * FROM users WHERE id = ?', userId);
5 if (!user) throw new Error('User not found');
6 if (!user.isActive) throw new Error('User inactive');
7
8 // Calculate total
9 let total = 0;
10 for (const item of items) {
11 const product = getProduct(item.id);
12 total += product.price * item.quantity;
13 }
14
15 // Apply discount
16 if (user.isPremium) total *= 0.9;
17
18 // Process payment
19 const paymentResult = stripe.charge(user.cardId, total);
20 if (!paymentResult.success) throw new Error('Payment failed');
21
22 // Send email
23 sendEmail(user.email, 'Order confirmation', total);
24
25 // Update inventory
26 for (const item of items) {
27 updateStock(item.id, -item.quantity);
28 }
29
30 return { orderId: generateId(), total };
31}
32
33// ✅ Good: Each function does one thing
34function processUserOrder(userId, items) {
35 const user = validateActiveUser(userId);
36 const total = calculateOrderTotal(items, user);
37 const payment = processPayment(user, total);
38 await sendOrderConfirmation(user.email, total);
39 await updateInventory(items);
40
41 return createOrder(user.id, items, total, payment.id);
42}
43
44function validateActiveUser(userId) {
45 const user = getUserById(userId);
46 if (!user) throw new UserNotFoundError(userId);
47 if (!user.isActive) throw new InactiveUserError(userId);
48 return user;
49}
50
51function calculateOrderTotal(items, user) {
52 const subtotal = items.reduce((sum, item) => {
53 const product = getProduct(item.id);
54 return sum + (product.price * item.quantity);
55 }, 0);
56
57 return user.isPremium ? applyPremiumDiscount(subtotal) : subtotal;
58}
59
60// Benefits:
61// - Each function is easy to test
62// - Easy to understand what each function does
63// - Easy to reuse (validateActiveUser used elsewhere)
64// - Easy to modify (change discount logic in one place)
SOLID Principles
1// S - Single Responsibility Principle
2// Each class should have only one reason to change
3
4// ❌ Bad: User class does too much
5class User {
6 constructor(name, email) {
7 this.name = name;
8 this.email = email;
9 }
10
11 saveToDatabase() {
12 // Database logic
13 }
14
15 sendWelcomeEmail() {
16 // Email logic
17 }
18
19 generateReport() {
20 // Reporting logic
21 }
22}
23
24// ✅ Good: Separate concerns
25class User {
26 constructor(name, email) {
27 this.name = name;
28 this.email = email;
29 }
30}
31
32class UserRepository {
33 save(user) {
34 // Database logic
35 }
36}
37
38class EmailService {
39 sendWelcomeEmail(user) {
40 // Email logic
41 }
42}
43
44class UserReportGenerator {
45 generate(user) {
46 // Reporting logic
47 }
48}
49
50// O - Open/Closed Principle
51// Open for extension, closed for modification
52
53// ❌ Bad: Must modify class for new shapes
54class AreaCalculator {
55 calculate(shapes) {
56 let area = 0;
57 for (const shape of shapes) {
58 if (shape.type === 'circle') {
59 area += Math.PI * shape.radius ** 2;
60 } else if (shape.type === 'rectangle') {
61 area += shape.width * shape.height;
62 }
63 // Adding triangle requires modifying this class
64 }
65 return area;
66 }
67}
68
69// ✅ Good: Extend without modifying
70class Shape {
71 area() {
72 throw new Error('Must implement area()');
73 }
74}
75
76class Circle extends Shape {
77 constructor(radius) {
78 super();
79 this.radius = radius;
80 }
81
82 area() {
83 return Math.PI * this.radius ** 2;
84 }
85}
86
87class Rectangle extends Shape {
88 constructor(width, height) {
89 super();
90 this.width = width;
91 this.height = height;
92 }
93
94 area() {
95 return this.width * this.height;
96 }
97}
98
99class AreaCalculator {
100 calculate(shapes) {
101 return shapes.reduce((sum, shape) => sum + shape.area(), 0);
102 }
103}
104
105// D - Dependency Inversion Principle
106// Depend on abstractions, not concretions
107
108// ❌ Bad: High-level depends on low-level
109class MySQLDatabase {
110 save(data) { /* MySQL specific */ }
111}
112
113class UserService {
114 constructor() {
115 this.db = new MySQLDatabase(); // Tightly coupled
116 }
117
118 saveUser(user) {
119 this.db.save(user);
120 }
121}
122
123// ✅ Good: Depend on interface
124class UserService {
125 constructor(database) {
126 this.database = database; // Inject dependency
127 }
128
129 saveUser(user) {
130 this.database.save(user);
131 }
132}
133
134// Can now use any database
135const service1 = new UserService(new MySQLDatabase());
136const service2 = new UserService(new PostgreSQLDatabase());
137const service3 = new UserService(new MockDatabase()); // For testing!
Error Handling
1// ❌ Bad: Silent failures
2function getUser(id) {
3 try {
4 return database.query('SELECT * FROM users WHERE id = ?', id);
5 } catch (error) {
6 console.log(error);
7 return null; // Caller doesn't know it failed
8 }
9}
10
11// ✅ Good: Explicit error handling
12class UserNotFoundError extends Error {
13 constructor(userId) {
14 super(`User with ID ${userId} not found`);
15 this.name = 'UserNotFoundError';
16 this.userId = userId;
17 }
18}
19
20function getUser(id) {
21 try {
22 const user = database.query('SELECT * FROM users WHERE id = ?', id);
23 if (!user) {
24 throw new UserNotFoundError(id);
25 }
26 return user;
27 } catch (error) {
28 if (error instanceof UserNotFoundError) {
29 throw error; // Re-throw expected errors
30 }
31 // Log unexpected errors
32 logger.error('Database error in getUser', { id, error });
33 throw new DatabaseError('Failed to fetch user');
34 }
35}
36
37// Caller can handle specific errors
38try {
39 const user = getUser(123);
40} catch (error) {
41 if (error instanceof UserNotFoundError) {
42 return res.status(404).json({ error: 'User not found' });
43 }
44 return res.status(500).json({ error: 'Internal server error' });
45}
Code Comments Done Right
1// ❌ Bad: Stating the obvious
2let i = 0; // Set i to 0
3i++; // Increment i
4
5// ❌ Bad: Commented-out code (use version control!)
6function processData(data) {
7 // const result = oldMethod(data);
8 // if (result > 10) {
9 // doSomething();
10 // }
11 return newMethod(data);
12}
13
14// ✅ Good: Explain WHY, not WHAT
15// Wait 100ms to avoid rate limiting (API allows 10 req/sec)
16await sleep(100);
17
18// Handle edge case: negative IDs are from legacy system
19if (userId < 0) {
20 userId = convertLegacyId(userId);
21}
22
23// TODO: Replace with bulk API when available (Q2 2025)
24for (const item of items) {
25 await saveItem(item);
26}
27
28// ✅ Good: Document complex algorithms
29/**
30 * Implements Levenshtein distance algorithm to calculate
31 * similarity between two strings.
32 *
33 * Time complexity: O(m * n)
34 * Space complexity: O(m * n)
35 *
36 * @param {string} str1 - First string
37 * @param {string} str2 - Second string
38 * @returns {number} Edit distance between strings
39 */
40function calculateEditDistance(str1, str2) {
41 // Implementation...
42}
43
44// Best comment is clear code that needs no comment
45// ❌ Bad:
46const d = 86400000; // milliseconds in a day
47
48// ✅ Good:
49const MILLISECONDS_PER_DAY = 86400000;
Key Takeaways
- Names should reveal intent - no need for comments if names are clear
- Functions should do one thing and do it well
- Classes should have single responsibility
- Prefer composition over inheritance for flexibility
- Write code for humans first, machines second
- Make implicit knowledge explicit (constants, enums)
- Test your code - untested code is legacy code
- Refactor continuously - don't let technical debt grow
- Use meaningful abstractions, not premature abstractions
- Follow team conventions over personal preferences
- Code reviews catch issues comments miss
- DRY (Don't Repeat Yourself) but don't abstract too early
- KISS (Keep It Simple, Stupid) - simplest solution wins
- YAGNI (You Aren't Gonna Need It) - build what you need now