codeintelligently
Back to posts
Code Intelligence & Analysis

Coupling and Cohesion: Metrics That Predict Maintainability

Vaibhav Verma
7 min read
couplingcohesionsoftware architecturecode metricsmaintainability

Coupling and Cohesion: Metrics That Predict Maintainability

Every developer learns about coupling and cohesion in their first software engineering class. "High cohesion, low coupling" is the mantra. Then they enter the real world and never measure either one.

I've reviewed over 300 codebases in consulting engagements, and I can tell you that the single best predictor of how painful a codebase will be to work with isn't test coverage, code complexity, or even documentation. It's the coupling-to-cohesion ratio. Teams that track and optimize this ratio ship faster and have fewer incidents. Teams that don't end up in a death spiral where every change breaks something unexpected.

Let me show you how to measure both, what the numbers actually mean, and how to use them to make your codebase better.

Coupling: The Connections Between Modules

Coupling measures how dependent one module is on another. The more a module needs to know about other modules to function, the higher its coupling.

There are several types, ranked from least to most problematic:

  1. Data coupling: Modules share data through parameters. Clean and explicit.
  2. Stamp coupling: Modules share a data structure, but each only uses part of it.
  3. Control coupling: One module controls the behavior of another via flags or parameters.
  4. Common coupling: Modules share global state.
  5. Content coupling: One module directly modifies the internals of another.

In practice, here's what these look like in a TypeScript codebase:

typescript
// Data coupling (GOOD) - explicit, minimal dependency
function calculateTax(amount: number, rate: number): number {
  return amount * rate;
}

// Stamp coupling (OK) - receives more data than needed
function formatUserName(user: User): string {
  return `${user.firstName} ${user.lastName}`;
  // user has 20 fields, we only need 2
}

// Control coupling (WATCH) - behavior controlled by caller
function fetchData(url: string, useCache: boolean): Promise<Data> {
  if (useCache) { /* ... */ }
  else { /* ... */ }
}

// Common coupling (BAD) - shared mutable state
let currentUser: User | null = null; // global state
function getPermissions() { return currentUser?.permissions; }
function setUser(user: User) { currentUser = user; }

// Content coupling (TERRIBLE) - reaching into internals
class OrderProcessor {
  process(cart: ShoppingCart) {
    cart._items.forEach(item => { // accessing private field
      item._price = item._price * 0.9; // modifying internal state
    });
  }
}

Measuring Coupling

You can measure coupling at different levels of granularity:

File-level coupling (easiest to measure):

bash
# Count imports per file in a TypeScript project
for file in $(find src -name "*.ts" -not -path "*/node_modules/*"); do
  imports=$(grep -c "^import" "$file" 2>/dev/null || echo 0)
  echo "$imports $file"
done | sort -rn | head -20

Module-level coupling (most useful):

Tools like Dependency Cruiser visualize and quantify coupling between modules:

bash
npx depcruise src --output-type dot | dot -T svg > dependency-graph.svg

The output shows you every dependency in your codebase as a directed graph. Dense graphs mean high coupling.

Afferent and efferent coupling (most precise):

  • Afferent coupling (Ca): How many modules depend ON this module. High Ca means the module is widely used; changing it is risky.
  • Efferent coupling (Ce): How many modules this module depends ON. High Ce means the module knows about too many things.

The instability metric is Ce / (Ca + Ce). A value near 1 means the module is unstable (depends on many, depended on by few). A value near 0 means it's stable (depended on by many, depends on few).

Stable modules should be abstract (interfaces, base classes). Unstable modules should be concrete (implementations). When you violate this principle, you get pain.

Cohesion: The Focus Within Modules

Cohesion measures how related the responsibilities within a single module are. High cohesion means a module does one thing well. Low cohesion means it's a grab bag of loosely related functions.

Types of cohesion, from worst to best:

  1. Coincidental: Functions are in the same module by accident. A "utils" file.
  2. Logical: Functions are related by category, not by actual dependency. "All string functions."
  3. Temporal: Functions are grouped because they run at the same time. "Startup routines."
  4. Procedural: Functions are grouped because they're called in sequence.
  5. Communicational: Functions operate on the same data.
  6. Sequential: Output of one function feeds into the next.
  7. Functional: Everything in the module contributes to a single, well-defined task.

Measuring Cohesion

Cohesion is harder to measure quantitatively, but there are reliable proxies:

LCOM (Lack of Cohesion of Methods): For a class, LCOM counts the number of method pairs that don't share instance variables minus the number that do. An LCOM of 0 means perfect cohesion. Higher numbers mean the class should probably be split.

File size distribution: This is a rough but useful proxy. Plot the distribution of file sizes in your codebase. Files that are 3x the median size probably have low cohesion.

bash
# File size distribution for TypeScript files
find src -name "*.ts" -not -path "*/node_modules/*" -exec wc -l {} + | \
  sort -rn | head -20

Method count per class/module: A module with 30+ exported functions almost certainly has low cohesion.

The "name test": Can you describe what the module does in one sentence without using "and"? If not, it probably has low cohesion. "This module handles user authentication" passes. "This module handles user authentication and email notifications and audit logging" doesn't.

The Coupling-Cohesion Ratio

Here's the framework I use. I call it the GRIP Score:

G - Graph it: Visualize your dependency graph. Use Dependency Cruiser, Madge, or similar tools. Look for clusters (good) and tangles (bad).

R - Rate each module: Score each module on two axes: coupling (count of dependencies in + out) and cohesion (LCOM score or the name test). Plot them on a 2x2 matrix.

I - Identify the worst offenders: Modules with high coupling AND low cohesion are your top refactoring priorities. These are the modules that know about everything and do everything. They're modification magnets and bug factories.

P - Plan the split: For each offender, identify the distinct responsibilities it contains. Design a refactoring plan that increases cohesion (splitting into focused modules) while reducing coupling (introducing interfaces or dependency injection).

The Contrarian Take: Some Coupling Is Good

The zero-coupling ideal is a myth, and chasing it creates worse problems than the coupling itself.

I've seen teams introduce so many abstraction layers to eliminate coupling that understanding a single request path required reading 12 files. The coupling between modules was low, but the cognitive coupling for any developer trying to understand the system was through the roof.

The goal isn't zero coupling. It's intentional coupling. You want your dependency arrows to point in one direction (toward stable abstractions), you want coupling to be explicit (imports, not global state), and you want the coupling graph to match your mental model of the system.

A well-coupled system where every dependency makes sense is better than a decoupled system where tracing a feature requires a treasure map.

Practical Next Steps

  1. Run Dependency Cruiser on your project today. Just seeing the graph is eye-opening.
  2. Identify your top 3 modules by efferent coupling. Ask: does this module really need to know about all of these things?
  3. Apply the name test to your largest files. If you can't describe what they do in one sentence, they need splitting.
  4. Track coupling metrics monthly. The trend matters more than the absolute number.

These aren't academic exercises. Coupling and cohesion directly predict how fast your team can ship changes and how many bugs those changes will introduce. Measure them.

$ ls ./related

Explore by topic