Skip to content
S sufi.my
Back to Guides

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_RETRIES is better than mr
  • Consistent: if you use userId in one place, use it everywhere, not user_id, userID, or user
  • No misleading contexts: don’t use List in 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