Free Guide
Clean Code Principles for Backend Engineers
Practical coding principles that make your backend code readable, maintainable, and team-friendly.
10 min read
Introduction
Clean code is not about perfection—it’s about clarity, consistency, and making life easier for your future self and your teammates. The code you write today will be read 10 times for every time it’s written. That means readability should be your primary concern.
Naming Conventions
Variables and Functions
Use intention-revealing names that make the code self-documenting.
Bad:
const d = new Date();
const calc = (p, r) => p * r * 0.01;
function proc(data) {
// ...
}
Good:
const currentDate = new Date();
const calculateMonthlyInterest = (principal, rate) => principal * rate * 0.01;
function processUserPayments(userData) {
// ...
}
Bad (Java):
int x;
String s;
List l;
Good (Java):
int userAge;
String firstName;
List<Payment> userPayments;
Rules for naming:
- Pronounceable: avoid cryptic abbreviations
- Searchable:
MAX_RETRIESis better thanmr - Consistent: if you use
userIdin one place, use it everywhere, notuser_id,userID, oruser - No misleading contexts: don’t use
Listin the name unless it’s actually a list - Classes are nouns:
UserService,PaymentProcessor - Functions are verbs:
getUserById(),calculateTax(),validateEmail()
Constants and Enums
Use UPPER_SNAKE_CASE for constants:
const MAX_CONCURRENT_REQUESTS = 10;
const DEFAULT_TIMEOUT_MS = 5000;
const API_BASE_URL = process.env.API_URL || 'https://api.example.com';
enum OrderStatus {
PENDING,
PROCESSING,
COMPLETED,
CANCELLED
}
Function Size and Responsibility
The Single Responsibility Principle
A function should do one thing, and do it well.
Bad function (does too much):
function handleUserRegistration(userData) {
// Validation
if (!userData.email || !userData.password) {
throw new Error('Missing fields');
}
// Transformation
const user = {
email: userData.email.toLowerCase(),
password: hashPassword(userData.password),
createdAt: new Date()
};
// Database
saveToDatabase(user);
// Email
sendWelcomeEmail(user.email);
// Logging
logUserSignup(user);
return user;
}
Good (separated concerns):
function registerNewUser(userData) {
const validatedData = validateRegistrationData(userData);
const user = createUser(validatedData);
notifyNewUserRegistered(user);
return user;
}
function validateRegistrationData(data) {
if (!data.email || !data.password) {
throw new ValidationError('Email and password are required');
}
return { email: data.email.toLowerCase(), password: data.password };
}
function createUser(validatedData) {
return {
email: validatedData.email,
password: hashPassword(validatedData.password),
createdAt: new Date()
};
}
function notifyNewUserRegistered(user) {
sendWelcomeEmail(user.email);
logUserSignup(user);
}
Keep Functions Small
A good function:
- Fits on one screen (15-20 lines max)
- Has 1-3 parameters (more than 3 is a code smell)
- Does one thing
- Has a descriptive name
If you’re nesting too deeply (3+ levels), extract inner logic to separate functions:
Bad:
function processOrders(orders) {
for (const order of orders) {
if (order.status === 'pending') {
if (order.total > 100) {
if (checkInventory(order.items)) {
updateInventory(order.items);
updateOrderStatus(order, 'processing');
}
}
}
}
}
Good:
function processOrders(orders) {
return orders
.filter(isPendingOrder)
.filter(isHighValueOrder)
.filter(hasInventory)
.forEach(fulfillOrder);
}
const isPendingOrder = (order) => order.status === 'pending';
const isHighValueOrder = (order) => order.total > 100;
const hasInventory = (order) => checkInventory(order.items);
const fulfillOrder = (order) => {
updateInventory(order.items);
updateOrderStatus(order, 'processing');
};
Error Handling Patterns
Use Specific Exceptions
Bad:
try {
const user = fetchUser(userId);
return user;
} catch (e) {
console.log('error');
return null;
}
Good:
try {
const user = await fetchUser(userId);
if (!user) {
throw new NotFoundError(`User ${userId} not found`);
}
return user;
} catch (error) {
if (error instanceof NotFoundError) {
logger.warn(`User lookup failed: ${error.message}`);
throw error;
}
logger.error('Unexpected error fetching user', error);
throw new InternalServerError('Failed to fetch user');
}
Java example:
try {
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
} catch (DatabaseException e) {
logger.error("Database error fetching user", e);
throw new InternalServerError("Failed to fetch user");
}
Don’t Ignore Exceptions
Bad:
try {
saveToDatabase(data);
} catch (e) {
// Ignore
}
Good:
try {
await saveToDatabase(data);
} catch (error) {
logger.error('Failed to save data', error);
throw new PersistenceError('Could not save data to database', error);
}
Use Error Context
Always provide context about what operation failed:
function withdrawFunds(accountId, amount) {
try {
const account = fetchAccount(accountId);
if (account.balance < amount) {
throw new InsufficientFundsError(
`Cannot withdraw $${amount} from account ${accountId}. ` +
`Current balance: $${account.balance}`
);
}
return processWithdrawal(account, amount);
} catch (error) {
logger.error(`Withdrawal failed for account ${accountId}`, { amount, error });
throw error;
}
}
When to Comment vs When to Refactor
Good Comments
Comments should explain why, not what:
// Business rule: Premium members get 20% discount on orders over $100
const discount = isPremiumMember(user) && order.total > 100 ? 0.2 : 0;
// HACK: API returns 500 instead of 404 for deleted users
// Remove once API is fixed (jira: BACKEND-1234)
if (response.status === 500) {
throw new NotFoundError();
}
Bad Comments
These are noise and often become stale:
// Increment i
i++;
// Check if user is admin
if (user.role === 'admin') {
// This function calculates the total
function calculateTotal(items) {
The Rule of Thumb
If you need to write a comment to explain what the code does, refactor instead:
Bad:
// Loop through orders and check if total > 100, then apply 10% discount
const discountedOrders = [];
for (const order of orders) {
if (order.total > 100) {
order.total = order.total * 0.9;
discountedOrders.push(order);
}
}
Good:
const applyBulkDiscount = (order) => {
return order.total > 100
? { ...order, total: order.total * 0.9 }
: order;
};
const discountedOrders = orders.map(applyBulkDiscount);
Code Smells to Watch For
1. Duplicate Code
Don’t Repeat Yourself (DRY). If you copy-paste code twice, it should be a function:
// Smelly: duplicated validation
function createUser(email, password) {
if (!email || !email.includes('@')) throw new Error('Invalid email');
if (password.length < 8) throw new Error('Password too short');
// ...
}
function updateUser(email, password) {
if (!email || !email.includes('@')) throw new Error('Invalid email');
if (password.length < 8) throw new Error('Password too short');
// ...
}
// Better: extracted validation
function validateEmail(email) {
if (!email || !email.includes('@')) {
throw new Error('Invalid email');
}
}
function validatePassword(password) {
if (password.length < 8) {
throw new Error('Password too short');
}
}
function createUser(email, password) {
validateEmail(email);
validatePassword(password);
// ...
}
2. Long Parameter Lists
If a function has more than 3 parameters, consider grouping them:
// Smelly
function createOrder(userId, itemId, quantity, shippingAddress, billingAddress, preferredDeliveryDate, giftWrap) {
}
// Better
function createOrder(userId, orderData) {
// orderData = { itemId, quantity, shippingAddress, billingAddress, preferredDeliveryDate, giftWrap }
}
3. God Objects
Classes that do too much:
// Smelly: UserService handles users, payments, notifications, and emails
class UserService {
public void registerUser(User user) { }
public void chargeUser(User user, BigDecimal amount) { }
public void sendEmail(User user, String message) { }
public void logUserActivity(User user, String action) { }
}
// Better: separated concerns
class UserService { }
class PaymentService { }
class EmailService { }
class AuditLogger { }
4. Magic Numbers
Use named constants instead:
// Smelly
if (user.age > 18 && order.total > 100) {
applyPremiumDiscount(order, 0.2);
}
// Better
const ADULT_AGE = 18;
const PREMIUM_DISCOUNT_THRESHOLD = 100;
const PREMIUM_DISCOUNT_RATE = 0.2;
if (user.age > ADULT_AGE && order.total > PREMIUM_DISCOUNT_THRESHOLD) {
applyPremiumDiscount(order, PREMIUM_DISCOUNT_RATE);
}
5. Inconsistent Error Handling
Don’t mix error handling styles:
// Inconsistent
function fetchUser(id) {
try {
return getUserFromDb(id);
} catch (e) {
return null; // sometimes null
}
}
function getOrders(userId) {
if (!userId) {
throw new Error('userId required'); // sometimes throws
}
return ordersDb.query(userId);
}
// Consistent
function fetchUser(id) {
if (!id) throw new ValidationError('userId is required');
try {
return getUserFromDb(id);
} catch (error) {
throw new UserNotFoundError(id, error);
}
}
function getOrders(userId) {
if (!userId) throw new ValidationError('userId is required');
try {
return ordersDb.query(userId);
} catch (error) {
throw new DatabaseError('Failed to fetch orders', error);
}
}
Refactoring Strategies
Extract Method
When a function has multiple responsibilities, extract smaller functions:
// Before
function reportSales(startDate, endDate) {
const orders = db.query(`SELECT * FROM orders WHERE date BETWEEN ? AND ?`, [startDate, endDate]);
let totalSales = 0;
let totalOrders = 0;
let avgOrderValue = 0;
for (const order of orders) {
totalSales += order.amount;
totalOrders++;
}
avgOrderValue = totalSales / totalOrders;
return { totalSales, totalOrders, avgOrderValue };
}
// After
function reportSales(startDate, endDate) {
const orders = fetchOrders(startDate, endDate);
return calculateSalesMetrics(orders);
}
function fetchOrders(startDate, endDate) {
return db.query(
`SELECT * FROM orders WHERE date BETWEEN ? AND ?`,
[startDate, endDate]
);
}
function calculateSalesMetrics(orders) {
const totalSales = orders.reduce((sum, o) => sum + o.amount, 0);
const totalOrders = orders.length;
const avgOrderValue = totalSales / totalOrders;
return { totalSales, totalOrders, avgOrderValue };
}
Replace Conditional with Polymorphism
Instead of if/else chains, use inheritance or composition:
// Before: lots of if statements
function calculateShippingCost(order, shippingMethod) {
if (shippingMethod === 'standard') {
return order.weight * 0.5;
} else if (shippingMethod === 'express') {
return order.weight * 1.5 + 10;
} else if (shippingMethod === 'overnight') {
return order.weight * 3 + 25;
}
}
// After: strategy pattern
const shippingStrategies = {
standard: (order) => order.weight * 0.5,
express: (order) => order.weight * 1.5 + 10,
overnight: (order) => order.weight * 3 + 25,
};
function calculateShippingCost(order, shippingMethod) {
const calculator = shippingStrategies[shippingMethod];
if (!calculator) throw new Error(`Unknown shipping method: ${shippingMethod}`);
return calculator(order);
}
Introduce Parameter Object
When functions take too many related parameters:
// Before
function saveUser(firstName, lastName, email, phone, address, city, state, zip) {
const user = new User(firstName, lastName, email, phone, address, city, state, zip);
return userDb.save(user);
}
// After
function saveUser(userDetails) {
const user = new User(userDetails);
return userDb.save(user);
}
// Called with
saveUser({
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
phone: '555-1234',
address: '123 Main St',
city: 'Springfield',
state: 'IL',
zip: '62701'
});
Testing and Clean Code
Write code that’s easy to test. Testable code is almost always cleaner code:
// Hard to test: dependencies hidden, side effects
function processPayment(amount) {
const user = getCurrentUser(); // global state
const rate = fetchExchangeRate(); // HTTP call
const result = chargeCard(user.card, amount * rate); // external call
updateDatabase(user, result); // side effect
return result;
}
// Easy to test: injected dependencies, pure logic
function processPayment(amount, user, rate, chargeCardFn, updateDbFn) {
const totalAmount = calculateTotal(amount, rate);
const result = chargeCardFn(user.card, totalAmount);
updateDbFn(user, result);
return result;
}
function calculateTotal(amount, rate) {
return amount * rate;
}
// In tests:
test('processes payment correctly', () => {
const result = processPayment(
100,
{ card: '4111...' },
1.2,
(card, amount) => ({ success: true, transactionId: '123' }),
jest.fn()
);
expect(result.success).toBe(true);
});
Summary Checklist
Before submitting code for review, verify:
- Variable and function names are intention-revealing
- Functions are small (< 20 lines) and do one thing
- No duplicate code
- Error handling is consistent and specific
- Comments explain why, not what
- No magic numbers (use named constants)
- Function parameters (< 3)
- Classes have single responsibility
- Code is tested and testable
- Consistent naming conventions throughout
- No temporary debug code left behind