Home / Notebooks / Software Engineering
Software Engineering
intermediate

Clean Code Principles

Essential principles and practices for writing clean, maintainable code

March 10, 2024
Updated regularly

Clean Code Principles

Quick reference guide for writing clean, maintainable code.

What is Clean Code?

Clean code is code that is easy to understand and easy to change:

  • Readable: Clear intent and logic
  • Simple: Does one thing well
  • Tested: Verified to work correctly
  • Maintainable: Easy to modify and extend
  • Efficient: Performs well without premature optimization
  • > "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." - Martin Fowler

    Meaningful Names

    Use intention-revealing names that explain what something does:

    Bad Examples

    // ❌ BAD: Unclear, abbreviated, meaningless
    let d; // elapsed time in days
    let list1;
    function getData() {}
    const a = 5;
    let temp;
    

    Good Examples

    // ✅ GOOD: Clear, descriptive, purposeful
    let elapsedTimeInDays;
    let activeUsers;
    function getUserProfile() {}
    const MAX_LOGIN_ATTEMPTS = 5;
    let userTemporaryPassword;
    

    Naming Rules

    // ========== Use Pronounceable Names ==========
    // ❌ BAD
    const yyyymmdstr = moment().format("YYYY/MM/DD");
    
    // ✅ GOOD
    const currentDate = moment().format("YYYY/MM/DD");
    
    // ========== Use Searchable Names ==========
    // ❌ BAD
    setTimeout(doSomething, 86400000); // What is 86400000?
    
    // ✅ GOOD
    const MILLISECONDS_IN_A_DAY = 86400000;
    setTimeout(doSomething, MILLISECONDS_IN_A_DAY);
    
    // ========== Avoid Mental Mapping ==========
    // ❌ BAD
    locations.forEach((l) => {
      doStuff();
      doSomeOtherStuff();
      // Wait, what is `l` again?
      dispatch(l);
    });
    
    // ✅ GOOD
    locations.forEach((location) => {
      doStuff();
      doSomeOtherStuff();
      dispatch(location);
    });
    

    Functions

    Functions should be small and do one thing:

    Function Size

    // ❌ BAD: Too long, does multiple things
    function processUser(user) {
      // Validate user
      if (!user.email) return false;
      if (!user.name) return false;
      
      // Save to database
      const result = db.save(user);
      
      // Send email
      sendEmail(user.email, "Welcome!");
      
      // Log activity
      logger.info("User processed");
      
      // Update cache
      cache.set(user.id, user);
      
      return result;
    }
    
    // ✅ GOOD: Small, single responsibility
    function validateUser(user) {
      return user.email && user.name;
    }
    
    function saveUser(user) {
      return db.save(user);
    }
    
    function notifyUser(user) {
      sendEmail(user.email, "Welcome!");
    }
    
    function logUserActivity(action) {
      logger.info(`User ${action}`);
    }
    
    function processUser(user) {
      if (!validateUser(user)) return false;
      
      const result = saveUser(user);
      notifyUser(user);
      logUserActivity("processed");
      
      return result;
    }
    

    Function Arguments

    // ❌ BAD: Too many arguments
    function createUser(name, email, age, country, city, phone, address) {
      // ...
    }
    
    // ✅ GOOD: Use object parameter
    function createUser(userDetails) {
      const { name, email, age, country, city, phone, address } = userDetails;
      // ...
    }
    
    // ========== Avoid Flag Arguments ==========
    // ❌ BAD
    function createFile(name, isPublic) {
      if (isPublic) {
        // create public file
      } else {
        // create private file
      }
    }
    
    // ✅ GOOD
    function createPublicFile(name) {
      // create public file
    }
    
    function createPrivateFile(name) {
      // create private file
    }
    

    Descriptive Function Names

    // ❌ BAD: Vague names
    function process() {}
    function handle() {}
    function do() {}
    
    // ✅ GOOD: Clear action and purpose
    function processPayment() {}
    function handleUserLogin() {}
    function validateEmailFormat() {}
    function calculateTotalPrice() {}
    function sendWelcomeEmail() {}
    

    Don't Repeat Yourself (DRY)

    Eliminate duplication by extracting common code:

    // ❌ BAD: Duplicated code
    function calculateDiscountForStudent(price) {
      const discount = price * 0.1;
      return price - discount;
    }
    
    function calculateDiscountForSenior(price) {
      const discount = price * 0.15;
      return price - discount;
    }
    
    function calculateDiscountForMilitary(price) {
      const discount = price * 0.2;
      return price - discount;
    }
    
    // ✅ GOOD: Reusable function
    function calculateDiscount(price, discountRate) {
      const discount = price * discountRate;
      return price - discount;
    }
    
    const DISCOUNT_RATES = {
      STUDENT: 0.1,
      SENIOR: 0.15,
      MILITARY: 0.2
    };
    
    calculateDiscount(100, DISCOUNT_RATES.STUDENT);
    calculateDiscount(100, DISCOUNT_RATES.SENIOR);
    

    Comments

    Code should be self-explanatory. Use comments only when necessary:

    Bad Comments

    // ❌ BAD: Obvious, redundant comments
    // Get the user
    const user = getUser();
    
    // Loop through items
    items.forEach(item => {
      // Process the item
      process(item);
    });
    
    // Increment i
    i++;
    

    Good Comments

    // ✅ GOOD: Explain WHY, not WHAT
    // Using regex instead of string methods for 10x performance improvement
    const isValid = /^[a-z0-9]+$/i.test(input);
    
    // Workaround for IE11 bug where Date.now() returns incorrect value
    const timestamp = Date.now() || new Date().getTime();
    
    // TODO: Refactor this when API v2 is ready
    // WARNING: This operation is expensive, consider caching
    // HACK: Temporary fix for legacy system, remove after migration
    

    Self-Documenting Code

    // ❌ BAD: Needs comment to explain
    // Check if user has permission
    if (user.role === 'admin' || user.role === 'moderator') {
      // Allow access
    }
    
    // ✅ GOOD: Code explains itself
    function hasModeratorPermission(user) {
      const moderatorRoles = ['admin', 'moderator'];
      return moderatorRoles.includes(user.role);
    }
    
    if (hasModeratorPermission(user)) {
      // Allow access
    }
    

    Error Handling

    Handle errors gracefully and consistently:

    // ❌ BAD: Silent failures
    function getUser(id) {
      try {
        return db.findUser(id);
      } catch (error) {
        return null; // What went wrong?
      }
    }
    
    // ✅ GOOD: Descriptive error handling
    function getUser(id) {
      try {
        return db.findUser(id);
      } catch (error) {
        logger.error(`Failed to fetch user ${id}:`, error);
        throw new UserNotFoundError(`User with ID ${id} not found`);
      }
    }
    
    // ========== Use Custom Errors ==========
    class ValidationError extends Error {
      constructor(message) {
        super(message);
        this.name = 'ValidationError';
      }
    }
    
    class DatabaseError extends Error {
      constructor(message) {
        super(message);
        this.name = 'DatabaseError';
      }
    }
    
    // ========== Handle Different Error Types ==========
    try {
      processPayment(order);
    } catch (error) {
      if (error instanceof ValidationError) {
        return res.status(400).json({ error: error.message });
      }
      if (error instanceof DatabaseError) {
        return res.status(500).json({ error: 'Database error occurred' });
      }
      throw error; // Unexpected error, rethrow
    }
    

    Code Organization

    Organize code logically and consistently:

    File Structure

    src/
    ├── components/          # UI components
    │   ├── Button/
    │   │   ├── Button.tsx
    │   │   ├── Button.test.tsx
    │   │   └── index.ts
    │   └── Card/
    ├── services/            # Business logic
    │   ├── userService.ts
    │   └── paymentService.ts
    ├── utils/               # Utility functions
    │   ├── formatters.ts
    │   └── validators.ts
    ├── types/               # Type definitions
    └── constants/           # App constants
    

    Code Grouping

    // ✅ GOOD: Group related code together
    class UserService {
      // Constants
      private readonly MAX_ATTEMPTS = 3;
      private readonly CACHE_TTL = 3600;
      
      // Constructor
      constructor(private db: Database) {}
      
      // Public methods
      async getUser(id: string) {
        return this.fetchUserFromCache(id) || this.fetchUserFromDB(id);
      }
      
      async createUser(data: UserData) {
        this.validateUserData(data);
        return this.saveUser(data);
      }
      
      // Private helper methods
      private async fetchUserFromCache(id: string) {
        // ...
      }
      
      private async fetchUserFromDB(id: string) {
        // ...
      }
      
      private validateUserData(data: UserData) {
        // ...
      }
    }
    

    SOLID Principles

    Single Responsibility Principle (SRP)

    A class should have one reason to change:

    // ❌ BAD: Multiple responsibilities
    class User {
      constructor(name, email) {
        this.name = name;
        this.email = email;
      }
      
      save() {
        // Database logic
        db.save(this);
      }
      
      sendEmail(message) {
        // Email logic
        emailService.send(this.email, message);
      }
      
      generateReport() {
        // Reporting logic
        return `Report for ${this.name}`;
      }
    }
    
    // ✅ GOOD: Single responsibility
    class User {
      constructor(name, email) {
        this.name = name;
        this.email = email;
      }
    }
    
    class UserRepository {
      save(user) {
        db.save(user);
      }
    }
    
    class EmailService {
      sendToUser(user, message) {
        emailService.send(user.email, message);
      }
    }
    
    class ReportGenerator {
      generateUserReport(user) {
        return `Report for ${user.name}`;
      }
    }
    

    Open/Closed Principle (OCP)

    Open for extension, closed for modification:

    // ❌ BAD: Need to modify for new types
    class PaymentProcessor {
      process(payment, type) {
        if (type === 'credit') {
          // Process credit card
        } else if (type === 'paypal') {
          // Process PayPal
        } else if (type === 'bitcoin') {
          // Process Bitcoin
        }
      }
    }
    
    // ✅ GOOD: Extend without modifying
    class PaymentProcessor {
      constructor(private paymentMethod: PaymentMethod) {}
      
      process(payment) {
        this.paymentMethod.processPayment(payment);
      }
    }
    
    class CreditCardPayment implements PaymentMethod {
      processPayment(payment) {
        // Process credit card
      }
    }
    
    class PayPalPayment implements PaymentMethod {
      processPayment(payment) {
        // Process PayPal
      }
    }
    

    Dependency Inversion Principle (DIP)

    Depend on abstractions, not concretions:

    // ❌ BAD: Direct dependency on concrete class
    class UserService {
      constructor() {
        this.database = new MySQLDatabase(); // Tightly coupled
      }
      
      getUser(id) {
        return this.database.find(id);
      }
    }
    
    // ✅ GOOD: Depend on interface/abstraction
    class UserService {
      constructor(private database: Database) {} // Injected dependency
      
      getUser(id) {
        return this.database.find(id);
      }
    }
    
    // Can easily swap implementations
    const mysqlDb = new MySQLDatabase();
    const mongoDb = new MongoDatabase();
    const userService = new UserService(mysqlDb); // or mongoDb
    

    Testing

    Write testable code with clear test cases:

    // ✅ GOOD: Small, testable functions
    function calculateTotalPrice(items, discountRate = 0) {
      const subtotal = items.reduce((sum, item) => sum + item.price, 0);
      const discount = subtotal * discountRate;
      return subtotal - discount;
    }
    
    // Test cases
    describe('calculateTotalPrice', () => {
      it('should calculate total without discount', () => {
        const items = [{ price: 10 }, { price: 20 }];
        expect(calculateTotalPrice(items)).toBe(30);
      });
      
      it('should apply discount correctly', () => {
        const items = [{ price: 100 }];
        expect(calculateTotalPrice(items, 0.1)).toBe(90);
      });
      
      it('should handle empty items', () => {
        expect(calculateTotalPrice([])).toBe(0);
      });
    });
    

    Code Smells to Avoid

    Long Methods

    // ❌ BAD: Method doing too much
    function processOrder(order) {
      // 100+ lines of code
    }
    
    // ✅ GOOD: Break into smaller functions
    function processOrder(order) {
      validateOrder(order);
      calculateTotal(order);
      applyDiscount(order);
      processPayment(order);
      sendConfirmation(order);
    }
    

    Magic Numbers

    // ❌ BAD: Unexplained numbers
    if (user.age > 18 && user.status === 2) {
      // ...
    }
    
    // ✅ GOOD: Named constants
    const LEGAL_AGE = 18;
    const STATUS_ACTIVE = 2;
    
    if (user.age > LEGAL_AGE && user.status === STATUS_ACTIVE) {
      // ...
    }
    

    Deep Nesting

    // ❌ BAD: Deeply nested
    function processUser(user) {
      if (user) {
        if (user.isActive) {
          if (user.hasPermission) {
            if (user.email) {
              // Do something
            }
          }
        }
      }
    }
    
    // ✅ GOOD: Early returns
    function processUser(user) {
      if (!user) return;
      if (!user.isActive) return;
      if (!user.hasPermission) return;
      if (!user.email) return;
      
      // Do something
    }
    

    Best Practices

    Consistent Formatting

    // ✅ GOOD: Consistent style
    const users = [
      { name: 'Alice', age: 25 },
      { name: 'Bob', age: 30 },
      { name: 'Charlie', age: 35 },
    ];
    
    function getActiveUsers(users) {
      return users.filter(user => user.isActive);
    }
    

    Meaningful Variable Names

    // ❌ BAD
    const x = users.filter(u => u.a > 18);
    
    // ✅ GOOD
    const adultUsers = users.filter(user => user.age > 18);
    

    Pure Functions

    // ❌ BAD: Has side effects
    let total = 0;
    function addToTotal(value) {
      total += value; // Modifies external state
    }
    
    // ✅ GOOD: Pure function
    function calculateTotal(currentTotal, value) {
      return currentTotal + value; // No side effects
    }
    

    Use Modern Language Features

    // ❌ BAD: Old style
    var result = [];
    for (var i = 0; i < items.length; i++) {
      if (items[i].active) {
        result.push(items[i].name);
      }
    }
    
    // ✅ GOOD: Modern approach
    const result = items
      .filter(item => item.active)
      .map(item => item.name);
    

    Tips

  • Write code for humans first, computers second
  • Refactor frequently to keep code clean
  • Test your code to ensure it works as expected
  • Use meaningful names for variables, functions, and classes
  • Keep functions small and focused on one task
  • Avoid premature optimization - make it work first, then optimize
  • Delete dead code instead of commenting it out
  • Be consistent with your team's style guide
  • Review your own code before asking others to review it
  • Continuous improvement - always look for ways to improve
  • Resources

  • Clean Code by Robert C. Martin
  • Refactoring by Martin Fowler
  • Code Complete by Steve McConnell
  • The Pragmatic Programmer
  • Clean Code JavaScript
  • Topics

    Clean CodeBest PracticesSoftware EngineeringCode Quality

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.