DevDocsDev Docs
Design PrinciplesSolid Principles

Single Responsibility Principle (SRP)

A class should have only one reason to change - the foundation of clean code

Single Responsibility Principle (SRP)

"A class should have only one reason to change." — Robert C. Martin

The Single Responsibility Principle states that a module, class, or function should be responsible for only one thing. When a component has multiple responsibilities, changes to one can unintentionally affect the others.


The Problem

God Class Anti-Pattern

A "God Class" is a class that knows too much or does too much. It violates SRP by combining multiple responsibilities.

/**
 * ❌ BAD: This class has too many responsibilities
 * 
 * Responsibilities mixed together:
 * 1. User data management
 * 2. Input validation
 * 3. Database persistence
 * 4. Email notifications
 * 5. Password hashing
 * 6. Session management
 */
interface UserData {
  id: string;
  name: string;
  email: string;
  password: string;
}

const createUserService = () => {
  const users = new Map<string, UserData>();
  const sessions = new Map<string, string>();
  
  return {
    // Responsibility 1: Data management + Validation
    createUser(data: { name: string; email: string; password: string }) {
      // Validation mixed with business logic
      if (!data.email.includes("@")) {
        throw new Error("Invalid email format");
      }
      if (data.password.length < 8) {
        throw new Error("Password must be at least 8 characters");
      }
      if (data.name.length < 2) {
        throw new Error("Name too short");
      }
      
      // Check for existing user (database concern)
      for (const user of users.values()) {
        if (user.email === data.email) {
          throw new Error("Email already exists");
        }
      }
      
      // Password hashing (security concern)
      const hashedPassword = `hashed_${data.password}_${Date.now()}`;
      
      // Create user
      const user: UserData = {
        id: `user_${Date.now()}`,
        name: data.name,
        email: data.email,
        password: hashedPassword,
      };
      
      // Save to database (persistence concern)
      users.set(user.id, user);
      
      // Send welcome email (notification concern)
      console.log(`📧 Sending welcome email to ${data.email}`);
      console.log(`Subject: Welcome to our platform!`);
      console.log(`Body: Hello ${data.name}, thanks for joining!`);
      
      // Log the action (logging concern)
      console.log(`[${new Date().toISOString()}] User created: ${user.id}`);
      
      return user;
    },
    
    // Same mixed responsibilities continue...
    updateUser(id: string, data: Partial<UserData>) {
      const user = users.get(id);
      if (!user) throw new Error("User not found");
      
      // Validation again (duplicated)
      if (data.email && !data.email.includes("@")) {
        throw new Error("Invalid email format");
      }
      
      const updated = { ...user, ...data };
      users.set(id, updated);
      
      // Notification again (duplicated)
      console.log(`📧 Sending update notification to ${updated.email}`);
      
      // Logging again (duplicated)
      console.log(`[${new Date().toISOString()}] User updated: ${id}`);
      
      return updated;
    },
    
    login(email: string, password: string) {
      // Find user (database concern)
      let foundUser: UserData | null = null;
      for (const user of users.values()) {
        if (user.email === email) {
          foundUser = user;
          break;
        }
      }
      
      if (!foundUser) throw new Error("User not found");
      
      // Verify password (security concern)
      const expectedHash = `hashed_${password}_`;
      if (!foundUser.password.startsWith(expectedHash.slice(0, -1))) {
        throw new Error("Invalid password");
      }
      
      // Create session (session management concern)
      const sessionId = `session_${Date.now()}`;
      sessions.set(sessionId, foundUser.id);
      
      // Log login (logging concern)
      console.log(`[${new Date().toISOString()}] User logged in: ${foundUser.id}`);
      
      return { sessionId, user: foundUser };
    },
    
    // Even more mixed concerns...
    deleteUser(id: string) {
      const user = users.get(id);
      if (!user) throw new Error("User not found");
      
      users.delete(id);
      
      // Clean up sessions
      for (const [sessionId, userId] of sessions.entries()) {
        if (userId === id) sessions.delete(sessionId);
      }
      
      // Send goodbye email
      console.log(`📧 Sending account deletion email to ${user.email}`);
      
      // Log deletion
      console.log(`[${new Date().toISOString()}] User deleted: ${id}`);
    },
  };
};

// Problems with this approach:
// 1. Hard to test - can't test email without creating users
// 2. Hard to modify - changing email format affects all methods
// 3. Hard to reuse - can't use validation elsewhere
// 4. Hard to maintain - 200+ lines in one file
// 5. Hard to extend - adding SMS notifications requires changing everything

The Solution

Separate Responsibilities into Focused Modules

/**
 * ✅ GOOD: Types are their own concern
 */

// Domain types
export interface User {
  readonly id: string;
  readonly name: string;
  readonly email: string;
  readonly passwordHash: string;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}

export interface CreateUserInput {
  readonly name: string;
  readonly email: string;
  readonly password: string;
}

export interface UpdateUserInput {
  readonly name?: string;
  readonly email?: string;
}

export interface LoginInput {
  readonly email: string;
  readonly password: string;
}

export interface Session {
  readonly id: string;
  readonly userId: string;
  readonly createdAt: Date;
  readonly expiresAt: Date;
}

// Result types
export type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E };
interface CreateUserInput {
  readonly name: string;
  readonly email: string;
  readonly password: string;
}

interface UpdateUserInput {
  readonly name?: string;
  readonly email?: string;
}

// ---cut---
/**
 * ✅ GOOD: Validation is its own concern
 * 
 * Single Responsibility: Input validation
 */

export interface ValidationError {
  readonly field: string;
  readonly message: string;
  readonly code: string;
}

export interface ValidationResult {
  readonly valid: boolean;
  readonly errors: ValidationError[];
}

// Individual validators - each has ONE job
const validateEmail = (email: string): ValidationError | null => {
  if (!email) {
    return { field: "email", message: "Email is required", code: "REQUIRED" };
  }
  if (!email.includes("@") || !email.includes(".")) {
    return { field: "email", message: "Invalid email format", code: "INVALID_FORMAT" };
  }
  if (email.length > 255) {
    return { field: "email", message: "Email too long", code: "TOO_LONG" };
  }
  return null;
};

const validatePassword = (password: string): ValidationError | null => {
  if (!password) {
    return { field: "password", message: "Password is required", code: "REQUIRED" };
  }
  if (password.length < 8) {
    return { field: "password", message: "Password must be at least 8 characters", code: "TOO_SHORT" };
  }
  if (password.length > 128) {
    return { field: "password", message: "Password too long", code: "TOO_LONG" };
  }
  if (!/[A-Z]/.test(password)) {
    return { field: "password", message: "Password must contain uppercase letter", code: "MISSING_UPPERCASE" };
  }
  if (!/[0-9]/.test(password)) {
    return { field: "password", message: "Password must contain a number", code: "MISSING_NUMBER" };
  }
  return null;
};

const validateName = (name: string): ValidationError | null => {
  if (!name) {
    return { field: "name", message: "Name is required", code: "REQUIRED" };
  }
  if (name.length < 2) {
    return { field: "name", message: "Name must be at least 2 characters", code: "TOO_SHORT" };
  }
  if (name.length > 100) {
    return { field: "name", message: "Name too long", code: "TOO_LONG" };
  }
  return null;
};

// Compose validators
export const createUserValidator = () => ({
  validateCreateUser(input: CreateUserInput): ValidationResult {
    const errors = [
      validateName(input.name),
      validateEmail(input.email),
      validatePassword(input.password),
    ].filter((e): e is ValidationError => e !== null);
    
    return { valid: errors.length === 0, errors };
  },
  
  validateUpdateUser(input: UpdateUserInput): ValidationResult {
    const errors: ValidationError[] = [];
    
    if (input.name !== undefined) {
      const nameError = validateName(input.name);
      if (nameError) errors.push(nameError);
    }
    
    if (input.email !== undefined) {
      const emailError = validateEmail(input.email);
      if (emailError) errors.push(emailError);
    }
    
    return { valid: errors.length === 0, errors };
  },
});

// Usage
const validator = createUserValidator();
const result = validator.validateCreateUser({
  name: "A", // Too short
  email: "invalid", // Missing @
  password: "short", // Too short, no uppercase, no number
});

if (!result.valid) {
  console.log("Validation errors:", result.errors);
}
interface User {
  readonly id: string;
  readonly name: string;
  readonly email: string;
  readonly passwordHash: string;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}

interface CreateUserInput {
  readonly name: string;
  readonly email: string;
  readonly password: string;
}

interface UpdateUserInput {
  readonly name?: string;
  readonly email?: string;
}

// ---cut---
/**
 * ✅ GOOD: Data persistence is its own concern
 * 
 * Single Responsibility: Database operations
 */

export interface UserRepository {
  findById: (id: string) => Promise<User | null>;
  findByEmail: (email: string) => Promise<User | null>;
  save: (user: User) => Promise<User>;
  update: (id: string, data: Partial<User>) => Promise<User>;
  delete: (id: string) => Promise<void>;
  exists: (email: string) => Promise<boolean>;
}

// In-memory implementation
export const createInMemoryUserRepository = (): UserRepository => {
  const users = new Map<string, User>();
  
  return {
    async findById(id) {
      return users.get(id) ?? null;
    },
    
    async findByEmail(email) {
      const normalizedEmail = email.toLowerCase();
      for (const user of users.values()) {
        if (user.email.toLowerCase() === normalizedEmail) {
          return user;
        }
      }
      return null;
    },
    
    async save(user) {
      users.set(user.id, user);
      return user;
    },
    
    async update(id, data) {
      const existing = users.get(id);
      if (!existing) {
        throw new Error(`User not found: ${id}`);
      }
      
      const updated: User = {
        ...existing,
        ...data,
        updatedAt: new Date(),
      };
      
      users.set(id, updated);
      return updated;
    },
    
    async delete(id) {
      if (!users.has(id)) {
        throw new Error(`User not found: ${id}`);
      }
      users.delete(id);
    },
    
    async exists(email) {
      const user = await this.findByEmail(email);
      return user !== null;
    },
  };
};

// PostgreSQL implementation (same interface, different storage)
export const createPostgresUserRepository = (connectionString: string): UserRepository => {
  // Real database implementation...
  return {
    async findById(id) {
      // SELECT * FROM users WHERE id = $1
      return null;
    },
    async findByEmail(email) {
      // SELECT * FROM users WHERE LOWER(email) = LOWER($1)
      return null;
    },
    async save(user) {
      // INSERT INTO users...
      return user;
    },
    async update(id, data) {
      // UPDATE users SET ... WHERE id = $1
      return data as User;
    },
    async delete(id) {
      // DELETE FROM users WHERE id = $1
    },
    async exists(email) {
      // SELECT EXISTS(SELECT 1 FROM users WHERE LOWER(email) = LOWER($1))
      return false;
    },
  };
};
interface User {
  readonly id: string;
  readonly name: string;
  readonly email: string;
  readonly passwordHash: string;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}
// ---cut---
/**
 * ✅ GOOD: Notifications are their own concern
 * 
 * Single Responsibility: Sending notifications
 */

export interface EmailOptions {
  to: string;
  subject: string;
  body: string;
  html?: string;
}

export interface EmailService {
  send: (options: EmailOptions) => Promise<void>;
}

// Console implementation (for development)
export const createConsoleEmailService = (): EmailService => ({
  async send({ to, subject, body }) {
    console.log("━".repeat(50));
    console.log(`📧 EMAIL TO: ${to}`);
    console.log(`   SUBJECT: ${subject}`);
    console.log(`   BODY: ${body}`);
    console.log("━".repeat(50));
  },
});

// Real email implementation
export const createSendGridEmailService = (apiKey: string): EmailService => ({
  async send({ to, subject, body, html }) {
    // Real SendGrid API call...
    console.log(`SendGrid: Sending "${subject}" to ${to}`);
  },
});

// User-specific notification templates
export interface UserNotificationService {
  sendWelcomeEmail: (user: User) => Promise<void>;
  sendPasswordResetEmail: (user: User, resetToken: string) => Promise<void>;
  sendAccountDeletedEmail: (user: User) => Promise<void>;
  sendEmailVerification: (user: User, verifyToken: string) => Promise<void>;
}

export const createUserNotificationService = (
  emailService: EmailService
): UserNotificationService => ({
  async sendWelcomeEmail(user) {
    await emailService.send({
      to: user.email,
      subject: "Welcome to Our Platform!",
      body: `Hello ${user.name},\n\nWelcome aboard! We're excited to have you.`,
      html: `<h1>Hello ${user.name}!</h1><p>Welcome aboard! We're excited to have you.</p>`,
    });
  },
  
  async sendPasswordResetEmail(user, resetToken) {
    const resetLink = `https://example.com/reset?token=${resetToken}`;
    await emailService.send({
      to: user.email,
      subject: "Password Reset Request",
      body: `Hi ${user.name},\n\nClick here to reset your password: ${resetLink}`,
    });
  },
  
  async sendAccountDeletedEmail(user) {
    await emailService.send({
      to: user.email,
      subject: "Account Deleted",
      body: `Hi ${user.name},\n\nYour account has been deleted. We're sorry to see you go.`,
    });
  },
  
  async sendEmailVerification(user, verifyToken) {
    const verifyLink = `https://example.com/verify?token=${verifyToken}`;
    await emailService.send({
      to: user.email,
      subject: "Verify Your Email",
      body: `Hi ${user.name},\n\nPlease verify your email: ${verifyLink}`,
    });
  },
});
/**
 * ✅ GOOD: Security operations are their own concern
 * 
 * Single Responsibility: Password hashing and verification
 */

export interface PasswordHasher {
  hash: (password: string) => Promise<string>;
  verify: (password: string, hash: string) => Promise<boolean>;
}

// Simple implementation (use bcrypt or argon2 in production!)
export const createPasswordHasher = (): PasswordHasher => ({
  async hash(password) {
    // In production: return bcrypt.hash(password, 12)
    const salt = Math.random().toString(36).substring(2);
    return `hashed:${salt}:${password}`;
  },
  
  async verify(password, hash) {
    // In production: return bcrypt.compare(password, hash)
    const parts = hash.split(":");
    if (parts.length !== 3) return false;
    return parts[2] === password;
  },
});

// Bcrypt implementation
export const createBcryptPasswordHasher = (): PasswordHasher => ({
  async hash(password) {
    // const bcrypt = require('bcrypt');
    // return bcrypt.hash(password, 12);
    return `bcrypt_hash_${password}`;
  },
  
  async verify(password, hash) {
    // const bcrypt = require('bcrypt');
    // return bcrypt.compare(password, hash);
    return hash === `bcrypt_hash_${password}`;
  },
});

/**
 * Session management is ALSO its own concern
 */
export interface Session {
  id: string;
  userId: string;
  createdAt: Date;
  expiresAt: Date;
}

export interface SessionService {
  create: (userId: string, durationMs?: number) => Promise<Session>;
  validate: (sessionId: string) => Promise<Session | null>;
  revoke: (sessionId: string) => Promise<void>;
  revokeAllForUser: (userId: string) => Promise<void>;
}

export const createSessionService = (): SessionService => {
  const sessions = new Map<string, Session>();
  const DEFAULT_DURATION = 24 * 60 * 60 * 1000; // 24 hours
  
  return {
    async create(userId, durationMs = DEFAULT_DURATION) {
      const session: Session = {
        id: `session_${Date.now()}_${Math.random().toString(36)}`,
        userId,
        createdAt: new Date(),
        expiresAt: new Date(Date.now() + durationMs),
      };
      
      sessions.set(session.id, session);
      return session;
    },
    
    async validate(sessionId) {
      const session = sessions.get(sessionId);
      if (!session) return null;
      
      if (new Date() > session.expiresAt) {
        sessions.delete(sessionId);
        return null;
      }
      
      return session;
    },
    
    async revoke(sessionId) {
      sessions.delete(sessionId);
    },
    
    async revokeAllForUser(userId) {
      for (const [id, session] of sessions.entries()) {
        if (session.userId === userId) {
          sessions.delete(id);
        }
      }
    },
  };
};
interface User {
  readonly id: string;
  readonly name: string;
  readonly email: string;
  readonly passwordHash: string;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}

interface CreateUserInput {
  readonly name: string;
  readonly email: string;
  readonly password: string;
}

interface ValidationResult {
  readonly valid: boolean;
  readonly errors: { field: string; message: string }[];
}

interface UserRepository {
  findById: (id: string) => Promise<User | null>;
  findByEmail: (email: string) => Promise<User | null>;
  save: (user: User) => Promise<User>;
  exists: (email: string) => Promise<boolean>;
}

interface PasswordHasher {
  hash: (password: string) => Promise<string>;
  verify: (password: string, hash: string) => Promise<boolean>;
}

interface UserNotificationService {
  sendWelcomeEmail: (user: User) => Promise<void>;
}

interface Logger {
  info: (message: string, meta?: Record<string, unknown>) => void;
  error: (message: string, error?: Error) => void;
}

// ---cut---
/**
 * ✅ GOOD: Orchestration layer - coordinates single-purpose modules
 * 
 * Single Responsibility: Coordinating the registration flow
 */

interface RegisterUserDeps {
  validator: { validateCreateUser: (input: CreateUserInput) => ValidationResult };
  userRepository: UserRepository;
  passwordHasher: PasswordHasher;
  notifications: UserNotificationService;
  logger: Logger;
}

type RegisterResult = 
  | { success: true; user: User }
  | { success: false; error: string; code: string };

export const createRegisterUserUseCase = (deps: RegisterUserDeps) => {
  const { validator, userRepository, passwordHasher, notifications, logger } = deps;
  
  return async (input: CreateUserInput): Promise<RegisterResult> => {
    // Step 1: Validate input (delegates to validator)
    const validation = validator.validateCreateUser(input);
    if (!validation.valid) {
      logger.info("Registration validation failed", { errors: validation.errors });
      return {
        success: false,
        error: validation.errors.map(e => e.message).join(", "),
        code: "VALIDATION_ERROR",
      };
    }
    
    // Step 2: Check for existing user (delegates to repository)
    const exists = await userRepository.exists(input.email);
    if (exists) {
      logger.info("Registration failed: email exists", { email: input.email });
      return {
        success: false,
        error: "Email already registered",
        code: "EMAIL_EXISTS",
      };
    }
    
    // Step 3: Hash password (delegates to hasher)
    const passwordHash = await passwordHasher.hash(input.password);
    
    // Step 4: Create user entity
    const user: User = {
      id: `user_${Date.now()}`,
      name: input.name,
      email: input.email.toLowerCase(),
      passwordHash,
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    
    // Step 5: Persist user (delegates to repository)
    await userRepository.save(user);
    logger.info("User created", { userId: user.id });
    
    // Step 6: Send notification (delegates to notification service)
    // Fire and forget - don't fail registration if email fails
    notifications.sendWelcomeEmail(user).catch(err => {
      logger.error("Failed to send welcome email", err as Error);
    });
    
    return { success: true, user };
  };
};

// Each dependency is easily testable and replaceable!

Real-World Example: E-Commerce Order Processing

/**
 * ✅ SRP in action: E-commerce order processing
 * Each module has ONE responsibility
 */

// --- Types ---
interface OrderItem {
  productId: string;
  quantity: number;
  unitPrice: number;
}

interface Order {
  id: string;
  customerId: string;
  items: OrderItem[];
  status: "pending" | "paid" | "shipped" | "delivered";
  total: number;
  createdAt: Date;
}

interface Customer {
  id: string;
  email: string;
  name: string;
}

// --- Price Calculator (ONE job: calculate prices) ---
const createPriceCalculator = () => ({
  calculateSubtotal(items: OrderItem[]): number {
    return items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
  },
  
  calculateTax(subtotal: number, taxRate: number = 0.08): number {
    return subtotal * taxRate;
  },
  
  calculateShipping(items: OrderItem[]): number {
    const totalQuantity = items.reduce((sum, item) => sum + item.quantity, 0);
    return totalQuantity > 5 ? 0 : 9.99; // Free shipping over 5 items
  },
  
  calculateTotal(items: OrderItem[], taxRate?: number): number {
    const subtotal = this.calculateSubtotal(items);
    const tax = this.calculateTax(subtotal, taxRate);
    const shipping = this.calculateShipping(items);
    return subtotal + tax + shipping;
  },
});

// --- Inventory Checker (ONE job: check stock) ---
interface InventoryService {
  checkAvailability: (productId: string, quantity: number) => Promise<boolean>;
  reserve: (productId: string, quantity: number) => Promise<void>;
  release: (productId: string, quantity: number) => Promise<void>;
}

const createInventoryService = (): InventoryService => {
  const stock = new Map<string, number>([
    ["prod_1", 100],
    ["prod_2", 50],
    ["prod_3", 25],
  ]);
  
  const reserved = new Map<string, number>();
  
  return {
    async checkAvailability(productId, quantity) {
      const available = (stock.get(productId) ?? 0) - (reserved.get(productId) ?? 0);
      return available >= quantity;
    },
    
    async reserve(productId, quantity) {
      const current = reserved.get(productId) ?? 0;
      reserved.set(productId, current + quantity);
    },
    
    async release(productId, quantity) {
      const current = reserved.get(productId) ?? 0;
      reserved.set(productId, Math.max(0, current - quantity));
    },
  };
};

// --- Payment Processor (ONE job: handle payments) ---
interface PaymentResult {
  success: boolean;
  transactionId?: string;
  error?: string;
}

interface PaymentService {
  charge: (customerId: string, amount: number) => Promise<PaymentResult>;
  refund: (transactionId: string, amount: number) => Promise<PaymentResult>;
}

const createPaymentService = (): PaymentService => ({
  async charge(customerId, amount) {
    // Simulate payment processing
    console.log(`💳 Charging $${amount.toFixed(2)} to customer ${customerId}`);
    
    // Random success/failure for demo
    const success = Math.random() > 0.1;
    
    return success
      ? { success: true, transactionId: `txn_${Date.now()}` }
      : { success: false, error: "Payment declined" };
  },
  
  async refund(transactionId, amount) {
    console.log(`💰 Refunding $${amount.toFixed(2)} for ${transactionId}`);
    return { success: true, transactionId: `refund_${Date.now()}` };
  },
});

// --- Order Repository (ONE job: persist orders) ---
interface OrderRepository {
  save: (order: Order) => Promise<Order>;
  findById: (id: string) => Promise<Order | null>;
  updateStatus: (id: string, status: Order["status"]) => Promise<void>;
}

const createOrderRepository = (): OrderRepository => {
  const orders = new Map<string, Order>();
  
  return {
    async save(order) {
      orders.set(order.id, order);
      return order;
    },
    
    async findById(id) {
      return orders.get(id) ?? null;
    },
    
    async updateStatus(id, status) {
      const order = orders.get(id);
      if (order) {
        orders.set(id, { ...order, status });
      }
    },
  };
};

// --- Order Notification (ONE job: send order-related emails) ---
interface OrderNotificationService {
  sendOrderConfirmation: (order: Order, customer: Customer) => Promise<void>;
  sendShippingNotification: (order: Order, customer: Customer, trackingNumber: string) => Promise<void>;
}

const createOrderNotificationService = (): OrderNotificationService => ({
  async sendOrderConfirmation(order, customer) {
    console.log(`📧 Order confirmation sent to ${customer.email} for order ${order.id}`);
  },
  
  async sendShippingNotification(order, customer, trackingNumber) {
    console.log(`📧 Shipping notification sent to ${customer.email}: ${trackingNumber}`);
  },
});

// --- Order Use Case (Orchestrates the flow) ---
interface PlaceOrderDeps {
  priceCalculator: ReturnType<typeof createPriceCalculator>;
  inventory: InventoryService;
  payment: PaymentService;
  orderRepository: OrderRepository;
  notifications: OrderNotificationService;
}

type PlaceOrderResult = 
  | { success: true; order: Order }
  | { success: false; error: string };

const createPlaceOrderUseCase = (deps: PlaceOrderDeps) => {
  const { priceCalculator, inventory, payment, orderRepository, notifications } = deps;
  
  return async (
    customer: Customer,
    items: OrderItem[]
  ): Promise<PlaceOrderResult> => {
    // Step 1: Check inventory for all items
    for (const item of items) {
      const available = await inventory.checkAvailability(item.productId, item.quantity);
      if (!available) {
        return { success: false, error: `Product ${item.productId} not available` };
      }
    }
    
    // Step 2: Reserve inventory
    for (const item of items) {
      await inventory.reserve(item.productId, item.quantity);
    }
    
    // Step 3: Calculate total
    const total = priceCalculator.calculateTotal(items);
    
    // Step 4: Process payment
    const paymentResult = await payment.charge(customer.id, total);
    
    if (!paymentResult.success) {
      // Release reserved inventory
      for (const item of items) {
        await inventory.release(item.productId, item.quantity);
      }
      return { success: false, error: paymentResult.error ?? "Payment failed" };
    }
    
    // Step 5: Create and save order
    const order: Order = {
      id: `order_${Date.now()}`,
      customerId: customer.id,
      items,
      status: "paid",
      total,
      createdAt: new Date(),
    };
    
    await orderRepository.save(order);
    
    // Step 6: Send confirmation (fire and forget)
    notifications.sendOrderConfirmation(order, customer).catch(console.error);
    
    return { success: true, order };
  };
};

Use Cases & Problem Solving


Summary

AspectWithout SRPWith SRP
TestingNeed full integration testsUnit tests for each module
ChangesRipple effects everywhereIsolated to one module
ReusabilityCan't reuse partsEach module is reusable
Team WorkMerge conflictsIndependent development
UnderstandingRead entire fileFocus on one concern

Key Takeaway

SRP is not about having tiny classes - it's about having classes/modules with a single, clear purpose. The test is: "Can you describe what this module does without using 'and'?"

On this page