DevToolBoxGRATIS
Blogg

Clean Code Guide: Naming Conventions, SOLID Principles, Code Smells, Refactoring & Best Practices

24 min readby DevToolBox Team

Clean Code: Principles, Practices and Patterns for Writing Maintainable Software

Master clean code principles: naming conventions, function design (SRP, small functions), code smells and refactoring, SOLID principles, DRY/KISS/YAGNI, comments and documentation, error handling patterns, testing and code quality, code review best practices, design by contract, clean architecture layers, and practical refactoring examples with before/after code.

TL;DR — Clean Code Principles in 60 Seconds
  • Names are documentation: variables, functions, and classes should reveal intent and eliminate guessing
  • Functions should be small, do one thing, and operate at a single level of abstraction
  • SOLID principles are the foundation of OO design: SRP, OCP, LSP, ISP, DIP
  • DRY eliminates duplication, KISS fights over-engineering, YAGNI prevents speculative design
  • Code smells signal deeper design problems — identify and eliminate them through refactoring
  • Prefer exceptions over error codes for error handling, never return or pass null
Key Takeaways
  • The core of clean code is readability — the read-to-write ratio is roughly 10:1
  • Good naming eliminates the need for comments: if you need a comment, try renaming first
  • Each function should be a short story: a title (name), a plot (logic), and an ending (return value)
  • SOLID principles help create loosely coupled, highly cohesive systems
  • Refactoring is a continuous process, not a one-time event — follow the Boy Scout Rule
  • Code review is the most effective way for teams to improve code quality
  • Clean Architecture makes systems testable, framework-independent, and resilient to change

1. Naming Conventions: Self-Documenting Code

Naming is the most important and most underrated skill in programming. A good name eliminates the need for comments, while a bad name wastes time for everyone reading the code. Names should reveal intent, prevent disinformation, and be searchable.

Variable Naming Rules

// Bad: What does d mean? What are these numbers?
const d = 86400;
const x = users.filter(u => u.a > 18);
let flag = true;

// Good: Intention-revealing names
const SECONDS_PER_DAY = 86400;
const adultUsers = users.filter(user => user.age > 18);
let isEligibleForDiscount = true;

Function Naming Rules

// Bad: vague verb, unclear purpose
function process(data) { /* ... */ }
function handle(req) { /* ... */ }
function doStuff(items) { /* ... */ }

// Good: verb + noun, describes action
function calculateMonthlyRevenue(transactions) { /* ... */ }
function validateEmailAddress(email) { /* ... */ }
function sendWelcomeNotification(user) { /* ... */ }

Boolean & Class Naming

// Booleans should read as yes/no questions
const isActive = true;
const hasPermission = user.roles.includes("admin");
const canEditDocument = !doc.isLocked && hasPermission;
const shouldRetry = attempts < MAX_RETRIES;

// Classes: nouns that describe what they represent
class InvoiceGenerator { /* ... */ }    // Good
class UserRepository { /* ... */ }       // Good
class OrderValidator { /* ... */ }       // Good
class DataManager { /* ... */ }          // Bad: too generic
class Utility { /* ... */ }              // Bad: meaningless
RuleBadGood
Reveal intentd, temp, valelapsedDays, currentUser
Avoid disinformationaccountList (not a List)accounts, accountGroup
Searchable7, e, tMAX_RETRIES, error, task
Pronounceablegenymdhms, modymdhmsgenerationTimestamp
Consistentfetch/get/retrieve mixedget everywhere

2. Function Design: Small, Focused, Single Responsibility

Functions are the fundamental building blocks of code. Clean functions should be small (ideally under 20 lines), do exactly one thing, take few arguments, have no side effects, and operate at a single level of abstraction.

Before: A Function That Does Too Much

// Bad: This function validates, transforms, saves, AND sends email
function processOrder(orderData) {
  // Validation
  if (!orderData.items || orderData.items.length === 0) {
    throw new Error("No items");
  }
  if (!orderData.customer.email) {
    throw new Error("No email");
  }
  for (const item of orderData.items) {
    if (item.quantity <= 0) throw new Error("Bad quantity");
    if (item.price < 0) throw new Error("Bad price");
  }

  // Calculation
  let total = 0;
  for (const item of orderData.items) {
    total += item.price * item.quantity;
  }
  const tax = total * 0.1;
  const finalTotal = total + tax;

  // Save to database
  const order = db.orders.insert({
    ...orderData,
    total: finalTotal,
    tax,
    status: "pending",
    createdAt: new Date()
  });

  // Send email
  emailService.send({
    to: orderData.customer.email,
    subject: "Order Confirmation",
    body: "Your order total is " + finalTotal
  });

  return order;
}

After: Each Function Does One Thing

// Good: Orchestrator function at a high abstraction level
function processOrder(orderData) {
  validateOrder(orderData);
  const pricing = calculateOrderPricing(orderData.items);
  const order = saveOrder(orderData, pricing);
  sendOrderConfirmation(order);
  return order;
}

// Each function is small, focused, and testable
function validateOrder(orderData) {
  if (!orderData.items?.length) {
    throw new InvalidOrderError("Order must contain items");
  }
  if (!orderData.customer?.email) {
    throw new InvalidOrderError("Customer email required");
  }
  orderData.items.forEach(validateLineItem);
}

function validateLineItem(item) {
  if (item.quantity <= 0) throw new InvalidOrderError("Quantity must be positive");
  if (item.price < 0) throw new InvalidOrderError("Price cannot be negative");
}

function calculateOrderPricing(items) {
  const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  const tax = subtotal * TAX_RATE;
  return { subtotal, tax, total: subtotal + tax };
}

Function Arguments: Fewer Is Better

// Bad: Too many positional arguments
function createUser(name, email, age, role, department, isActive) {
  /* ... */
}
createUser("Alice", "a@b.com", 30, "admin", "eng", true);

// Good: Use an options object for 3+ parameters
function createUser({ name, email, age, role, department, isActive }) {
  /* ... */
}
createUser({
  name: "Alice",
  email: "a@b.com",
  age: 30,
  role: "admin",
  department: "eng",
  isActive: true,
});

3. Code Smells and Refactoring

Code smells, a concept coined by Martin Fowler, are surface-level indicators in code that hint at deeper design problems. Smells are not bugs themselves, but they increase the probability of bugs and the difficulty of maintenance. Identifying smells is the first step toward refactoring.

SmellSymptomRefactoring
Long Method>20 lines, deep nestingExtract Method
Large ClassMultiple unrelated responsibilitiesExtract Class
Duplicate CodeSimilar logic in multiple placesExtract Method / Template Method
Long Parameter ListMore than 3 parametersIntroduce Parameter Object
Feature EnvyMethod uses other class data excessivelyMove Method
Primitive ObsessionStrings/numbers instead of domain objectsReplace with Value Object
Switch StatementsLong switch/if-else chainsReplace with Polymorphism
Dead CodeCode that is never executedDelete it

Refactoring Example: Eliminating Switch Statements

// Before: Switch statement that will grow with each new type
function calculateShippingCost(order) {
  switch (order.shippingType) {
    case "standard":
      return order.weight * 1.5;
    case "express":
      return order.weight * 3.0 + 5;
    case "overnight":
      return order.weight * 5.0 + 15;
    default:
      throw new Error("Unknown shipping type");
  }
}

// After: Strategy pattern — open for extension, closed for modification
const shippingStrategies = {
  standard:  (weight) => weight * 1.5,
  express:   (weight) => weight * 3.0 + 5,
  overnight: (weight) => weight * 5.0 + 15,
};

function calculateShippingCost(order) {
  const strategy = shippingStrategies[order.shippingType];
  if (!strategy) {
    throw new UnknownShippingTypeError(order.shippingType);
  }
  return strategy(order.weight);
}

4. SOLID Principles Deep Dive

SOLID represents five core principles of object-oriented design, popularized by Robert C. Martin. These principles help create loosely coupled, highly cohesive, and extensible systems. Even in functional programming and modern JavaScript/TypeScript, the ideas behind SOLID remain relevant.

S — Single Responsibility Principle (SRP)

A class or module should have only one reason to change. If a class handles both business logic and database access, it has two reasons to change.

// Violates SRP: Report class does formatting AND data access
class Report {
  getData() { /* queries database */ }
  formatAsHTML() { /* builds HTML string */ }
  formatAsPDF() { /* generates PDF */ }
  sendEmail() { /* sends report via email */ }
}

// Follows SRP: Each class has one responsibility
class ReportDataProvider {
  getData() { /* queries database */ }
}

class HTMLReportFormatter {
  format(data) { /* builds HTML string */ }
}

class PDFReportFormatter {
  format(data) { /* generates PDF */ }
}

class ReportEmailSender {
  send(report, recipient) { /* sends report via email */ }
}

O — Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification. Adding new features should not require modifying existing, tested code.

// Violates OCP: Must modify this function for every new shape
function calculateArea(shape) {
  if (shape.type === "circle") return Math.PI * shape.radius ** 2;
  if (shape.type === "rectangle") return shape.width * shape.height;
  // Adding triangle requires modifying this function
}

// Follows OCP: New shapes extend without modifying existing code
interface Shape {
  area(): number;
}

class Circle implements Shape {
  constructor(private radius: number) {}
  area() { return Math.PI * this.radius ** 2; }
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}
  area() { return this.width * this.height; }
}

// Adding Triangle requires zero changes to existing code
class Triangle implements Shape {
  constructor(private base: number, private height: number) {}
  area() { return 0.5 * this.base * this.height; }
}

L — Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without breaking program correctness. The classic violation is Square extending Rectangle — setting width should not unexpectedly change height.

I — Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use. Prefer many specific, small interfaces over one large, general-purpose interface.

// Violates ISP: Printer interface forces all implementations
// to implement methods they may not support
interface Printer {
  print(doc: Document): void;
  scan(doc: Document): void;
  fax(doc: Document): void;
  staple(doc: Document): void;
}

// Follows ISP: Segregated interfaces
interface Printable {
  print(doc: Document): void;
}
interface Scannable {
  scan(doc: Document): void;
}
interface Faxable {
  fax(doc: Document): void;
}

// Simple printer only implements what it needs
class SimplePrinter implements Printable {
  print(doc: Document) { /* ... */ }
}

// Multi-function device implements multiple interfaces
class MultiFunctionDevice implements Printable, Scannable, Faxable {
  print(doc: Document) { /* ... */ }
  scan(doc: Document) { /* ... */ }
  fax(doc: Document) { /* ... */ }
}

D — Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

// Violates DIP: High-level OrderService depends on low-level MySQLDatabase
class OrderService {
  private db = new MySQLDatabase(); // tightly coupled
  save(order) { this.db.query("INSERT INTO ..."); }
}

// Follows DIP: Depend on abstraction, inject implementation
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

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

// Now you can swap implementations freely
class MySQLOrderRepo implements OrderRepository { /* ... */ }
class MongoOrderRepo implements OrderRepository { /* ... */ }
class InMemoryOrderRepo implements OrderRepository { /* ... */ } // for tests

5. DRY, KISS, and YAGNI

These three principles form the golden triangle of software development. DRY eliminates duplication, KISS fights complexity, and YAGNI prevents over-design. They complement each other, but balance is needed — excessive DRY can lead to unnecessary abstractions that violate KISS.

PrincipleMeaningViolation Signal
DRYEvery piece of knowledge in a single placeChanging one thing requires changes in many places
KISSChoose the simplest solution that worksTeam members struggle to understand the code
YAGNIDo not build until you actually need itUnused abstraction layers or features exist

DRY: Correct vs Excessive Application

// Good DRY: Extract shared validation logic
function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// Used in multiple places — single source of truth
function registerUser(data) { validateEmail(data.email); /* ... */ }
function updateProfile(data) { validateEmail(data.email); /* ... */ }
function inviteUser(email) { validateEmail(email); /* ... */ }

// Bad DRY (over-abstraction): These look similar but serve
// different purposes and will diverge over time
// DO NOT force them into one generic function
function formatUserForAdmin(user) {
  return { name: user.name, email: user.email, role: user.role };
}
function formatUserForPublic(user) {
  return { name: user.name, avatar: user.avatar };
}
// These should stay separate — they change for different reasons

6. Comments and Documentation

Good code should be self-explanatory. Comments should not explain what code does (that is the job of good naming), but why it does it. If you need a comment to explain what a piece of code does, consider renaming variables or extracting functions to make the code self-documenting first.

Good Comments vs Bad Comments

// BAD: Restating the code (noise comment)
// Increment counter by one
counter += 1;

// BAD: Journal comments (use git log instead)
// 2024-01-15 - Added feature X
// 2024-01-16 - Fixed bug in feature X

// BAD: Commented-out code (delete it, git has history)
// const oldValue = calculateLegacy(data);
// if (oldValue > threshold) { sendAlert(); }

// GOOD: Explain WHY, not WHAT
// We use a 30-second timeout because the payment gateway
// occasionally takes 20+ seconds during peak hours
const PAYMENT_TIMEOUT_MS = 30_000;

// GOOD: Warn about consequences
// WARNING: This cache is shared across all tenants.
// Flushing it will cause a ~5 second latency spike.
function flushGlobalCache() { /* ... */ }

// GOOD: Explain complex business rules
// Tax-exempt if: registered non-profit AND purchase
// is for educational purposes (IRS ruling 2023-47)
function isTaxExempt(org, purchase) { /* ... */ }

// GOOD: TODO with context and ownership
// TODO(@alice): Replace with streaming API once backend
// supports SSE (tracked in JIRA-1234, ETA: Q2 2026)

7. Error Handling Patterns

Error handling is the most frequently overlooked aspect of clean code. Good error handling should make code more robust, not more cluttered. Core principles: prefer exceptions over error codes, never return or pass null, and handle errors at the right level of abstraction.

Custom Exception Class Hierarchy

// Define a domain-specific error hierarchy
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    public readonly isOperational: boolean = true
  ) {
    super(message);
    this.name = this.constructor.name;
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(
      `\${resource} with id \${id} not found`,
      "RESOURCE_NOT_FOUND",
      404
    );
  }
}

class ValidationError extends AppError {
  constructor(public readonly fields: Record<string, string>) {
    super("Validation failed", "VALIDATION_ERROR", 400);
  }
}

// Usage: clear, specific, informative
throw new NotFoundError("User", userId);
throw new ValidationError({ email: "Invalid format", age: "Must be positive" });

Never Return Null

// Bad: Caller must always check for null
function findUser(id: string): User | null {
  return db.users.find(u => u.id === id) || null;
}
// Every call site needs: if (user === null) { ... }

// Better: Throw for "must exist" scenarios
function getUser(id: string): User {
  const user = db.users.find(u => u.id === id);
  if (!user) throw new NotFoundError("User", id);
  return user;
}

// Better: Return empty collection instead of null
function getOrdersByUser(userId: string): Order[] {
  return db.orders.filter(o => o.userId === userId); // [] not null
}

// Better: Use Optional/Result pattern for "may not exist"
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

function parseConfig(raw: string): Result<Config> {
  try {
    return { success: true, data: JSON.parse(raw) };
  } catch (e) {
    return { success: false, error: new Error("Invalid config") };
  }
}

8. Testing and Code Quality

Tests are the guardians of clean code. Refactoring without tests is reckless. Test code itself should also be clean — readable, maintainable, and following the AAA (Arrange-Act-Assert) pattern. Tests not only verify correctness but also serve as living documentation for the code.

AAA Pattern & Clean Tests

// Clean test: readable, focused, follows AAA
describe("OrderPricingService", () => {
  describe("calculateTotal", () => {
    it("applies percentage discount to subtotal", () => {
      // Arrange
      const items = [
        { name: "Widget", price: 100, quantity: 2 },
        { name: "Gadget", price: 50, quantity: 1 },
      ];
      const discount = { type: "percentage", value: 10 };

      // Act
      const total = calculateTotal(items, discount);

      // Assert
      expect(total).toBe(225); // (200 + 50) * 0.9
    });

    it("does not allow negative totals from over-discount", () => {
      const items = [{ name: "Widget", price: 10, quantity: 1 }];
      const discount = { type: "fixed", value: 50 };

      const total = calculateTotal(items, discount);

      expect(total).toBe(0); // Floor at zero
    });
  });
});

Test Naming Conventions

PatternExample
should...when..."should return 404 when user not found"
given...when...then..."given empty cart, when checkout, then throws"
verb + condition"rejects invalid email format"

Aim for 80% test coverage, but do not test trivial code just for numbers. Focus on business-critical paths, boundary conditions, and error handling. Every bug fix should be accompanied by a regression test that prevents recurrence.

9. Code Review Best Practices

Code review is the most effective technique for teams to improve code quality. Good reviews focus on design, readability, and correctness — not formatting issues (those should be handled automatically by linters and formatters).

Reviewer Should Focus OnReviewer Should Avoid
Design and architecture choicesIndentation and formatting (leave to tools)
Edge cases and error handlingPersonal style preferences
Naming and readabilityNitpicking minor issues
Test coverage and qualityRewriting others' code
Security and performance implicationsEmotional or personal comments

Reviewer Checklist

# Code Review Checklist

## Correctness
- [ ] Does the code do what it claims to do?
- [ ] Are edge cases handled (null, empty, negative, overflow)?
- [ ] Are race conditions possible in concurrent code?

## Design
- [ ] Does it follow SOLID principles?
- [ ] Is the abstraction level appropriate?
- [ ] Could this be simplified without losing functionality?

## Readability
- [ ] Are names intention-revealing?
- [ ] Can I understand this without asking the author?
- [ ] Are functions small and focused?

## Testing
- [ ] Are there tests for happy path AND failure cases?
- [ ] Do tests cover the new code adequately?
- [ ] Are tests readable and maintainable?

## Security
- [ ] Is user input validated and sanitized?
- [ ] Are secrets kept out of code?
- [ ] Are SQL/NoSQL injection risks mitigated?

10. Design by Contract

Design by Contract, introduced by Bertrand Meyer, clarifies the contract of functions and classes by defining preconditions (what must be true before calling), postconditions (what is guaranteed after calling), and invariants (what always remains true). This makes behavior predictable and catches bugs earlier.

// Design by Contract with runtime assertions
class BankAccount {
  private balance: number;

  constructor(initialBalance: number) {
    // Precondition
    assert(initialBalance >= 0, "Initial balance must be non-negative");
    this.balance = initialBalance;
    // Invariant established
    this.checkInvariant();
  }

  withdraw(amount: number): void {
    // Preconditions
    assert(amount > 0, "Withdrawal amount must be positive");
    assert(amount <= this.balance, "Insufficient funds");

    const previousBalance = this.balance;
    this.balance -= amount;

    // Postcondition
    assert(
      this.balance === previousBalance - amount,
      "Balance must decrease by exactly the withdrawal amount"
    );
    // Invariant preserved
    this.checkInvariant();
  }

  private checkInvariant(): void {
    assert(this.balance >= 0, "Invariant: balance must never be negative");
  }
}

function assert(condition: boolean, message: string): asserts condition {
  if (!condition) throw new ContractViolationError(message);
}

In TypeScript, the type system acts as a compile-time contract. Combined with libraries like Zod or io-ts for runtime validation, you can enforce contracts at system boundaries (API entry points, user input, external data).

// Zod: Runtime contracts at system boundaries
import { z } from "zod";

const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;

// At the API boundary, validate and parse
function createUserHandler(req: Request) {
  const input = CreateUserSchema.parse(req.body);
  // input is now typed AND validated
  // No null checks needed downstream
  return userService.create(input);
}

11. Clean Architecture Layers

Clean Architecture, proposed by Robert C. Martin, organizes systems into concentric layers. The core idea is the Dependency Rule: dependencies must point inward. Inner layers must not know about outer layers. This makes core business logic independent of frameworks, databases, and UI.

LayerContainsExample
EntitiesCore business rules, domain modelsOrder, User, Product
Use CasesApplication-specific business logicPlaceOrder, RegisterUser
Interface AdaptersControllers, presenters, gatewaysOrderController, UserPresenter
Frameworks & DriversWeb frameworks, databases, external APIsExpress, PostgreSQL, Stripe API

Project Structure Example

src/
  domain/                # Entities & business rules (innermost)
    entities/
      Order.ts           # Pure domain model, no dependencies
      User.ts
    value-objects/
      Money.ts
      EmailAddress.ts

  application/           # Use cases (depends only on domain)
    use-cases/
      PlaceOrder.ts      # Orchestrates domain logic
      RegisterUser.ts
    ports/               # Interfaces for external dependencies
      OrderRepository.ts # Interface, not implementation
      PaymentGateway.ts

  infrastructure/        # Adapters & frameworks (outermost)
    persistence/
      PostgresOrderRepo.ts  # Implements OrderRepository
    payment/
      StripeGateway.ts      # Implements PaymentGateway
    web/
      controllers/
        OrderController.ts  # HTTP -> Use Case
      middleware/
        auth.ts

Use Case Implementation Example

// application/use-cases/PlaceOrder.ts
// Depends ONLY on domain entities and port interfaces
import { Order } from "../domain/entities/Order";
import { OrderRepository } from "../ports/OrderRepository";
import { PaymentGateway } from "../ports/PaymentGateway";
import { NotificationService } from "../ports/NotificationService";

export class PlaceOrderUseCase {
  constructor(
    private orderRepo: OrderRepository,
    private payment: PaymentGateway,
    private notifications: NotificationService
  ) {}

  async execute(input: PlaceOrderInput): Promise<Order> {
    // 1. Create domain entity (validates business rules)
    const order = Order.create(input.items, input.customer);

    // 2. Process payment through port
    const payment = await this.payment.charge(
      order.total,
      input.paymentMethod
    );

    // 3. Persist through port
    order.confirmPayment(payment.id);
    await this.orderRepo.save(order);

    // 4. Notify through port
    await this.notifications.sendOrderConfirmation(order);

    return order;
  }
}

12. Practical Refactoring Examples

Here are three real-world refactoring scenarios showing how to gradually transform messy code into clean code. Each example includes before/after comparisons and the principles applied.

Example 1: Extract Early Returns (Guard Clauses)

// Before: Deep nesting, hard to follow
function getDiscount(customer) {
  let discount = 0;
  if (customer !== null) {
    if (customer.isActive) {
      if (customer.orders.length > 10) {
        if (customer.loyaltyYears >= 5) {
          discount = 0.25;
        } else {
          discount = 0.15;
        }
      } else {
        discount = 0.05;
      }
    }
  }
  return discount;
}

// After: Guard clauses eliminate nesting
function getDiscount(customer) {
  if (!customer || !customer.isActive) return 0;
  if (customer.orders.length <= 10) return 0.05;
  if (customer.loyaltyYears >= 5) return 0.25;
  return 0.15;
}

Example 2: Replace Magic Numbers and Primitive Obsession

// Before: Magic numbers, unclear intent
function calculatePrice(quantity, type) {
  if (type === 1) return quantity * 29.99;
  if (type === 2) return quantity * 49.99;
  if (type === 3) return quantity * 99.99 * 0.9;
  return 0;
}

// After: Named constants, value objects, clear semantics
const PricingTier = {
  BASIC:      { name: "Basic",      unitPrice: 29.99, discount: 0 },
  STANDARD:   { name: "Standard",   unitPrice: 49.99, discount: 0 },
  ENTERPRISE: { name: "Enterprise", unitPrice: 99.99, discount: 0.1 },
} as const;

type TierKey = keyof typeof PricingTier;

function calculatePrice(quantity: number, tier: TierKey): number {
  const { unitPrice, discount } = PricingTier[tier];
  const subtotal = quantity * unitPrice;
  return subtotal * (1 - discount);
}

Example 3: Replace Conditional Logic with Polymorphism

// Before: Growing if-else chain for notification types
function sendNotification(type, recipient, message) {
  if (type === "email") {
    const html = buildEmailHTML(message);
    smtpClient.send(recipient.email, html);
    logSentEmail(recipient, message);
  } else if (type === "sms") {
    const text = truncate(message, 160);
    twilioClient.send(recipient.phone, text);
    logSentSMS(recipient, message);
  } else if (type === "push") {
    const payload = { title: "New Message", body: message };
    firebaseClient.send(recipient.deviceToken, payload);
    logSentPush(recipient, message);
  }
  // Adding "slack" requires modifying this function
}

// After: Strategy pattern — each channel is independent
interface NotificationChannel {
  send(recipient: Recipient, message: string): Promise<void>;
}

class EmailChannel implements NotificationChannel {
  async send(recipient, message) {
    const html = buildEmailHTML(message);
    await smtpClient.send(recipient.email, html);
  }
}

class SMSChannel implements NotificationChannel {
  async send(recipient, message) {
    await twilioClient.send(recipient.phone, truncate(message, 160));
  }
}

// Adding Slack: zero changes to existing code
class SlackChannel implements NotificationChannel {
  async send(recipient, message) {
    await slackClient.postMessage(recipient.slackId, message);
  }
}

// Orchestrator is simple and stable
class NotificationService {
  constructor(private channels: Map<string, NotificationChannel>) {}
  async send(type: string, recipient: Recipient, message: string) {
    const channel = this.channels.get(type);
    if (!channel) throw new Error(`Unknown channel: \${type}`);
    await channel.send(recipient, message);
  }
}

Conclusion

Clean code is not a one-time achievement but a continuous discipline. Follow the Boy Scout Rule — always leave the code a little cleaner than you found it. Start with good naming, keep functions small and focused, apply SOLID and DRY/KISS/YAGNI principles, write meaningful tests, and elevate each other through code reviews.

Clean Architecture makes your system testable, swappable, and resilient to change. Design by Contract makes behavior predictable. Refactoring is safe — as long as you have test coverage. Most importantly, clean code is respect for your team: every line you write will be read dozens of times by others, including your future self.

Take action now: pick the messiest file in your codebase and apply one principle from this guide to refactor it. Then repeat. That is the path to clean code.

Frequently Asked Questions

What is clean code and why does it matter?

Clean code is code that is easy to read, understand, and modify. It matters because developers spend far more time reading code than writing it — studies suggest the ratio is 10:1. Clean code reduces bugs, lowers onboarding time for new team members, decreases maintenance costs, and makes refactoring safer. Writing clean code is a professional discipline that pays dividends throughout the lifetime of a software project.

What are the SOLID principles in software design?

SOLID is an acronym for five design principles: Single Responsibility Principle (a class should have one reason to change), Open/Closed Principle (open for extension, closed for modification), Liskov Substitution Principle (subtypes must be substitutable for their base types), Interface Segregation Principle (prefer many specific interfaces over one general interface), and Dependency Inversion Principle (depend on abstractions, not concrete implementations). Together they promote loosely coupled, highly cohesive, and maintainable code.

What is the difference between DRY, KISS, and YAGNI?

DRY (Don't Repeat Yourself) means every piece of knowledge should have a single authoritative representation — avoid duplicating logic. KISS (Keep It Simple, Stupid) means prefer the simplest solution that works — avoid unnecessary complexity. YAGNI (You Aren't Gonna Need It) means don't build features or abstractions until you actually need them. These three principles complement each other: DRY eliminates duplication, KISS fights over-engineering, and YAGNI prevents speculative design.

How do I identify code smells and when should I refactor?

Code smells are symptoms of deeper design problems. Common smells include: long methods (over 20 lines), large classes, duplicate code, long parameter lists (more than 3 parameters), feature envy (a method using another class's data more than its own), and primitive obsession (overusing primitives instead of domain objects). Refactor when you encounter a smell during development (Boy Scout Rule), before adding new features to affected code, or when bugs cluster in a particular module.

What are good naming conventions for variables, functions, and classes?

Good names are intention-revealing, pronounceable, and searchable. Variables should describe what they hold (userAge, not x). Functions should describe what they do using verb phrases (calculateTotalPrice, not process). Classes should be nouns describing what they represent (InvoiceGenerator, not Manager). Booleans should read as questions (isActive, hasPermission). Avoid abbreviations, single-letter names (except loop indices), and generic names like data, info, or temp.

How should I write functions for clean code?

Clean functions should be small (ideally under 20 lines), do exactly one thing (Single Responsibility), operate at one level of abstraction, have descriptive names, take few arguments (0-2 ideal, 3 maximum), have no side effects, and either return a value (query) or change state (command) but not both (Command-Query Separation). Extract nested logic into well-named helper functions. Each function should read like a paragraph in a well-written essay.

What are the best practices for error handling in clean code?

Prefer exceptions over error codes for cleaner control flow. Write try-catch blocks first when implementing error-prone code. Create custom exception classes for domain-specific errors. Never return null — use Optional/Maybe types or throw exceptions instead. Never pass null as a function argument. Log errors with full context (what, where, why) at appropriate levels. Handle errors at the right level of abstraction — low-level code throws, high-level code catches and handles.

How do clean architecture layers work and why are they important?

Clean Architecture organizes code into concentric layers: Entities (core business rules), Use Cases (application-specific logic), Interface Adapters (controllers, presenters, gateways), and Frameworks/Drivers (web frameworks, databases, external services). The dependency rule states that dependencies must point inward — inner layers must not know about outer layers. This structure makes the codebase testable without frameworks, independent of UI and database choices, and resilient to external changes.

𝕏 Twitterin LinkedIn
Var dette nyttig?

Hold deg oppdatert

Få ukentlige dev-tips og nye verktøy.

Ingen spam. Avslutt når som helst.

Try These Related Tools

{ }JSON FormatterJSTypeScript to JavaScriptTSJSON to TypeScript

Related Articles

Software Design Patterns Guide: Creational, Structural & Behavioral Patterns

Comprehensive design patterns guide covering Factory, Builder, Singleton, Adapter, Decorator, Proxy, Facade, Observer, Strategy, Command, and State patterns with TypeScript and Python examples.

Software Testing Strategies Guide: Unit, Integration, E2E, TDD & BDD

Complete testing strategies guide covering unit testing, integration testing, E2E testing, TDD, BDD, test pyramids, mocking, coverage, CI pipelines, and performance testing with Jest, Vitest, Playwright, and Cypress.

Data Structures and Algorithms Guide: Arrays, Trees, Graphs, Hash Tables & Big O

Complete data structures and algorithms guide for developers. Learn arrays, linked lists, trees, graphs, hash tables, heaps, stacks, queues, Big O notation, sorting algorithms, and searching with practical code examples in TypeScript and Python.