Skip to content
Published on

Clean Code Principles 2025: 30 Rules for Writing Readable Code

Authors

Introduction

There is a seemingly insurmountable gap between "working code" and "readable code." Most developers focus on feature implementation and push code quality to the back burner, eventually watching maintenance costs snowball out of control.

Robert C. Martin's "Clean Code" was published 17 years ago, yet its principles remain as relevant as ever. However, with the emergence of modern languages and tools like TypeScript, Python, and Rust, the ways we practice those principles have evolved.

In this guide, we present 30 practical rules across 6 categories, each with Bad/Good code comparisons. All examples are in TypeScript and Python.

The Boy Scout Rule: Always leave the code cleaner than you found it.


Category 1: Naming — 6 Rules

Rule 1: Use Intention-Revealing Names

Variable, function, and class names should tell you "what it does" and "why it exists."

Bad:

const d: number = 86400;
const list1: number[] = [1, 2, 3];

function calc(a: number, b: number): number {
  return a * b * 0.1;
}

Good:

const SECONDS_PER_DAY: number = 86400;
const activeUserIds: number[] = [1, 2, 3];

function calculateDiscountedPrice(originalPrice: number, quantity: number): number {
  const DISCOUNT_RATE = 0.1;
  return originalPrice * quantity * DISCOUNT_RATE;
}

Python version:

# Bad
d = 86400
lst = [1, 2, 3]

def calc(a, b):
    return a * b * 0.1

# Good
SECONDS_PER_DAY = 86400
active_user_ids = [1, 2, 3]

def calculate_discounted_price(original_price: float, quantity: int) -> float:
    DISCOUNT_RATE = 0.1
    return original_price * quantity * DISCOUNT_RATE

Rule 2: Avoid Abbreviations

Abbreviations are clear only to the author and are puzzles for everyone else.

Bad:

const usrMgr = new UserManager();
const btnClkHndlr = () => {};
const genRpt = (dt: Date) => {};

Good:

const userManager = new UserManager();
const handleButtonClick = () => {};
const generateReport = (reportDate: Date) => {};

Acceptable abbreviations: Industry standards only — id, url, api, db, io, html, css, http

Rule 3: Use Searchable Names

Magic numbers and single-letter variables are impossible to search across a project.

Bad:

if (user.role === 5) {
  setTimeout(callback, 86400000);
}

Good:

const ADMIN_ROLE_ID = 5;
const ONE_DAY_IN_MILLISECONDS = 86400000;

if (user.role === ADMIN_ROLE_ID) {
  setTimeout(callback, ONE_DAY_IN_MILLISECONDS);
}

Rule 4: Use Pronounceable Names

You should be able to say names aloud during code reviews.

Bad:

class DtaRcrd102 {
  private genymdhms: Date;
  private modymdhms: Date;
  private pszqint: number;
}

Good:

class Customer {
  private generationTimestamp: Date;
  private modificationTimestamp: Date;
  private recordId: number;
}

Rule 5: Use Consistent Vocabulary

Do not mix multiple words for the same concept.

Bad:

class UserService {
  fetchUser(id: number) {}
  retrieveAccount(id: number) {}
  getUserProfile(id: number) {}
  obtainUserSettings(id: number) {}
}

Good:

class UserService {
  getUser(id: number) {}
  getAccount(id: number) {}
  getUserProfile(id: number) {}
  getUserSettings(id: number) {}
}

Team naming convention example:

ActionConventionExample
Read (single)get + noungetUser, getOrder
Read (list)list + plural nounlistUsers, listOrders
Createcreate + nouncreateUser, createOrder
Updateupdate + nounupdateUser, updateOrder
Deletedelete + noundeleteUser, deleteOrder
Check existencehas/is + adjective/nounhasPermission, isActive
Convertto + target formtoJSON, toString

Rule 6: Name Length Should Match Scope

Short scopes get short names; wide scopes get descriptive names.

Bad:

// Long name for a loop variable
for (let arrayIndex = 0; arrayIndex < items.length; arrayIndex++) {
  process(items[arrayIndex]);
}

// Short name for a global constant
const T = 3000;

Good:

// Loop variables are short
for (let i = 0; i < items.length; i++) {
  process(items[i]);
}

// Global/module-level is descriptive
const API_REQUEST_TIMEOUT_MS = 3000;

Category 2: Functions — 6 Rules

Rule 7: Keep Functions Small

The ideal function length is under 20 lines. It should fit on one screen.

Bad:

function processOrder(order: Order): OrderResult {
  // Validation (20 lines)
  if (!order.items || order.items.length === 0) throw new Error('No items');
  if (!order.customer) throw new Error('No customer');
  // ... more validation

  // Price calculation (30 lines)
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
  const tax = total * 0.1;
  const shipping = total > 100 ? 0 : 10;
  // ... more calculations

  // Payment processing (20 lines)
  const paymentResult = chargeCard(order.customer.cardId, total + tax + shipping);
  // ... payment logic

  // Notification (15 lines)
  sendEmail(order.customer.email, 'Order confirmed');
  // ... notification logic

  return { orderId: generateId(), total, status: 'completed' };
}

Good:

function processOrder(order: Order): OrderResult {
  validateOrder(order);
  const pricing = calculatePricing(order.items);
  const paymentResult = processPayment(order.customer, pricing.total);
  notifyCustomer(order.customer, paymentResult);

  return createOrderResult(paymentResult, pricing);
}

function validateOrder(order: Order): void {
  if (!order.items?.length) throw new InvalidOrderError('No items');
  if (!order.customer) throw new InvalidOrderError('No customer');
}

function calculatePricing(items: OrderItem[]): Pricing {
  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_FEE;
  return { subtotal, tax, shipping, total: subtotal + tax + shipping };
}

Rule 8: Do One Thing (Single Responsibility)

A function should do one thing, do it well, and do it only.

Bad:

function emailClients(clients: Client[]): void {
  for (const client of clients) {
    const record = database.lookup(client.id);
    if (record.isActive()) {
      email(client.email, 'Welcome!');
    }
  }
}

Good:

function emailActiveClients(clients: Client[]): void {
  const activeClients = getActiveClients(clients);
  activeClients.forEach(client => sendWelcomeEmail(client));
}

function getActiveClients(clients: Client[]): Client[] {
  return clients.filter(client => isClientActive(client.id));
}

function isClientActive(clientId: number): boolean {
  return database.lookup(clientId).isActive();
}

function sendWelcomeEmail(client: Client): void {
  email(client.email, 'Welcome!');
}

Rule 9: Keep Parameters Few (Ideally 2 or Fewer)

When you have 3 or more parameters, wrap them in an object.

Bad:

function createUser(
  name: string, email: string, age: number,
  role: string, department: string, isActive: boolean
): User { /* ... */ }

createUser('Alice', 'alice@example.com', 30, 'admin', 'engineering', true);

Good:

interface CreateUserParams {
  name: string;
  email: string;
  age: number;
  role: UserRole;
  department: string;
  isActive: boolean;
}

function createUser(params: CreateUserParams): User { /* ... */ }

createUser({
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
  role: UserRole.ADMIN,
  department: 'engineering',
  isActive: true,
});

Rule 10: Avoid Side Effects

Functions should not do unexpected things.

Bad:

let currentUser: User | null = null;

function checkPassword(userName: string, password: string): boolean {
  const user = database.findUser(userName);
  if (user && user.password === hash(password)) {
    // Side effect! A password check function initializes a session
    currentUser = user;
    initializeSession(user);
    return true;
  }
  return false;
}

Good:

function verifyPassword(userName: string, password: string): boolean {
  const user = database.findUser(userName);
  return user !== null && user.password === hash(password);
}

function login(userName: string, password: string): LoginResult {
  if (!verifyPassword(userName, password)) {
    return { success: false };
  }
  const user = database.findUser(userName);
  const session = initializeSession(user);
  return { success: true, user, session };
}

Rule 11: Command-Query Separation

A function should either "do something" or "return something," not both.

Bad:

function setAndCheckAttribute(name: string, value: string): boolean {
  const attribute = findAttribute(name);
  if (attribute) {
    attribute.value = value;
    return true;
  }
  return false;
}

// Confusing call site
if (setAndCheckAttribute('username', 'alice')) { /* ??? */ }

Good:

function attributeExists(name: string): boolean {
  return findAttribute(name) !== null;
}

function setAttribute(name: string, value: string): void {
  const attribute = findAttribute(name);
  if (!attribute) throw new AttributeNotFoundError(name);
  attribute.value = value;
}

// Clear call site
if (attributeExists('username')) {
  setAttribute('username', 'alice');
}

Rule 12: DRY (Don't Repeat Yourself)

Duplicate code is a breeding ground for bugs.

Bad:

function getAdminUsers(): User[] {
  const users = database.query('SELECT * FROM users');
  return users
    .filter(u => u.role === 'admin')
    .map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
    .sort((a, b) => a.displayName.localeCompare(b.displayName));
}

function getModeratorUsers(): User[] {
  const users = database.query('SELECT * FROM users');
  return users
    .filter(u => u.role === 'moderator')
    .map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
    .sort((a, b) => a.displayName.localeCompare(b.displayName));
}

Good:

function getUsersByRole(role: UserRole): User[] {
  const users = database.query('SELECT * FROM users WHERE role = ?', [role]);
  return users.map(addDisplayName).sort(byDisplayName);
}

function addDisplayName(user: User): User {
  return { ...user, displayName: `${user.firstName} ${user.lastName}` };
}

function byDisplayName(a: User, b: User): number {
  return a.displayName.localeCompare(b.displayName);
}

Category 3: Comments — 4 Rules

Rule 13: Explain "Why," Not "What"

Let the code express "what" and use comments to explain "why you did it this way."

Bad:

// Get the user name
const userName = user.getName();

// Iterate over the array
for (const item of items) {
  // Process the item
  processItem(item);
}

Good:

// External API limits batch requests to 100 items max (API docs: https://...)
const MAX_BATCH_SIZE = 100;

// Must encode as ISO-8859-1 for legacy system compatibility
// Will switch to UTF-8 after new system migration (JIRA-4521)
const encodedData = encode(data, 'ISO-8859-1');

// Lock must be acquired first due to concurrency issue
// Details: https://wiki.internal/concurrency-fix-2024
await acquireLock(resourceId);

Rule 14: Avoid Noise Comments

Comments that add no information make code harder to read.

Bad:

/**
 * Constructor
 */
constructor() {}

/**
 * Returns the id
 * @returns the id
 */
getId(): number {
  return this.id;
}

Good:

// Code so clear it needs no comments
class User {
  constructor(
    private readonly id: number,
    private readonly name: string,
  ) {}

  getId(): number {
    return this.id;
  }
}

Rule 15: TODOs Should Follow a Standard Format

TODOs are useful tools for tracking technical debt.

Bad:

// TODO fix later
// FIXME this doesn't work
// HACK temporary workaround

Good:

// TODO(team-auth): Add OAuth2 token refresh logic [JIRA-1234] @2025-Q2
// FIXME(alice): Race condition on concurrent requests. Need distributed lock [JIRA-5678]
// HACK: Safari 17.2 flexbox bug workaround. Remove after Safari 18 fix

Rule 16: Use JSDoc/Docstrings for Public APIs

TypeScript (JSDoc):

/**
 * Calculates user activity statistics for a given time period.
 *
 * @param userId - The user ID to query statistics for
 * @param startDate - Start date (inclusive)
 * @param endDate - End date (inclusive)
 * @returns Activity statistics object
 * @throws {UserNotFoundError} When the user does not exist
 * @throws {InvalidDateRangeError} When start date is after end date
 *
 * @example
 * const stats = await getUserActivityStats('user-123', '2025-01-01', '2025-03-31');
 * console.log(stats.totalLogins); // 45
 */
async function getUserActivityStats(
  userId: string,
  startDate: string,
  endDate: string,
): Promise<ActivityStats> {
  // ...
}

Python (Docstring):

def get_user_activity_stats(
    user_id: str,
    start_date: str,
    end_date: str,
) -> ActivityStats:
    """Calculate user activity statistics for a given time period.

    Args:
        user_id: The user ID to query statistics for
        start_date: Start date (inclusive, ISO 8601 format)
        end_date: End date (inclusive, ISO 8601 format)

    Returns:
        ActivityStats: Activity statistics object

    Raises:
        UserNotFoundError: When the user does not exist
        InvalidDateRangeError: When start date is after end date

    Example:
        >>> stats = get_user_activity_stats('user-123', '2025-01-01', '2025-03-31')
        >>> stats.total_logins
        45
    """
    ...

Category 4: Error Handling — 4 Rules

Rule 17: Use Exceptions Instead of Error Codes

Error codes force callers to handle errors immediately, making code convoluted.

Bad:

function withdraw(account: Account, amount: number): number {
  if (amount > account.balance) return -1;  // Insufficient funds
  if (amount < 0) return -2;                // Negative amount
  if (account.isFrozen) return -3;          // Frozen account

  account.balance -= amount;
  return 0;  // Success
}

const result = withdraw(myAccount, 100);
if (result === -1) { /* handle */ }
else if (result === -2) { /* handle */ }
else if (result === -3) { /* handle */ }

Good:

class InsufficientFundsError extends Error {
  constructor(public readonly balance: number, public readonly amount: number) {
    super(`Insufficient funds: balance ${balance}, requested ${amount}`);
    this.name = 'InsufficientFundsError';
  }
}

class AccountFrozenError extends Error {
  constructor(public readonly accountId: string) {
    super(`Account ${accountId} is frozen`);
    this.name = 'AccountFrozenError';
  }
}

function withdraw(account: Account, amount: number): void {
  if (amount < 0) throw new InvalidAmountError(amount);
  if (account.isFrozen) throw new AccountFrozenError(account.id);
  if (amount > account.balance) throw new InsufficientFundsError(account.balance, amount);

  account.balance -= amount;
}

Rule 18: Fail Fast

Detect invalid states as early as possible and report immediately.

Bad:

function processPayment(orderId: string, amount: number): PaymentResult {
  // ... 100 lines of logic ...

  // Validation only at the very end
  if (!orderId) return { success: false, error: 'Invalid order ID' };
  if (amount < 0) return { success: false, error: 'Invalid amount' };

  // ... actual payment processing ...
}

Good:

function processPayment(orderId: string, amount: number): PaymentResult {
  // Guard clauses: validate first
  if (!orderId) throw new InvalidArgumentError('orderId is required');
  if (amount <= 0) throw new InvalidArgumentError('amount must be positive');

  const order = orderRepository.findById(orderId);
  if (!order) throw new OrderNotFoundError(orderId);

  return paymentGateway.charge(order, amount);
}

Rule 19: Define Custom Errors

Build an error hierarchy that matches your business logic.

abstract class AppError extends Error {
  abstract readonly statusCode: number;
  abstract readonly isOperational: boolean;

  constructor(message: string, public readonly cause?: Error) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  readonly statusCode = 404;
  readonly isOperational = true;
}

class ValidationError extends AppError {
  readonly statusCode = 400;
  readonly isOperational = true;

  constructor(message: string, public readonly field: string, public readonly value: unknown) {
    super(message);
  }
}

class DatabaseError extends AppError {
  readonly statusCode = 500;
  readonly isOperational = false;  // System error — alert ops
}

Rule 20: Never Catch Generic Exceptions

catch (error) or except Exception hides unexpected errors.

Bad:

try {
  const data = await fetchUserData(userId);
  await processData(data);
  await saveToDatabase(data);
} catch (error) {
  console.log('Something went wrong');  // Swallows everything
}

Good:

try {
  const data = await fetchUserData(userId);
  await processData(data);
  await saveToDatabase(data);
} catch (error) {
  if (error instanceof NetworkError) {
    logger.warn('Network error fetching user data', { userId, error });
    await retryWithBackoff(() => fetchUserData(userId));
  } else if (error instanceof ValidationError) {
    logger.error('Invalid user data', { userId, field: error.field });
    throw error;
  } else if (error instanceof DatabaseError) {
    logger.fatal('Database error', { error });
    alertOpsTeam(error);
    throw error;
  } else {
    throw error;  // Re-throw unexpected errors
  }
}

Category 5: Code Organization — 5 Rules

Rule 21: Respect Vertical Formatting

Related code stays close; unrelated code is separated by blank lines.

Bad:

import { Logger } from './logger';
import { Database } from './database';
import { UserRepository } from './repositories/user';
const MAX_RETRIES = 3;
const logger = new Logger('UserService');
export class UserService {
  private db: Database;
  private repo: UserRepository;
  constructor(db: Database) {
    this.db = db;
    this.repo = new UserRepository(db);
  }
  async getUser(id: string): Promise<User> {
    const user = await this.repo.findById(id);
    if (!user) throw new NotFoundError('User not found');
    return user;
  }
}

Good:

import { Logger } from './logger';
import { Database } from './database';
import { UserRepository } from './repositories/user';

const MAX_RETRIES = 3;
const logger = new Logger('UserService');

export class UserService {
  private db: Database;
  private repo: UserRepository;

  constructor(db: Database) {
    this.db = db;
    this.repo = new UserRepository(db);
  }

  async getUser(id: string): Promise<User> {
    const user = await this.repo.findById(id);
    if (!user) throw new NotFoundError('User not found');
    return user;
  }
}

Rule 22: The Newspaper Metaphor

When reading a file top to bottom, abstraction level should decrease progressively.

// 1. First: Public interface (headline)
export class OrderProcessor {
  async processOrder(orderId: string): Promise<OrderResult> {
    const order = await this.validateAndFetchOrder(orderId);
    const pricing = this.calculatePricing(order);
    return this.executePayment(order, pricing);
  }

  // 2. Middle: Business logic (body)
  private async validateAndFetchOrder(orderId: string): Promise<Order> {
    const order = await this.orderRepo.findById(orderId);
    if (!order) throw new OrderNotFoundError(orderId);
    this.validateOrderState(order);
    return order;
  }

  private calculatePricing(order: Order): Pricing {
    const subtotal = this.sumItems(order.items);
    const discount = this.applyDiscounts(subtotal, order.coupons);
    const tax = this.calculateTax(subtotal - discount);
    return { subtotal, discount, tax, total: subtotal - discount + tax };
  }

  // 3. Last: Implementation details
  private validateOrderState(order: Order): void { /* ... */ }
  private sumItems(items: OrderItem[]): number { /* ... */ }
  private applyDiscounts(amount: number, coupons: Coupon[]): number { /* ... */ }
  private calculateTax(amount: number): number { /* ... */ }
}

Rule 23: Organize Imports

Import statements should follow a consistent ordering convention.

// 1. Node.js built-in modules
import { readFile } from 'fs/promises';
import path from 'path';

// 2. External libraries (node_modules)
import express from 'express';
import { z } from 'zod';

// 3. Internal modules — absolute paths
import { AppConfig } from '@/config';
import { Logger } from '@/utils/logger';

// 4. Internal modules — relative paths
import { UserService } from './services/user';
import { OrderService } from './services/order';

// 5. Type imports (TypeScript)
import type { User, Order } from './types';

Rule 24: Limit File Size

MetricRecommendedWarningDanger
File linesUnder 200200-400Over 400
Function linesUnder 2020-50Over 50
Class methodsUnder 1010-20Over 20
Parameters2 or fewer3-45 or more
Nesting depth2 levels max3 levels4+ levels

Rule 25: Define Clear Module Boundaries

src/
├── modules/
│   ├── user/
│   │   ├── index.ts          # Only export public API
│   │   ├── user.service.ts
│   │   ├── user.repository.ts
│   │   ├── user.controller.ts
│   │   ├── user.types.ts
│   │   └── __tests__/
│   ├── order/
│   │   ├── index.ts
│   │   └── ...
│   └── payment/
│       ├── index.ts
│       └── ...
├── shared/
│   ├── errors/
│   ├── utils/
│   └── types/
└── infrastructure/
    ├── database/
    ├── cache/
    └── messaging/

Key principle: Modules expose only through index.ts. Internal implementation stays hidden.

// modules/user/index.ts — only the public API
export { UserService } from './user.service';
export type { User, CreateUserDTO } from './user.types';
// UserRepository is NOT exported — internal implementation

Category 6: Testing — 5 Rules

Rule 26: FIRST Principles

PrincipleDescription
FastTests run quickly (milliseconds)
IndependentNo dependencies between tests
RepeatableSame results in any environment
Self-validatingPass/Fail is determined automatically
TimelyWritten just before/after production code

Rule 27: AAA Pattern (Arrange-Act-Assert)

describe('UserService', () => {
  describe('createUser', () => {
    it('should create user with valid data', async () => {
      // Arrange
      const userRepo = new InMemoryUserRepository();
      const service = new UserService(userRepo);
      const userData: CreateUserDTO = {
        name: 'Alice',
        email: 'alice@example.com',
      };

      // Act
      const result = await service.createUser(userData);

      // Assert
      expect(result.name).toBe('Alice');
      expect(result.email).toBe('alice@example.com');
      expect(result.id).toBeDefined();
    });
  });
});

Rule 28: One Assert Concept per Test

Bad:

it('should handle user lifecycle', async () => {
  const user = await service.createUser(data);
  expect(user.id).toBeDefined();
  const fetched = await service.getUser(user.id);
  expect(fetched.name).toBe(data.name);
  await service.updateUser(user.id, { name: 'Bob' });
  const updated = await service.getUser(user.id);
  expect(updated.name).toBe('Bob');
  await service.deleteUser(user.id);
  await expect(service.getUser(user.id)).rejects.toThrow();
});

Good:

describe('UserService', () => {
  it('should create user and return with generated id', async () => {
    const user = await service.createUser(validUserData);
    expect(user.id).toBeDefined();
    expect(user.name).toBe(validUserData.name);
  });

  it('should retrieve existing user by id', async () => {
    const created = await service.createUser(validUserData);
    const fetched = await service.getUser(created.id);
    expect(fetched).toEqual(created);
  });

  it('should update user name', async () => {
    const user = await service.createUser(validUserData);
    await service.updateUser(user.id, { name: 'Bob' });
    const updated = await service.getUser(user.id);
    expect(updated.name).toBe('Bob');
  });

  it('should throw NotFoundError when getting deleted user', async () => {
    const user = await service.createUser(validUserData);
    await service.deleteUser(user.id);
    await expect(service.getUser(user.id)).rejects.toThrow(NotFoundError);
  });
});

Rule 29: Write Readable Tests

Bad:

it('test1', async () => {
  const r = await s.calc({ a: 100, b: 2, c: 0.1, d: true });
  expect(r).toBe(180);
});

Good:

it('should apply 10% discount when premium member orders 2 items at 100 each', async () => {
  // Given
  const order = createOrder({
    items: [{ price: 100, quantity: 2 }],
    isPremiumMember: true,
    discountRate: 0.1,
  });

  // When
  const totalPrice = await pricingService.calculateTotal(order);

  // Then
  expect(totalPrice).toBe(180); // (100 * 2) - 10% discount = 180
});

Rule 30: Test Names Should Describe Scenarios

Naming pattern: should [expected result] when [condition]

describe('PasswordValidator', () => {
  it('should return valid when password has 8+ chars with mixed case and numbers', () => {});
  it('should return invalid when password is shorter than 8 characters', () => {});
  it('should return invalid when password lacks uppercase letters', () => {});
  it('should throw EmptyPasswordError when password is empty string', () => {});
});

Code Smells Catalog

Representative code smells from Martin Fowler's Refactoring and their solutions:

Code SmellSymptomSolution
Long MethodFunction over 50 linesExtract Method
Large ClassClass has 3+ responsibilitiesExtract Class
Primitive ObsessionDomain expressed only with primitivesValue Object
Long Parameter List4+ parametersParameter Object
Data ClumpsSame data group repeatedExtract Class
Feature EnvyExcessive use of another class's dataMove Method
Shotgun SurgeryOne change affects many classesMove Method, Inline Class
Divergent ChangeOne class changes for multiple reasonsExtract Class
CommentsExcessive code-explaining commentsRename, Extract Method
Dead CodeUnused codeDelete it
Speculative GeneralityOver-abstraction for the futureCollapse Hierarchy
Duplicated CodeSame code in 2+ placesExtract Method

Refactoring Techniques: Before/After

Technique 1: Guard Clauses to Remove Nesting

Before:

function getPaymentAmount(employee: Employee): number {
  let result: number;
  if (employee.isSeparated) {
    result = separatedAmount();
  } else {
    if (employee.isRetired) {
      result = retiredAmount();
    } else {
      result = normalPayAmount();
    }
  }
  return result;
}

After:

function getPaymentAmount(employee: Employee): number {
  if (employee.isSeparated) return separatedAmount();
  if (employee.isRetired) return retiredAmount();
  return normalPayAmount();
}

Technique 2: Strategy Pattern to Eliminate Conditionals

Before:

function calculateShippingCost(order: Order): number {
  if (order.shippingMethod === 'standard') {
    return order.weight * 1.5;
  } else if (order.shippingMethod === 'express') {
    return order.weight * 3.0 + 5;
  } else if (order.shippingMethod === 'overnight') {
    return order.weight * 5.0 + 15;
  } else if (order.shippingMethod === 'international') {
    return order.weight * 10.0 + 25;
  }
  throw new Error('Unknown shipping method');
}

After:

interface ShippingStrategy {
  calculate(weight: number): number;
}

const shippingStrategies: Record<string, ShippingStrategy> = {
  standard: { calculate: (w) => w * 1.5 },
  express: { calculate: (w) => w * 3.0 + 5 },
  overnight: { calculate: (w) => w * 5.0 + 15 },
  international: { calculate: (w) => w * 10.0 + 25 },
};

function calculateShippingCost(order: Order): number {
  const strategy = shippingStrategies[order.shippingMethod];
  if (!strategy) throw new UnknownShippingMethodError(order.shippingMethod);
  return strategy.calculate(order.weight);
}

Technique 3: Pipeline Refactoring from Loops

Before:

function getActiveAdminEmails(users: User[]): string[] {
  const result: string[] = [];
  for (let i = 0; i < users.length; i++) {
    if (users[i].isActive) {
      if (users[i].role === 'admin') {
        result.push(users[i].email.toLowerCase());
      }
    }
  }
  result.sort();
  return result;
}

After:

function getActiveAdminEmails(users: User[]): string[] {
  return users
    .filter(user => user.isActive)
    .filter(user => user.role === 'admin')
    .map(user => user.email.toLowerCase())
    .sort();
}

Cognitive Complexity

Cognitive complexity, as measured by SonarQube, quantifies the mental effort required to understand code.

Factors That Increase Cognitive Complexity

FactorIncrementExample
if, else if, else+1Branching
switch case+1Branching
for, while, do-while+1Looping
catch+1Exception handling
break/continue to label+1Flow change
Nesting+1 per levelNested if/for
Logical operator chain change+1a && b || c
Recursive call+1Self-reference
LevelCognitive ComplexityAction
Good0-5Maintain
Warning6-10Review for refactoring
Danger11-15Must refactor
Critical16+Refactor immediately

SOLID Principles Quick Reference

S — Single Responsibility Principle

// Bad: One class, multiple responsibilities
class UserService {
  createUser(data: CreateUserDTO) {}
  sendEmail(to: string, body: string) {}
  generatePDF(user: User) {}
  logAction(action: string) {}
}

// Good: One class, one responsibility
class UserService { createUser(data: CreateUserDTO) {} }
class EmailService { send(to: string, body: string) {} }
class PDFGenerator { generate(user: User) {} }
class AuditLogger { log(action: string) {} }

O — Open/Closed Principle

interface PaymentProcessor {
  process(amount: number): Promise<PaymentResult>;
}

class StripeProcessor implements PaymentProcessor {
  async process(amount: number) { /* Stripe API */ }
}

class PayPalProcessor implements PaymentProcessor {
  async process(amount: number) { /* PayPal API */ }
}

// Adding a new payment method requires no changes to existing code
class CryptoProcessor implements PaymentProcessor {
  async process(amount: number) { /* Crypto API */ }
}

L — Liskov Substitution Principle

// Bad: Penguin cannot fly, so it shouldn't extend Bird
class Bird { fly(): void { /* flies */ } }
class Penguin extends Bird {
  fly(): void { throw new Error('Cannot fly!'); }
}

// Good: Separate interfaces
interface FlyingBird { fly(): void; }
interface SwimmingBird { swim(): void; }

class Eagle implements FlyingBird { fly() {} }
class Penguin implements SwimmingBird { swim() {} }

I — Interface Segregation Principle

// Bad: Fat interface
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  attendMeeting(): void;
}

// Good: Segregated interfaces
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Meetable { attendMeeting(): void; }

class Developer implements Workable, Feedable, Meetable {
  work() {} eat() {} attendMeeting() {}
}

class Robot implements Workable {
  work() {}
  // No need for eat() or attendMeeting()
}

D — Dependency Inversion Principle

// Bad: High-level module depends on low-level module
class OrderService {
  private mysqlDb = new MySQLDatabase();
  save(order: Order) { this.mysqlDb.insert('orders', order); }
}

// Good: Depend on abstractions
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

class OrderService {
  constructor(private readonly repo: OrderRepository) {}
  async save(order: Order) { await this.repo.save(order); }
}

class MySQLOrderRepository implements OrderRepository { /* ... */ }
class MongoOrderRepository implements OrderRepository { /* ... */ }
class InMemoryOrderRepository implements OrderRepository { /* ... */ }

Code Review Checklist

## Naming
- [ ] Do names reveal intent?
- [ ] No abbreviations?
- [ ] Follows team naming conventions?

## Functions
- [ ] Does each function do one thing?
- [ ] 3 or fewer parameters?
- [ ] No side effects?

## Error Handling
- [ ] Uses appropriate custom errors?
- [ ] Errors are not swallowed?
- [ ] Guard clauses for fail-fast?

## Tests
- [ ] Tests exist for new features?
- [ ] Edge cases covered?
- [ ] Test names describe scenarios?

## Code Structure
- [ ] File under 400 lines?
- [ ] Nesting depth 3 or fewer?
- [ ] DRY principle followed?

## Security
- [ ] User input validated?
- [ ] No SQL injection or XSS risks?
- [ ] No sensitive info in logs?

Practical Quiz

Quiz 1: What are the problems with this code?
function proc(lst: any[], flg: boolean): any[] {
  let res: any[] = [];
  for (let i = 0; i < lst.length; i++) {
    if (flg) {
      if (lst[i].a > 0) {
        res.push(lst[i].a * 2);
      }
    } else {
      if (lst[i].b !== null) {
        res.push(lst[i].b);
      }
    }
  }
  return res;
}

Problems:

  1. Naming: proc, lst, flg, res, a, b are all meaningless
  2. Types: any overuse
  3. Responsibility: A flag parameter signals two functions are needed
  4. Nesting depth: 3 levels deep
  5. Imperative loop: Can be replaced with filter/map pipeline

Refactored:

function getDoubledPositiveAmounts(transactions: Transaction[]): number[] {
  return transactions
    .filter(t => t.amount > 0)
    .map(t => t.amount * 2);
}

function getNonNullBalances(accounts: Account[]): number[] {
  return accounts
    .filter(a => a.balance !== null)
    .map(a => a.balance);
}
Quiz 2: Improve this error handling code.
async function getData(id: string) {
  try {
    const res = await fetch(`/api/data/${id}`);
    const data = await res.json();
    return data;
  } catch (e) {
    console.log(e);
    return null;
  }
}

Problems:

  1. Swallows all errors
  2. Poor logging with console.log
  3. Does not check HTTP status codes
  4. Return type is ambiguous (nullable)

Refactored:

class ApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly endpoint: string,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchDataById(id: string): Promise<UserData> {
  const endpoint = `/api/data/${id}`;
  const response = await fetch(endpoint);

  if (!response.ok) {
    throw new ApiError(
      `Failed to fetch data: ${response.statusText}`,
      response.status,
      endpoint,
    );
  }

  return response.json() as Promise<UserData>;
}
Quiz 3: Find the SOLID violations in this class.
class ReportGenerator {
  generateReport(data: any[]): string {
    if (!data.length) throw new Error('No data');
    let html = '<table>';
    for (const row of data) {
      html += `<tr><td>${row.name}</td><td>${row.value}</td></tr>`;
    }
    html += '</table>';
    fs.writeFileSync('/tmp/report.html', html);
    sendEmail('admin@company.com', 'Report', html);
    return html;
  }
}

Violations:

  • SRP: Validation, HTML generation, file saving, email sending — 4 responsibilities
  • OCP: Adding PDF format requires class modification
  • DIP: Direct dependency on fs and sendEmail

Refactored:

interface ReportFormatter { format(data: ReportData[]): string; }
interface ReportExporter { export(content: string): Promise<void>; }

class HtmlFormatter implements ReportFormatter {
  format(data: ReportData[]): string { /* ... */ }
}

class ReportService {
  constructor(
    private formatter: ReportFormatter,
    private exporters: ReportExporter[],
  ) {}

  async generate(data: ReportData[]): Promise<string> {
    if (!data.length) throw new EmptyDataError();
    const content = this.formatter.format(data);
    await Promise.all(this.exporters.map(e => e.export(content)));
    return content;
  }
}
Quiz 4: Calculate the cognitive complexity of this function.
function calculatePrice(product: Product, user: User): number {
  let price = product.basePrice;

  if (user.isPremium) {                         // +1
    if (product.category === 'electronics') {   // +2 (nesting)
      price *= 0.8;
    } else {                                    // +1
      price *= 0.9;
    }
  } else {                                      // +1
    if (user.hasCoupon) {                       // +2 (nesting)
      price *= 0.95;
    }
  }

  if (product.isOnSale && user.isPremium) {     // +1
    price *= 0.9;
  }

  return price;
}

Cognitive Complexity: 8 (warning level, consider refactoring)

Refactored:

function calculatePrice(product: Product, user: User): number {
  const basePrice = product.basePrice;
  const memberDiscount = getMemberDiscount(user, product.category);
  const couponDiscount = getCouponDiscount(user);
  const saleDiscount = getSaleDiscount(product, user);

  return basePrice * memberDiscount * couponDiscount * saleDiscount;
}

function getMemberDiscount(user: User, category: string): number {
  if (!user.isPremium) return 1;
  return category === 'electronics' ? 0.8 : 0.9;
}

function getCouponDiscount(user: User): number {
  return !user.isPremium && user.hasCoupon ? 0.95 : 1;
}

function getSaleDiscount(product: Product, user: User): number {
  return product.isOnSale && user.isPremium ? 0.9 : 1;
}
Quiz 5: Improve this test code.
test('test1', () => {
  const s = new CalcService();
  expect(s.add(1, 2)).toBe(3);
  expect(s.add(-1, 1)).toBe(0);
  expect(s.subtract(5, 3)).toBe(2);
  expect(s.multiply(3, 4)).toBe(12);
  expect(s.divide(10, 2)).toBe(5);
  expect(() => s.divide(1, 0)).toThrow();
});

Refactored:

describe('CalcService', () => {
  const calculator = new CalcService();

  describe('add', () => {
    it('should return sum of two positive numbers', () => {
      expect(calculator.add(1, 2)).toBe(3);
    });

    it('should handle negative numbers', () => {
      expect(calculator.add(-1, 1)).toBe(0);
    });
  });

  describe('divide', () => {
    it('should return quotient of two numbers', () => {
      expect(calculator.divide(10, 2)).toBe(5);
    });

    it('should throw DivisionByZeroError when divisor is zero', () => {
      expect(() => calculator.divide(1, 0)).toThrow(DivisionByZeroError);
    });
  });
});

Tools and Automation

ESLint + Prettier Configuration

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "rules": {
    "max-lines": ["warn", { "max": 400 }],
    "max-lines-per-function": ["warn", { "max": 50 }],
    "max-params": ["warn", { "max": 3 }],
    "max-depth": ["warn", { "max": 3 }],
    "complexity": ["warn", { "max": 10 }],
    "no-else-return": "error",
    "prefer-const": "error",
    "no-var": "error"
  }
}

SonarQube Quality Gate

sonar.qualitygate.conditions:
  - metric: cognitive_complexity
    operator: GT
    error: 15
  - metric: duplicated_lines_density
    operator: GT
    error: 3
  - metric: coverage
    operator: LT
    error: 80

References

  1. Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship" (2008)
  2. Martin Fowler, "Refactoring: Improving the Design of Existing Code" (2018, 2nd Edition)
  3. G. Ann Campbell, "Cognitive Complexity: A new way of measuring understandability" — SonarSource (2023)
  4. Google Engineering Practices — Code Review Guidelines
  5. Airbnb JavaScript Style Guide
  6. TypeScript Deep Dive — Style Guide
  7. PEP 8 — Python Style Guide
  8. "The Art of Readable Code" by Dustin Boswell and Trevor Foucher (2011)
  9. SonarQube Documentation — Cognitive Complexity
  10. ESLint Rules Reference
  11. Kent Beck, "Test Driven Development: By Example" (2002)
  12. Michael Feathers, "Working Effectively with Legacy Code" (2004)
  13. "A Philosophy of Software Design" by John Ousterhout (2018)
  14. Clean Code JavaScript — GitHub (ryanmcdermott/clean-code-javascript)