Skip to content
← All posts

Cohesion in Software Design: From Worst to Best

9 min read
software-engineeringpatterns

If coupling is about how much modules depend on each other, cohesion is about how well the parts inside a single module belong together. A highly cohesive module does one thing well. A low-cohesion module is a grab bag of unrelated functionality stuffed into the same file.

High cohesion is what you want. It means every function, every variable, every line of code in a module is working toward the same purpose. When you open the file, you immediately understand what it does. When you need to change something, you know exactly where to look.

Low cohesion is the opposite. The module does five different things, and changing one of them risks breaking the other four. Nobody can explain what the module is "for" in a single sentence.

Why cohesion matters

Consider two modules. Module A handles user authentication: login, logout, password reset, session management. Module B handles user authentication, email formatting, PDF generation, and database backups.

Module A is cohesive. Everything in it relates to one concern. When you need to change how sessions work, you open one file and you are confident the change is contained.

Module B is a disaster. It exists because someone needed a place to put code and this file was already open. When you need to change database backups, you are editing a file called UserAuth.py, and you have no idea what else might break.

The cost of low cohesion compounds over time. Every new developer has to read the entire module to understand which parts relate to which concern. Every change carries risk because unrelated functionality shares the same space. Every bug could be anywhere.

The cohesion spectrum

Like coupling, cohesion exists on a spectrum. Here are the types, ranked from worst to best.

Low cohesion

Coincidental cohesion (worst)

A module has coincidental cohesion when its parts have no meaningful relationship at all. They are in the same module by accident.

# Coincidental cohesion: these have nothing to do with each other
class Utilities:
    def format_date(self, date):
        return date.strftime("%Y-%m-%d")

    def calculate_shipping(self, weight, distance):
        return weight * 0.5 + distance * 0.1

    def send_email(self, to, subject, body):
        ...

    def compress_image(self, image, quality):
        ...

This is the classic "Utils" or "Helpers" class. It exists because the developer needed somewhere to put code that did not obviously belong anywhere else. The functions are completely unrelated. If you need to change date formatting, you are editing the same file as image compression. There is no logical connection.

The fix is straightforward: break this into focused modules. DateFormatter, ShippingCalculator, EmailService, ImageProcessor. Each one has a single purpose.

Logical cohesion

A module has logical cohesion when its parts are grouped because they are the same category of thing, not because they work together.

# Logical cohesion: all "validators" but completely unrelated domains
class Validators:
    def validate_email(self, email):
        return "@" in email and "." in email

    def validate_credit_card(self, number):
        # Luhn algorithm
        ...

    def validate_json_schema(self, data, schema):
        ...

    def validate_blood_pressure(self, systolic, diastolic):
        return systolic > 0 and diastolic > 0

These are all "validation" functions, but they validate completely different things in completely different domains. Email validation has nothing to do with blood pressure checks. Grouping them by category (they all validate) rather than by domain (they all relate to the same feature) is a sign of logical cohesion.

Temporal cohesion

A module has temporal cohesion when its parts are grouped because they happen at the same time, not because they are logically related.

# Temporal cohesion: all run at startup, but unrelated
def initialize_app():
    load_config()
    connect_to_database()
    start_background_jobs()
    send_startup_notification()
    clear_temp_files()
    warm_cache()

These all run during startup, but loading config has nothing to do with clearing temp files. If you need to change how the cache warms, you are editing a function whose name says "initialize app." The grouping is based on when things happen, not what they do.

Medium cohesion

Procedural cohesion

A module has procedural cohesion when its parts are grouped because they follow a specific sequence. They must execute in order, but they are not necessarily operating on the same data.

# Procedural cohesion: steps happen in order but work on different data
def process_order(order):
    validated = validate_order(order)
    inventory = check_inventory(validated.items)
    charge_customer(order.customer, order.total)
    update_inventory(inventory)
    send_confirmation_email(order.customer.email)

The functions are related by sequence, not by data. charge_customer works on payment data. send_confirmation_email works on email data. They are in the same module because they happen in a specific order, not because they operate on the same information.

Communicational cohesion

A module has communicational cohesion when its parts operate on the same data but perform different operations on it.

# Communicational cohesion: all work on the same "report" data
class ReportProcessor:
    def __init__(self, report_data):
        self.data = report_data

    def generate_summary(self):
        return summarize(self.data)

    def generate_chart(self):
        return plot(self.data)

    def export_to_pdf(self):
        return render_pdf(self.data)

    def email_report(self):
        send_email(attachment=render_pdf(self.data))

All functions work on the same report data, which is a stronger connection than coincidental or procedural cohesion. But they perform fundamentally different operations: summarizing, charting, exporting, and emailing. Changing the PDF rendering has nothing to do with changing the chart generation, even though they use the same data.

High cohesion

Sequential cohesion

A module has sequential cohesion when the output of one part is the input to the next. The parts form a pipeline where data flows through a sequence of transformations.

# Sequential cohesion: each step feeds the next
class TextProcessor:
    def process(self, raw_text):
        cleaned = self.remove_html_tags(raw_text)
        normalized = self.normalize_whitespace(cleaned)
        tokens = self.tokenize(normalized)
        return self.stem_words(tokens)

    def remove_html_tags(self, text):
        ...

    def normalize_whitespace(self, text):
        ...

    def tokenize(self, text):
        ...

    def stem_words(self, tokens):
        ...

Each function takes the output of the previous function as input. They all work on the same data as it flows through transformations. The module has a clear, linear purpose: take raw text and process it into stemmed tokens. This is closely related to the pipe-and-filter architecture pattern.

Functional cohesion (best)

A module has functional cohesion when every part contributes to a single, well-defined task. Nothing is extra. Nothing is missing. The module does one thing, and every piece of it exists to support that one thing.

# Functional cohesion: everything serves one purpose
class Stack:
    def __init__(self):
        self._items = []

    def push(self, val):
        self._items.append(val)

    def pop(self):
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()

    def peek(self):
        if not self._items:
            raise IndexError("peek at empty stack")
        return self._items[-1]

    def is_empty(self):
        return len(self._items) == 0

    def size(self):
        return len(self._items)

Every method in this class exists to support one purpose: LIFO stack operations. There is no send_email hiding in here. There is no format_date. Every line of code is about being a stack. If you ask "what does this module do?" the answer is one sentence: "It is a stack."

This is what you should aim for. Not every module will achieve perfect functional cohesion, but the closer you get, the easier your code is to understand, test, and maintain.

The cohesion spectrum, summarized

TypeLevelDescription
CoincidentalWorstParts have no relationship (the "Utils" class)
LogicalLowGrouped by category, not by purpose
TemporalLowGrouped because they happen at the same time
ProceduralMediumGrouped because they follow a sequence
CommunicationalMediumGrouped because they use the same data
SequentialHighOutput of one part feeds into the next
FunctionalBestEvery part contributes to a single task

Cohesion and coupling go together

High cohesion and loose coupling are two sides of the same coin. When each module has a single, focused responsibility (high cohesion), it naturally has fewer reasons to depend on other modules (loose coupling). When a module does too many things (low cohesion), it needs to reach into many other modules to get its work done (tight coupling).

If you find yourself with a tightly coupled system, the fix is often to increase cohesion. Split the bloated modules into focused ones. Once each module has a clear purpose, the unnecessary dependencies fall away.

The reverse is also true. If you are struggling to make a module cohesive, check whether it has too many incoming dependencies. A module that 15 other modules depend on often ends up accumulating unrelated functionality because "this is where everyone looks."

How to spot low cohesion

Here are the warning signs:

  • The module name is vague. "Utils", "Helpers", "Manager", "Service" without a qualifier. If you cannot describe the module's purpose in one sentence, its cohesion is probably low.
  • You cannot explain how two functions in the same module relate. If format_date and compress_image are in the same file, ask yourself why.
  • Changes to one feature require editing a module that "should not" be involved. If fixing a bug in email sending means editing PaymentProcessor.py, something is in the wrong place.
  • The module has too many imports. A module that imports from 15 different packages is probably doing 15 different things.
  • Tests for the module are hard to write. If setting up a test requires mocking 8 unrelated dependencies, the module is doing too much.

A useful rule of thumb: if you can split a module into two pieces and neither piece needs the other, the original module had low cohesion. The parts were independent, which means they did not belong together.

High cohesion in practice

You do not need to obsess over which exact type of cohesion a module has. The practical goal is simple: every module should have a clear, single purpose that you can state in one sentence.

  • A Stack class manages LIFO storage.
  • A PriceCalculator computes prices given items and tax rules.
  • A TokenRefresher handles OAuth token refresh logic.
  • A BinarySearch module provides search over sorted data.

When you can describe a module this clearly, it is cohesive enough. When you struggle to explain what a module does without using the word "and," it probably needs to be split.

These same principles apply at every scale. A function should do one thing. A class should have one responsibility. A module should serve one purpose. A microservice should own one domain. The vocabulary changes, but the principle is the same: group things that belong together and separate things that do not.

The takeaway

Cohesion is about focus. A highly cohesive module does one thing and does it well. Every part of the module contributes to that one purpose. Nothing extra, nothing missing.

Low cohesion creates modules that are hard to understand, hard to test, and hard to change safely. High cohesion creates modules that are easy to work with because you always know what they do and what they do not do.

The next time you create a new file or class, ask yourself: can I describe its purpose in one sentence? If the answer is yes, you are on the right track. If the answer requires the word "and," consider splitting it.

Related posts