Software Dev Notes

Home › Software Architecture

Coupling and Cohesion: The Two Measures That Shape Maintainable Modules

Understand coupling and cohesion, the two classic measures of module design, with examples, a cohesion scale and practical steps to improve both.

2026-10-19 · 6 min read

Most advice about software design comes down to two questions. How much does one part of the code depend on another? And how closely related are the things inside one part? These two questions are called coupling and cohesion. Structured design researchers, including Larry Constantine and his colleagues in the 1970s, described them as the main measures of module quality, and they remain the most useful vocabulary for judging a codebase today. This post defines both, shows how to recognize each in real code, and gives a method for improving them.

What coupling means

Coupling is the degree to which one module depends on the internals of another. Two modules are loosely coupled when a change in one rarely forces a change in the other. They are tightly coupled when a small edit ripples across several files.

Coupling comes in several forms, and the forms are not equally harmful. Data coupling is the mildest: one module passes a simple value, such as a number or a string, to another. Control coupling is stronger, because one module tells another what to do by passing a flag that changes its behavior. Content coupling is the worst, because one module reads or writes the private state of another directly. Many real systems also have common coupling, where several modules share a global variable.

A simple test helps. Ask what would break if you renamed a private field in module A. If the answer is nothing, the coupling is healthy. If the answer is a list of files in module B, you have content coupling that deserves attention.

What cohesion means

Cohesion is the degree to which the elements inside one module belong together. A cohesive module does one job and every part of it serves that job. A weakly cohesive module is a grab bag, where unrelated functions sit side by side because they were convenient to put there.

Constantine's work described a scale of cohesion types, from best to worst. Functional cohesion is the best: every element contributes to a single well-defined task, such as computing a tax amount. Sequential cohesion comes next, where the output of one step is the input of the next, as in a parser that reads and then validates. Communicational cohesion groups code that operates on the same data, such as a class that formats and saves one record. Procedural cohesion groups steps that run in a fixed order without sharing a clear purpose. Temporal cohesion groups code only because it runs at the same time, such as all startup tasks in one function. Logical cohesion groups code by category, such as a single module for all input handling regardless of purpose. Coincidental cohesion is the worst: elements are unrelated and share a file by accident.

The two measures in a table

Coupling and cohesion are not opposites in a strict sense, but they usually move in opposite directions when design goes wrong. Good design aims for high cohesion inside modules and low coupling between them.

Measure Goal Warning sign
Cohesion Each module has one clear purpose A file named utils with forty unrelated functions
Coupling Modules depend on small, stable interfaces Changing one class breaks tests in five other packages
Coupling (data) Pass only what is needed Passing an entire user object to read one field
Cohesion (type) Functional or communicational Temporal grouping such as "startup stuff"

A before and after example

Suppose a checkout module handles payment, inventory and email in one class. The payment code reads the inventory count directly and writes to the email queue. Cohesion is low, because three responsibilities share one class, and coupling is high, because a change to the email format forces a retest of payment logic.

A better design splits the class into three. A payment module exposes a function that takes an amount and a payment token and returns a result. An inventory module exposes a function that reserves stock. A notification module listens for an order-placed event and sends the email. The checkout coordinator calls the three in order. Each module now has one purpose, and each depends only on the small interface of the others. The email format can change without touching payment, and the payment provider can change without touching email.

Notice that the total amount of code may stay about the same. The gain comes from where the boundaries sit, not from writing less.

How to improve cohesion

Start by listing what a module does in one sentence. If the sentence needs the word "and" more than once, the module probably has several responsibilities. Next, look at which functions use which fields. Functions that touch the same data form natural groups. Move each group into its own module and see whether the names become clearer.

Another useful signal is change frequency. If two functions in the same file always change together, they belong together. If they change for different reasons, such as a business rule and a logging format, they should be separated. Version control history makes this pattern easy to see, because a file that appears in unrelated commits is often a cohesion problem.

How to reduce coupling

Reducing coupling usually means narrowing interfaces. Replace a parameter that carries a whole object with one that carries only the values needed. Replace a shared global with an argument passed in explicitly. Replace a direct call into another module's internals with a call to a public function that hides those internals.

Dependency injection is a common technique for loosening coupling. A class that needs a database receives an interface rather than creating a concrete connection itself. This makes the dependency visible in the constructor and allows a test to pass a fake. The class no longer knows which database exists, only that something capable of storing records was provided.

Events are another tool. When one module publishes an event such as "order placed," other modules can react without the publisher knowing they exist. This removes a direct dependency at the cost of making the flow of control harder to trace. Use events for side effects that do not need an immediate result, and direct calls where the caller needs an answer.

Common mistakes

A frequent error is treating low coupling as "no dependencies at all." Modules must depend on something, because a program is made of parts that cooperate. The goal is to depend on stable, narrow interfaces instead of volatile internals.

Another mistake is splitting too early. Creating many tiny modules before the boundaries are understood can lock in the wrong seams. A common approach is to keep code together until a change pressure appears, then split along the line that the pressure reveals.

Finally, be careful with the word "utils." A folder of helper functions often has low cohesion by definition. Name modules after the concept they serve, such as invoice-totals or session-tokens, and the boundaries become easier to defend.

Measuring progress

Metrics can support judgment but should not replace it. Tools exist that count afferent and efferent dependencies between packages, and that compute instability ratios. Those numbers help spot hotspots, such as a package that many others depend on and that itself depends on many others. Use them to find candidates for review, then read the code and decide.

Key takeaways

Coupling measures how much modules depend on each other's internals, and cohesion measures how well the parts of one module belong together. Aim for high cohesion, where each module does one clear job, and low coupling, where modules talk through small and stable interfaces. Use the sentence test and change-frequency signals to find weak cohesion, and use narrow parameters, injected dependencies and events to reduce coupling. Measure with tools when they help, but let the code's actual change history and your reading of it drive the decisions.

More in Software Architecture

Architecture

How Layered Architecture Works: Layers, Boundaries and Dependency Direction

Learn how layered architecture splits an application into presentation, business and data layers, and why dependency direction decides maintainability.

5 min read