Cohesion: The Secret to Long-Lasting Modules
A cohesive module does one thing and does it well. Learn how to measure and improve the cohesion of your classes and modules for more maintainable code.
We often talk about coupling (the dependencies between modules), but its companion is just as important: cohesion (what holds a module together). A highly cohesive module is easy to understand, test, and modify. A low-cohesion module is a box of surprises.
What is Cohesion?
Cohesion measures how strongly the elements within a module (functions, properties, methods) are related to each other and contribute to a single, common goal.
// ❌ Low cohesion — this class does 3 different jobs
class UserManager {
createUser(data: UserDto) { /* ... */ }
deleteUser(id: string) { /* ... */ }
sendWelcomeEmail(user: User) { /* ... */ }
generateMonthlyReport() { /* ... */ }
resizeProfileImage(path: string) { /* ... */ }
calculateSubscriptionPrice(plan: Plan) { /* ... */ }
}
The name UserManager is a classic red flag. When a name ends with “Manager,” “Service,” “Utils,” or “Helper” without further specificity, it often signals low cohesion—the class is a catch-all.
// ✅ High cohesion — each class has a clear purpose
class UserRepository {
create(data: UserDto): Promise<User> { /* ... */ }
findById(id: string): Promise<User | null> { /* ... */ }
delete(id: string): Promise<void> { /* ... */ }
}
class WelcomeEmailSender {
send(user: User): Promise<void> { /* ... */ }
}
class SubscriptionPricing {
calculate(plan: Plan): Money { /* ... */ }
}
Levels of Cohesion
From worst to best, here are the types of cohesion recognized in software engineering:
Coincidental Cohesion (the worst)
Elements are grouped together randomly, without any logical basis:
// ❌ Random utilities thrown together
// utils.ts
export function formatDate(d: Date): string { /* ... */ }
export function calculateTax(amount: number): number { /* ... */ }
export function slugify(text: string): string { /* ... */ }
export function retryWithBackoff<T>(fn: () => Promise<T>): Promise<T> { /* ... */ }
Logical Cohesion
Grouping by technical category, not by purpose:
// ❌ "All validators together"
class Validators {
validateEmail(email: string): boolean { /* ... */ }
validateOrderTotal(total: number): boolean { /* ... */ }
validateImageDimensions(w: number, h: number): boolean { /* ... */ }
}
These validators have nothing in common except the word “validate.” Email validation belongs to the user domain, image validation to the media domain.
Functional Cohesion (the best)
All elements contribute to a single, well-defined task:
// ✅ Everything in this class serves to manage the shopping cart
class ShoppingCart {
private items: CartItem[] = [];
add(product: Product, quantity: number): void {
const existing = this.items.find(i => i.productId === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.items.push(new CartItem(product, quantity));
}
}
remove(productId: string): void {
this.items = this.items.filter(i => i.productId !== productId);
}
total(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.subtotal()),
Money.zero()
);
}
isEmpty(): boolean {
return this.items.length === 0;
}
itemCount(): number {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
}
Every method uses this.items. Every method contributes to the “shopping cart” concept. This is functional cohesion.
How to Detect Low Cohesion
Test 1: The Name
If you cannot describe the module’s responsibility in one sentence without “and”, cohesion is likely low.
- ❌ “This class manages users and sends emails and generates reports”
- ✅ “This class calculates the price of a subscription based on the chosen plan”
Test 2: The Imports
A class that imports from 10 different domains is rarely cohesive:
// ❌ Warning sign — too many domains
import { EmailClient } from "./email";
import { PDFGenerator } from "./pdf";
import { ImageResizer } from "./images";
import { PaymentGateway } from "./payments";
import { MetricsCollector } from "./monitoring";
Test 3: Methods that Ignore Each Other
If the methods within a class do not use the same properties, they probably don’t belong together:
// ❌ Two groups of methods that share nothing
class ProductService {
// Group A — uses productRepo
findProduct(id: string) { return this.productRepo.find(id); }
listProducts() { return this.productRepo.findAll(); }
// Group B — uses searchEngine, unrelated to productRepo
indexForSearch(product: Product) { this.searchEngine.index(product); }
searchProducts(query: string) { return this.searchEngine.search(query); }
}
// ✅ Separated into two cohesive classes
class ProductCatalog {
find(id: string) { return this.repo.find(id); }
list() { return this.repo.findAll(); }
}
class ProductSearchIndex {
index(product: Product) { this.engine.index(product); }
search(query: string) { return this.engine.search(query); }
}
Test 4: LCOM (Lack of Cohesion of Methods)
This is a formal metric. For each pair of methods, check if they share at least one instance property. High LCOM = low cohesion.
In practice, no need to calculate—if your class has methods that don’t touch the same fields, it’s the same signal.
Cohesion at the Module/File Level
Cohesion isn’t just about classes. File organization matters:
# ❌ Technical organization — low cohesion by folder
src/
controllers/
userController.ts
orderController.ts
productController.ts
services/
userService.ts
orderService.ts
repositories/
userRepository.ts
orderRepository.ts
# ✅ Domain organization — high cohesion by folder
src/
users/
userController.ts
userService.ts
userRepository.ts
user.ts
orders/
orderController.ts
orderService.ts
orderRepository.ts
order.ts
In the second organization, when you work on orders, everything is in one place. Modifying a feature affects only one folder.
Cohesion and Size
Cohesion naturally guides size. A cohesive class is rarely enormous—if it grows too large, it’s accumulating responsibilities.
But be careful: don’t split blindly. Two 50-line classes with strong interdependencies are worse than one cohesive 100-line class. The goal isn’t to have small classes; it’s to have focused classes.
// ❌ Artificial split — these two classes are inseparable
class OrderValidator {
validate(order: Order) { /* uses OrderRules */ }
}
class OrderRules {
getRules() { /* used only by OrderValidator */ }
}
// ✅ A single cohesive class
class OrderValidator {
validate(order: Order) {
return this.rules().every(rule => rule(order));
}
private rules(): Array<(o: Order) => boolean> {
return [
o => o.items.length > 0,
o => o.total.isPositive(),
o => o.shippingAddress !== null,
];
}
}
Key Takeaway
Cohesion is the glue that justifies a module’s existence. Regularly ask yourself: “Why are these elements together?” If the answer is “because they concern the same concept,” cohesion is good. If the answer is “because we needed a place to put them”… it’s time to refactor.
High cohesion + low coupling is the recipe for an architecture that ages well.