Cohesion: Measuring Module Design Quality
Why Cohesion Matters
Have you ever opened a class called UserManager and found it handling database queries, email sending, PDF generation, and logging? That class is suffering from low cohesion, and it is the reason why a simple change to the email template forces you to re-test half the system.
Cohesion is a measure of how well the parts of a module fit together. A module is considered cohesive when all its components are closely related and packaged together. Breaking the module into smaller parts would lead to increased coupling between modules, as they would need to communicate with each other to achieve the desired results.
Attempting to divide a cohesive module would only result in increased coupling and decreased readability. Larry Constantine
The goal is straightforward: every element in a module should be working toward the same purpose. When that is the case, the module is easy to name, easy to understand, and easy to change.
The Seven Types of Cohesion
Larry Constantine and Edward Yourdon identified seven types of cohesion, ranked here from best to worst. The higher a module sits on this scale, the easier it is to understand, test, and maintain.
Functional Cohesion (best)
Every part of the module is related to the other, and the module contains everything essential to perform a single, well-defined function. This is the gold standard.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TaxCalculator {
private final TaxRateRepository taxRates;
TaxCalculator(TaxRateRepository taxRates) {
this.taxRates = taxRates;
}
Money calculateIncomeTax(Income income) {
TaxRate rate = taxRates.rateFor(income.bracket());
return income.amount().multiplyBy(rate);
}
Money calculateSalesTax(Money price, Region region) {
TaxRate rate = taxRates.salesRateFor(region);
return price.multiplyBy(rate);
}
TaxReport generateReport(Income income, Region region) {
Money incomeTax = calculateIncomeTax(income);
Money salesTax = calculateSalesTax(income.totalSales(), region);
return new TaxReport(incomeTax, salesTax);
}
}
Every method computes or reports on taxes. Nothing else sneaks in. You can name this class in two words, and those two words tell you everything it does.
Sequential Cohesion
The output from one part of the module serves as the input to another part. The parts are related because they form a processing pipeline where data flows sequentially from one step to the next.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ImageProcessor {
private byte[] rawData;
private BufferedImage decoded;
private BufferedImage resized;
void loadRaw(byte[] data) {
this.rawData = data;
}
void decode() {
this.decoded = ImageDecoder.decode(rawData);
}
void resize(int width, int height) {
this.resized = ImageScaler.scale(decoded, width, height);
}
byte[] compress() {
return JpegEncoder.encode(resized);
}
}
Each step consumes the output of the previous one: raw bytes become a decoded image, then a resized image, then compressed bytes. The ordering is essential, and every method works with the same evolving data.
Communicational Cohesion
Two or more operations use the same input data or contribute to the same output. They are grouped because they operate on shared data, not because they form a pipeline.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class OrderSummary {
private final Order order;
OrderSummary(Order order) {
this.order = order;
}
Receipt generateReceipt() {
return new Receipt(order.items(), order.total());
}
void saveToDatabase(OrderRepository repository) {
repository.save(order);
}
void sendConfirmationEmail(EmailService emailService) {
emailService.send(order.customerEmail(), order.summary());
}
}
All three methods operate on the same Order object. They do not feed into each other, but they share the same core data.
Procedural Cohesion
Two or more operations must execute in a particular order, but they do not necessarily share data. The grouping exists because of the required sequence, not because the operations are functionally related.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class UserRegistration {
void register(String username, String email) {
validateInput(username, email);
createAccount(username, email);
sendVerificationEmail(email);
logRegistrationEvent(username);
}
private void validateInput(String username, String email) {
// validation logic
}
private void createAccount(String username, String email) {
// persistence logic
}
private void sendVerificationEmail(String email) {
// email logic
}
private void logRegistrationEvent(String username) {
// audit logic
}
}
The steps must happen in this order, but validation, persistence, email sending, and logging are fundamentally different concerns. This is better than temporal or logical cohesion because the ordering is meaningful, but the mixed responsibilities are a smell.
Temporal Cohesion
Modules are related based on timing dependencies rather than functional relationships. The classic example is system startup: a collection of unrelated tasks that all need to happen “at the same time.”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ApplicationStartup {
void initialize() {
loadConfiguration();
openDatabaseConnection();
startMessageBroker();
warmUpCaches();
registerHealthChecks();
}
private void loadConfiguration() { /* ... */ }
private void openDatabaseConnection() { /* ... */ }
private void startMessageBroker() { /* ... */ }
private void warmUpCaches() { /* ... */ }
private void registerHealthChecks() { /* ... */ }
}
These methods are grouped together only because they all run during initialization. Configuration, database connections, message brokers, and caches have nothing in common functionally.
Logical Cohesion
A module contains elements that perform similar activities, but the caller selects which activity to execute (often through a control flag or parameter). The elements share a general category but are otherwise unrelated.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FormatUtils {
static String formatXml(Object data) {
return new XmlMapper().writeValueAsString(data);
}
static String formatJson(Object data) {
return new ObjectMapper().writeValueAsString(data);
}
static String formatCsv(Object data) {
return new CsvMapper().writeValueAsString(data);
}
}
These methods are grouped because they all “format something,” but XML formatting has nothing to do with CSV formatting. Removing formatCsv() would not affect the other two methods in any way. The connection is purely categorical.
Coincidental Cohesion (worst)
Elements in a module are not related other than being in the same source file. This is the most harmful form of cohesion.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Utils {
static void sendEmail(String to, String body) {
// email logic
}
static Money calculateTax(Money amount, TaxRate rate) {
// tax logic
}
static byte[] compressFile(byte[] data) {
// compression logic
}
static String formatDate(LocalDate date) {
// date formatting logic
}
}
Email sending, tax calculation, file compression, and date formatting have absolutely nothing in common. This class exists because someone needed a place to put code and chose the path of least resistance. Every project has at least one class like this, and it tends to grow without bound.
LCOM: Measuring Cohesion with Numbers
Talking about cohesion in qualitative terms is useful, but sometimes you want a number. Lack of Cohesion of Methods (LCOM) is a metric that measures the structural cohesion of a class by examining which methods access which fields.
The idea is simple: if every method in a class uses every field, the class is perfectly cohesive. If methods cluster into groups that each touch a separate set of fields, the class is doing too many things.
Consider the three classes shown below.
Fields appear as single letters and methods appear as blocks.
In Class X, the LCOM score is low, indicating good structural cohesion.
Class Y, however, lacks cohesion; each of the field/method pairs in Class Y could appear in its own class without affecting behavior.
Class Z shows mixed cohesion, where developers could refactor the last field/method combination into its own class.
Step-by-Step LCOM Calculation
LCOM is easier to understand with a concrete example. Consider a class with three fields and four methods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ReportGenerator {
private DataSource dataSource; // field a
private Formatter formatter; // field b
private AuditLogger auditLogger; // field c
void fetchData() { // uses: a
dataSource.query();
}
void formatReport() { // uses: a, b
formatter.format(dataSource.getResults());
}
void printReport() { // uses: b
System.out.println(formatter.getOutput());
}
void logAuditTrail() { // uses: c
auditLogger.log("Report generated");
}
}
First, list which fields each method accesses:
| Method | Fields accessed |
|---|---|
fetchData() | a |
formatReport() | a, b |
printReport() | b |
logAuditTrail() | c |
Next, for every pair of methods, check whether they share at least one field:
| Method pair | Shared fields? |
|---|---|
fetchData() – formatReport() | Yes (a) |
fetchData() – printReport() | No |
fetchData() – logAuditTrail() | No |
formatReport() – printReport() | Yes (b) |
formatReport() – logAuditTrail() | No |
printReport() – logAuditTrail() | No |
Count the pairs that share no fields: 4 (P). Count the pairs that share at least one field: 2 (Q).
LCOM = P - Q (if positive, otherwise 0) LCOM = 4 - 2 = 2
An LCOM of 2 tells us this class has some disconnected clusters. And looking at the code, that checks out: logAuditTrail() is completely isolated from the rest. It is an extraction candidate. After pulling logAuditTrail() (and its auditLogger field) into a separate AuditLogger class, the remaining three methods form a tighter unit with a lower LCOM score.
Practical Interpretation of LCOM Scores
As a rule of thumb: an LCOM value of 1 indicates a perfectly cohesive class, where every method accesses every field and the class has a single, focused responsibility. As the score increases, it signals that the class likely contains distinct clusters of behavior that could be separated. In practice, I start paying attention when LCOM exceeds 2 or 3 for a class with more than a handful of methods. A very high LCOM score (say, above 5) is a strong indicator that the class is doing too much and should be split. Of course, context matters: a data-transfer object with many independent getters will naturally have a higher LCOM, and that is acceptable.
High vs. Low Cohesion: A Code Example
A low-cohesion class bundles unrelated responsibilities together:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Low cohesion: this class handles user data, email sending, AND logging
class UserManager {
private String userName;
private String email;
private List<String> logs;
void updateUserName(String name) {
this.userName = name;
}
void sendWelcomeEmail() {
// uses only 'email', has nothing to do with 'logs' or 'userName'
EmailService.send(email, "Welcome!");
}
void addLog(String message) {
// uses only 'logs', unrelated to user data or email
logs.add(message);
}
}
A high-cohesion design separates these into focused classes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// High cohesion: each class has a single, focused responsibility
class User {
private String userName;
private String email;
void updateUserName(String name) {
this.userName = name;
}
String getEmail() {
return email;
}
}
class WelcomeEmailSender {
void send(String email) {
EmailService.send(email, "Welcome!");
}
}
class AuditLog {
private List<String> logs;
void addLog(String message) {
logs.add(message);
}
}
In the low-cohesion version, the three methods operate on disjoint sets of fields, so the LCOM score would be high. After splitting, each class has methods that all work with the same fields, resulting in strong cohesion and a low LCOM score.
Cohesion and Coupling: Two Sides of the Same Coin
Cohesion and coupling are complementary metrics. They are not independent concerns; they directly influence each other.
When a module has high cohesion, its internals are self-contained. It does not need to reach into other modules for data or behavior that should be local. This naturally reduces the number of connections between modules, which means lower coupling.
When a module has low cohesion, its responsibilities are scattered. Some of its behavior belongs elsewhere, and it often needs to collaborate with many other modules to get anything done. The result is higher coupling: more dependencies, more shared state, more coordination.
Think of it this way: if you split a poorly cohesive class into three focused classes, those three classes might need to talk to each other (increasing coupling). But if you split it along the right seams, where each new class fully owns its data and behavior, the coupling between them will be minimal. The key is that the split should follow the natural clusters of related functionality, not arbitrary boundaries.
In practice, I have found that focusing on cohesion first tends to solve coupling problems for free. When every module has a clear, singular purpose, the interfaces between modules become narrow and well-defined.
How to Improve Cohesion During Design
Recognizing low cohesion is only half the battle. Here are practical steps I use when designing or refactoring classes:
The naming test. If you cannot name your class without using “and,” “or,” or “Manager,” it probably has too many responsibilities.
OrderValidatoris focused.OrderValidatorAndNotifieris a red flag.Look for orphan methods. Methods that share no fields with other methods in the same class are extraction candidates. They are living in the wrong home. The LCOM calculation above shows exactly how to spot them.
Extract until every method works with the same core state. After extraction, every remaining method in the class should operate on the same set of fields. If you have methods that touch field
aand methods that touch fieldbbut nothing touches both, you likely have two classes hiding inside one.Watch for flag parameters. A method that takes a boolean or enum to decide what behavior to execute is often a sign of logical cohesion. Consider replacing it with separate classes or a strategy pattern.
Follow the data. If a group of fields always travels together (passed as parameters, stored together, validated together), they probably belong in their own class. This is the “data clump” smell, and extracting it often improves cohesion in the original class.
Revisit after every feature. Cohesion tends to degrade over time as new requirements are bolted onto existing classes. A class that was perfectly cohesive six months ago might have grown a new responsibility that does not belong. Periodic review keeps things clean.
References
- Larry Constantine and Edward Yourdon, Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design (Prentice Hall, 1979)
- Shyam Chidamber and Chris Kemerer, “A Metrics Suite for Object Oriented Design,” IEEE Transactions on Software Engineering 20, no. 6 (1994)
Related posts: What is coupling?

