Coupling and Cohesion

Strive for low coupling between modules and high cohesion within them.

Overview

Coupling and cohesion are complementary measures of software quality at the module level. Cohesion measures how strongly related the elements within a single module are—high cohesion means everything in a module works toward a single purpose. Coupling measures how dependent modules are on each other—low coupling means modules can be understood and modified independently.

The ideal is modules with high internal cohesion and low external coupling. Such modules are easier to understand (everything inside is related), easier to modify (changes don’t ripple to other modules), and easier to reuse (few external dependencies).

Cohesion

Cohesion describes why elements are grouped together. Ranked from worst to best:

Coincidental Cohesion (Worst)

Elements grouped arbitrarily, with no meaningful relationship.

// Coincidental: Utilities grab bag
class Utilities {
  formatDate(date: Date): string { ... }
  calculateTax(amount: number): number { ... }
  validateEmail(email: string): boolean { ... }
  compressImage(image: Buffer): Buffer { ... }
}

Temporal Cohesion

Elements grouped because they execute at the same time.

// Temporal: All run at startup, but unrelated purposes
function initializeApp() {
  loadConfiguration();
  connectToDatabase();
  startMetricsCollection();
  warmCache();
  logStartupTime();
}

Logical Cohesion

Elements perform similar operations but on different data or contexts.

// Logical: All "output" but to different destinations
class OutputHandler {
  printToConsole(data: string) { ... }
  writeToFile(data: string, path: string) { ... }
  sendToNetwork(data: string, url: string) { ... }
}

Functional Cohesion (Best)

All elements contribute to a single, well-defined task.

// Functional: Everything serves order validation
class OrderValidator {
  validate(order: Order): ValidationResult {
    return this.combineResults([
      this.validateItems(order.items),
      this.validateCustomer(order.customerId),
      this.validateShipping(order.shippingAddress),
      this.validatePayment(order.paymentMethod)
    ]);
  }
 
  private validateItems(items: OrderItem[]): ValidationResult { ... }
  private validateCustomer(customerId: string): ValidationResult { ... }
  private validateShipping(address: Address): ValidationResult { ... }
  private validatePayment(method: PaymentMethod): ValidationResult { ... }
  private combineResults(results: ValidationResult[]): ValidationResult { ... }
}

Coupling

Coupling describes how modules depend on each other. Ranked from worst to best:

Content Coupling (Worst)

One module directly accesses or modifies another’s internal data.

// Content coupling: Directly manipulating internals
function processOrder(orderService: OrderService) {
  orderService._internalQueue.push(order); // Accessing private state
  orderService._counter++; // Modifying internal variable
}

Common Coupling

Modules share global state.

// Common coupling: Shared global
let currentUser: User; // Global state
 
class OrderService {
  createOrder() {
    const order = { userId: currentUser.id, ... }; // Uses global
  }
}
 
class NotificationService {
  notify() {
    sendEmail(currentUser.email, ...); // Also uses global
  }
}

Stamp Coupling

Modules share a data structure but only use parts of it.

// Stamp coupling: Function receives more than it needs
interface User {
  id: string;
  name: string;
  email: string;
  address: Address;
  preferences: UserPreferences;
  paymentMethods: PaymentMethod[];
}
 
function sendWelcomeEmail(user: User) {
  // Only uses email, but receives entire User
  sendEmail(user.email, 'Welcome!');
}

Data Coupling (Best)

Modules communicate through minimal, necessary parameters.

// Data coupling: Only passes what's needed
function sendWelcomeEmail(email: string) {
  sendEmail(email, 'Welcome!');
}
 
function calculateOrderTotal(items: OrderItem[], taxRate: number): number {
  // Receives exactly what it needs
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  return subtotal * (1 + taxRate);
}

Example

Consider refactoring for better coupling and cohesion:

// Before: Low cohesion (mixed concerns), high coupling (knows internals)
class OrderProcessor {
  process(order: Order, db: Database) {
    // Validation logic mixed with persistence
    if (order.items.length === 0) throw new Error('Empty order');
 
    // Direct database manipulation
    db.query(`INSERT INTO orders VALUES (${order.id}, ...)`);
 
    // Email logic mixed in
    const smtp = new SmtpClient();
    smtp.connect('mail.server.com');
    smtp.send(order.customerEmail, 'Order confirmed');
 
    // Logging mixed in
    console.log(`Order ${order.id} processed`);
  }
}

After refactoring:

// High cohesion: Each class has focused purpose
// Low coupling: Dependencies injected, minimal interfaces
 
class OrderValidator {
  validate(order: Order): ValidationResult {
    if (order.items.length === 0) {
      return { valid: false, error: 'Empty order' };
    }
    return { valid: true };
  }
}
 
class OrderRepository {
  constructor(private readonly db: Database) {}
 
  save(order: Order): Promise<void> {
    return this.db.orders.insert(order);
  }
}
 
class OrderNotifier {
  constructor(private readonly emailService: EmailService) {}
 
  sendConfirmation(email: string, orderId: string): Promise<void> {
    return this.emailService.send(email, `Order ${orderId} confirmed`);
  }
}
 
class OrderProcessor {
  constructor(
    private readonly validator: OrderValidator,
    private readonly repository: OrderRepository,
    private readonly notifier: OrderNotifier,
    private readonly logger: Logger
  ) {}
 
  async process(order: Order): Promise<void> {
    const validation = this.validator.validate(order);
    if (!validation.valid) throw new Error(validation.error);
 
    await this.repository.save(order);
    await this.notifier.sendConfirmation(order.customerEmail, order.id);
    this.logger.info(`Order ${order.id} processed`);
  }
}

Common Mistakes

Utility classes - Classes named “Utils” or “Helpers” usually have coincidental cohesion. Find better homes for those functions.

Feature envy - A method that uses more data from another class than its own indicates misplaced cohesion.

Tight coupling to frameworks - When business logic depends on specific framework classes, it can’t be tested or reused independently.

Over-injection - Many constructor parameters might indicate low cohesion—the class is doing too many things.

References

  • Yourdon, E., & Constantine, L. (1979). Structured Design.
  • Bass, L., Clements, P., & Kazman, R. (2012). Software Architecture in Practice.
  • Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design.