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.
Related Concepts
- Single Responsibility - High cohesion principle
- Separation of Concerns - Achieving proper boundaries
- Dependency Injection - Managing coupling through abstractions
- SOLID Principles - Design principles supporting these goals
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.