Cohesion
Cohesion is the degree to which the contents of a module belong together, and the word is what lets a team tell a module that has a clear purpose from one that’s a grab bag.
What It Is
Cohesion is the degree to which the parts inside a module, component, file, package, or service belong together. A module is highly cohesive when everything inside contributes to a single, nameable purpose. It’s weakly cohesive when the contents share a location but not a reason: a junk drawer that grew because someone had to put each piece somewhere. The word is the measurement of that fit, not a verdict on whether a module is “good”; cohesion is one axis among several, and it pairs with coupling as the two oldest measures of structural quality.
Practitioners have been refining a hierarchy of cohesion types since the 1970s, from worst to best:
- Coincidental cohesion. Elements are together because they happened to be put there. A 2,000-line
utils.pywith date formatters, retry logic, string sanitizers, and config loaders is the textbook case. There is no reason behind the grouping, and that fact is doing the structural damage. - Logical cohesion. Elements are grouped because they are the same kind of thing, even when they don’t cooperate. A package called
validatorsthat holds an email validator, a phone validator, and a credit-card validator is logically cohesive; the parts share a category but not a flow. - Temporal cohesion. Elements are grouped because they run at the same time. A startup module that initializes logging, opens the database, primes the cache, and registers signal handlers is temporally cohesive; the parts share a moment, not a purpose.
- Procedural cohesion. Elements are grouped because they’re steps in a procedure. The grouping is real but shallow — the procedure could be reordered or rerouted without the module noticing.
- Communicational cohesion. Elements are grouped because they operate on the same data. A
Customermodule that reads and writes the customer record from a dozen angles holds together around the data, even when the operations aren’t otherwise related. - Sequential cohesion. Elements are grouped because the output of one is the input of the next. A pipeline module that parses, normalizes, and emits a record is sequentially cohesive — there’s a chain, and the chain is the reason.
- Functional cohesion. Elements are grouped because they all contribute to a single, well-defined task. Every function in the module pulls in the same direction; you can describe what the module does in one sentence without using “and.” Strongest and the default target.
The hierarchy isn’t a checklist to climb in every refactor. It’s vocabulary that lets a team say more than “this module feels off.” A module suffering from coincidental cohesion needs a different intervention than one suffering from temporal cohesion, and the names are what make the intervention designable.
In agentic coding the same vocabulary applies, with the stakes adjusted upward. An agent reading a cohesive module can hold its single purpose in its context window and reason about edits against that purpose. An agent reading a coincidentally cohesive module has to load five unrelated stories at once and try to figure out which one the current task is actually about. The hierarchy isn’t only a human-readability measure anymore; it’s a measure of how confidently an agent can edit one module without surfacing accidental side effects in another.
Why It Matters
Software lives or dies by whether you can find the right place to make a change. Cohesion is the property that decides whether finding the place is fast or slow. In a highly cohesive system, the answer to “where does this new behavior go?” is usually obvious within seconds, because each module’s purpose is sharp. In a system riddled with coincidental cohesion, the answer is “it could go in any of these six files,” and whichever file you pick will silently make the next person’s life harder.
Naming the property collapses dozens of micro-debates into a single question: does this change push the module toward a single purpose or away from one? A team fluent in cohesion doesn’t argue case-by-case about whether a new helper goes here or there. They ask whether the proposed home already has a purpose the helper extends, or whether the helper would be the third unrelated thing in the file. The vocabulary turns gut-feel objections into legible ones.
There’s a second-order effect on every other structural property. Highly cohesive modules are smaller and more nameable, which makes boundaries easier to draw. They have fewer reasons to change, which pushes coupling down. They map cleanly onto domain concepts and onto ownership, which means a single team or agent can hold the whole module in mind. Most structural improvements are downstream of cohesion, which is why teams that ignore it spend their time treating symptoms rather than causes.
For agentic workflows the diagnostic urgency is sharper. An agent given a cohesive module can be told “work inside notifications/ and respect its public surface” and stay inside the scope. An agent given a low-cohesion utils.py has no scope to stay inside; the module’s purpose is “miscellaneous,” and miscellaneous doesn’t bound anything. Cohesion is what makes the agent’s read set tractable, the agent’s write set safe, and the agent’s reasoning fast, because the module isn’t asking the agent to hold five unrelated stories in mind at once.
How to Recognize It
A cohesive module feels like it has a clear answer to the question “what does this do?” A low-cohesion module either can’t answer that question without “and” or answers it with a category (“utilities”) rather than a purpose. A few concrete signs of low cohesion:
- The one-sentence-without-“and” test. Try to describe what the module does in a single sentence. If you can’t get there without “and” — “it handles authentication and email formatting and logging configuration” — the module is doing too much, and the cohesion is at best logical.
utils,helpers,common, ormiscin the name. Names that don’t promise anything specific are an honest signal that the contents don’t share a specific purpose. Sometimes acceptable for genuinely tiny shared primitives; usually a warning sign that the file became a junk drawer.- Files that grew past the team’s reading-comfort threshold. When
payments.pyis 3,000 lines, the issue isn’t usually line count. It’s that several unrelated responsibilities migrated into it because it was the most convenient place to land. - Imports that span unrelated subsystems. A module importing from networking, persistence, templating, and timezones is probably wearing several hats. The import set is a quick map of what the module reaches into, and a sprawling map suggests sprawling responsibility.
- Reason-for-change diversity. A useful version of the “single responsibility” question: how many distinct kinds of change would force you to edit this file in the next quarter? One kind is fine. Five kinds means the module is five modules pretending to be one.
- Inconsistent style or abstraction level inside one file. When the first half of the module is high-level orchestration and the second half is low-level byte twiddling, two cohesive modules have been zipped together into one incoherent one.
- The agent reads farther than it edits. An agent asked to change one function in a module ends up loading the whole 3,000-line file because it can’t tell what’s relevant. The module’s purpose isn’t bounded enough to scope the read.
A few cohesion-prone patterns are worth recognizing in their own right:
utils.pyaccretion. Any module whose name is a catch-all will keep accreting catch-all contents until someone names the implicit categories inside it and breaks them out.- Layer-only grouping. Putting all controllers in one package and all models in another tends to produce low cohesion within each package — the contents share a kind (logical cohesion) without sharing a purpose. Most teams eventually slice the other way too, grouping by feature so that the feature’s controller, model, and view live together.
- God modules. A single module that “knows about everything” because it grew faster than the boundary around it. The cohesion gradient between it and the rest of the system tells you which side is the problem; almost always, the god module is the side to split.
- Junk-drawer inheritance bases. A base class that exists only so several subclasses can share three or four small methods. The base isn’t a concept; it’s a place to dump cross-cutting helpers. Naming it as a cohesion failure is what unblocks the refactor.
The honest test for cohesion is the rename test: can you give this module a name that promises exactly what’s inside it? If the answer is yes and the name is shorter than the file, the module is probably cohesive. If you need a slash or an “and” in the name, or if the only honest name is “miscellaneous,” that’s the cohesion problem talking.
How It Plays Out
A backend team inherits a 2,000-line utils.py that nobody owns. Date formatting functions live next to HTTP retry logic, which lives next to string sanitizers, which lives next to configuration loaders. Every new contributor adds to the file because that’s where “small things” go, and the file keeps growing. The team finally splits it into four cohesive modules: date_utils.py, http_retry.py, sanitizers.py, and config.py. Each is small enough to understand at a glance, each has a single owner, and the next “small thing” someone adds has a real home or forces an explicit naming conversation rather than landing in a junk drawer by default.
A platform team realizes its Customer service is doing three jobs: it stores the canonical customer record, it sends billing-related emails, and it computes a churn-risk score for the analytics team. Each job changes for different reasons and on different cadences. A schema change forces a deployment of the emailer that didn’t need to change; a marketing-driven tweak to the churn formula forces a redeployment of the storage layer that didn’t need to change either. The team splits the service into three cohesive ones (CustomerStore, CustomerNotifications, and ChurnScoring), each connected to the others through a defined contract. Each can now evolve on its own cadence, owned by the team that cares about it, with no accidental coupling through a shared deployment.
An AI agent is asked to fix a bug in notification delivery. The project has a notifications/ module containing only notification-related code: templates, delivery logic, preference management. The agent reads the module, understands the full picture, and fixes the bug in one pass. In a parallel timeline where the notification code is scattered across a generic services.py, the agent loads the whole file, finds itself reading payment retry logic and email parsing along the way, makes a “fix” that touches an adjacent function it didn’t fully understand, and breaks the cart at checkout. Same agent, same bug, same prompt — the difference is cohesion.
“Split utils.py into cohesive modules: date_utils.py for date formatting, http_retry.py for retry logic, sanitizers.py for string cleaning, and config.py for configuration loading. Update all imports. Don’t change any function bodies.”
Consequences
When a team has the vocabulary of cohesion and uses it deliberately, the system gets easier to navigate without anyone making a navigation decision. Files have promises in their names, and the contents keep those promises. New code has obvious homes, and the conversation about where to add something becomes a five-second one rather than a five-minute one. Agents can be turned loose inside a single module with the read set bounded by the module’s purpose, and the agent’s edits stay scoped to that purpose.
The honest tradeoffs are worth naming, because pushing cohesion up the hierarchy isn’t free.
- More modules, more boundaries to manage. Highly cohesive systems produce a larger number of small, single-purpose modules. The total surface area between modules grows, which raises the cost of cross-module navigation. Some teams over-correct and end up with hundreds of micro-modules where the cost of crossing dominates the cost of working.
- The wrong cohesive cut is hard to undo. A boundary placed around a “single purpose” the team later realizes wasn’t actually single calcifies fast. The naming and the public surface get embedded in callers, tests, and dependencies, and the refactor to re-cut becomes expensive. The vocabulary helps you delay the cut until you understand the purpose, not rush a confident-looking cut on the first try.
- Cohesion can’t always be measured locally. A module looks cohesive on its own but turns out to be cohesive with another module that is calling into its private implementation. The two are conceptually one module pretending to be two. The vocabulary helps you spot this, but it requires reading across the boundary.
- Some grouping by mechanism is necessary. A team that insists on functional cohesion everywhere ends up with no shared infrastructure modules. Some legitimately cross-cutting concerns (logging, telemetry, error-handling helpers) live best in modules that are logically cohesive by design. The taxonomy doesn’t make those modules wrong; it makes them legible.
The goal isn’t maximal cohesion; the goal is appropriate cohesion at the right level of the hierarchy, with each module’s purpose nameable and the parts inside it pulling the same direction.
Related Patterns
Sources
- Larry Constantine introduced cohesion and coupling as named measures of modular design in the late 1960s, presenting an early version at the 1968 National Symposium on Modular Programming. The cohesion-as-degree-of-fit framing used throughout this article is Constantine’s.
- Wayne Stevens, Glenford Myers, and Larry Constantine’s “Structured Design” (IBM Systems Journal, 1974) is the paper that formalized the cohesion spectrum from coincidental to functional. It became one of the most-requested reprints in the journal’s history and supplies the seven-level hierarchy reproduced in What It Is.
- Edward Yourdon and Larry Constantine’s Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design (Prentice-Hall, 2nd ed. 1979) is the canonical book-length treatment and the source most later textbooks draw from.
- Robert C. Martin’s “single responsibility” framing — that a module should have one reason to change — is a later restatement of functional cohesion in object-oriented vocabulary, and the reason-for-change test in How to Recognize It is Martin’s principle restated as a cohesion question.