# Cohesion and Coupling

Cohesion is how related a module's contents are; coupling is how dependent modules are on each other.

**Rests on:** Separation of Concerns

*Software Engineering · Software Architecture*


## Why learn this

This concept is the foundation for making every software architecture decision you will ever face. Understanding cohesion and coupling is the key to judging whether a system's boundaries are drawn well, whether a change will be cheap or expensive, and whether a codebase can be tested at all. It unlocks the rest of your library: **Layered Architecture**, **Hexagonal Architecture**, **Ports and Adapters**, **Dependency Inversion**, and **Microservice Cost Tradeoffs** — all of them are just different strategies for achieving high cohesion and loose coupling. Master these two ideas and you'll be able to look at any system — a monolith, a microservices fleet, an AI pipeline — and say where the pain will come from.


## The intuition

Think of a software system as a collection of boxes (called **modules**). Each box contains some code — functions, data, logic. Cohesion asks: *how closely related are the things inside one box?* Coupling asks: *how much does one box depend on the contents of another box?*

A module with **high cohesion** has a single, clear purpose. Everything inside it belongs there. You can name it in a sentence: "This module handles user authentication." Open it, and you find only authentication-related code. A module with **low cohesion** is a junk drawer. It contains a bit of this, a bit of that. You can't name it without an 'and': "This module handles user authentication, and also file I/O, and occasionally formatting dates." Nothing in it is closely related to the rest.

Coupling, on the other hand, is about the *connections between* boxes. Two modules are **loosely coupled** if they know very little about each other's internals. They communicate through a small, clear interface — like a plug and socket. One can be changed or replaced without disturbing the other. Two modules are **tightly coupled** if one reaches inside the other, reads its private variables, or relies on precise steps happening in a particular order. Change anything in one, and the other breaks.

The two ideas pull in the same direction. High cohesion inside modules naturally leads to loose coupling between them, because if each module has a single clear job, it exposes only what's needed to do that job. Low cohesion forces tight coupling, because a junk-drawer module has its tentacles in everything — other modules have to know about all the unrelated things it does just to use any part of it.

Real software starts out neat but rots toward low cohesion and tight coupling. Every time a developer adds a feature, they are tempted to bolt it onto an existing module because it's convenient. The code in each box becomes more unrelated, and the boxes become more tangled. This concept gives you the vocabulary to notice that rot and the target to push back toward.


## An analogy

Think of a restaurant kitchen. The kitchen is divided into stations: the grill, the salad prep, the pastry section, the dishwashing area. Each station has a clear purpose — this is **cohesion**. The grill station only grills; the pastry station only bakes. Any chef walking in knows where to find what they need and what each station is responsible for.

The stations also have a way of working together — this is **coupling**. The grill needs raw meat from the walk-in fridge; the pastry station needs butter from the same fridge. They communicate through a small, standard interface: a ticket with the order, a shelf where finished plates are placed. The grill chef doesn't need to know how the pastry chef folds croissant dough. The pastry chef doesn't need to know the grill's temperature settings. They just exchange what's necessary through the pass.

Now imagine the opposite — a kitchen where the grill chef stores their spices in the pastry section's cabinet, and the salad prep's cutting board is also used for plating desserts. The stations are now coupled: pastry can't work if the grill chef is in the way; salad prep can't be cleaned without disturbing the pastry station. And within each station, cohesion has broken down — the pastry section now contains spices that belong to the grill. The kitchen becomes a mess: nobody can work independently, every change to one station affects the others, and the whole operation grinds to a halt.

The analogy stops working when you consider that in software, the modules aren't people who can talk it out. They're code. A module can't adapt or communicate — it either knows the other module's internals or it doesn't. The 'interface' is not a plate on a shelf; it's a carefully defined set of function signatures and data structures. And in software, the cost of coupling grows non-linearly: two modules that are tightly coupled cause four times as many problems as one, not twice as many.


## Definition

Cohesion is the degree to which the elements inside a single module belong together; coupling is the degree to which one module depends on the internals of another. The goal is high cohesion and loose coupling.


## How it connects

This sits alongside **Separation of Concerns** and **Single Responsibility** as the foundational criteria for judging any architectural decision. Separation of Concerns says distinct responsibilities should live in distinct places; cohesion is how you tell whether a place actually holds one responsibility or several. Single Responsibility says a module should have one reason to change; cohesion is how you tell whether the contents justify that single reason. This concept will be the lens through which you later evaluate **Layered Architecture** (are the layers cohesive? is the dependency direction coupling only to the layer beneath?), **Hexagonal Architecture** (the core has high cohesion and zero coupling to the outside world), **Ports and Adapters** (ports are pure interfaces that decouple the core), and **Dependency Inversion** (depending on an abstraction instead of a concrete implementation is the ultimate way to loosen coupling).


## The mathematics

Cohesion and coupling are qualitative concepts — there is no perfect number that tells you a module is 'cohesive enough'. But we can reason about them quantitatively in a simplified way to build intuition. Think of a module as a collection of items: functions, variables, classes. Coupling can be thought of as the number of external references a module makes — how many times it reaches out to another module's internals. Cohesion is the opposite — the number of internal connections between the items inside a module, compared to the total possible connections.

Imagine two modules. Module A has 3 items inside it. If all 3 relate to each other (each pair has some connection), the internal connections are 3 pairs. Module B also has 3 items, but they're unrelated — maybe 0 connections. Module A has higher cohesion. Now count external connections: Module A references 2 things in Module B, and Module B references 5 things in Module A. That's tight coupling. If we can reduce those numbers — fewer references, smaller interfaces — the coupling loosens.

We won't use formulas to 'score' cohesion and coupling — the real world is too messy for that. But the arithmetic of counting connections is what makes the intuition precise: more internal connections (relatedness) is good; more external connections (dependence) is bad.

There is no single formula. But we can express the ideas: Let C_internal(m) be the number of meaningful internal connections in module m. High cohesion = high C_internal relative to the size of m. Let C_external(m, n) be the number of references from module m to module n's internals. Loose coupling = low C_external(m, n) for all pairs (m, n). Reducing C_external is the goal.


## A worked example

```
Two modules, A and B. Module A contains three functions: `calculate_total`, `apply_discount`, `format_receipt` — they all relate to pricing, so they relate to each other. Module B contains three functions: `calculate_total` (same as A? no, let's say `send_email`, `generate_report`, `log_error`) — these are unrelated. Count the internal connections in each module: each pair that relates counts as 1.

Step 1: Module A: pairs that relate = (calculate_total, apply_discount), (calculate_total, format_receipt), (apply_discount, format_receipt) = 3 pairs   # All three functions in A are about the same thing — pricing — so all three pairs are closely related.
Step 2: Module B: pairs that relate = (send_email, generate_report)? No. (send_email, log_error)? No. (generate_report, log_error)? No. = 0 pairs   # None of the functions in B relate to each other — one sends email, one makes reports, one logs. That's low cohesion.
Step 3: Now count external references: Module A calls 3 functions from Module B. Module B calls 5 functions from Module A. Total = 3 + 5 = 8 external references.   # Each external reference is a point of coupling. The more there are, the more A and B are entangled.
Step 4: If we refactor Module B into three separate modules (Email, Report, Logger), each with one function, the couplings change. Module A now calls only `Email.send()` (1 reference) and maybe `Report.generate()` (1 reference if needed), and the Logger function is called nowhere from A. Total external references from A = 2. Module B (now Email module) calls 0 functions from A (it's independent). Total = 2 references.   # By improving cohesion (each new module has one purpose), we also reduced coupling — external references dropped from 8 to 2.

Result: Higher cohesion (more internal connections per module) led to lower coupling (fewer external references). The arithmetic shows the two principles reinforcing each other.
```


## From scratch, in Python

```python
# Plain Python to demonstrate cohesion and coupling

# --- Low cohesion module: a junk drawer ---
class Utility:
    def __init__(self):
        pass

    def format_date(self, date):
        # format a date as DD/MM/YYYY
        return date.strftime("%d/%m/%Y")

    def send_email(self, address, subject, body):
        # pretend this sends an email
        print(f"Sending email to {address}: {subject}")

    def calculate_discount(self, price, percent):
        return price * (1 - percent/100)

    def log_error(self, message):
        with open("error.log", "a") as f:
            f.write(message + "\n")

# --- High cohesion: split into separate modules ---
class DateFormatter:
    def format(self, date):
        return date.strftime("%d/%m/%Y")

class EmailSender:
    def send(self, address, subject, body):
        # pretend this sends an email
        print(f"Sending email to {address}: {subject}")

class PricingCalculator:
    def apply_discount(self, price, percent):
        return price * (1 - percent/100)

class ErrorLogger:
    def log(self, message):
        with open("error.log", "a") as f:
            f.write(message + "\n")

# --- Loose coupling: modules only interact through clear interfaces ---
# Instead of Utility calling everything, each module is self-contained.
# If we want to send an email to a customer, we use EmailSender only.
# No module reaches into another's internals.

def main():
    formatter = DateFormatter()
    emailer = EmailSender()
    calculator = PricingCalculator()
    logger = ErrorLogger()

    # Use each module independently
    date_str = formatter.format(date(2025, 1, 1))
    logger.log("Application started")
    discounted = calculator.apply_discount(100, 10)
    # emailer.send("customer@example.com", "Discount!", "Your price: " + str(discounted))

# --- Tight coupling example: one module reaching into another's internals ---
class OrderService:
    def __init__(self, email_sender, logger):
        # This is okay if these are passed in (dependency injection)
        self.email = email_sender
        self.logger = logger

    def place_order(self, order):
        # This method reaches into order.amount directly, and also
        # reaches into logger's file path to change logging behaviour
        # This is tight coupling because it depends on logger's internals
        if order.amount > 100:
            # BAD: reaching into logger's internals
            self.logger.log("Big order, but log file path might change")
        # Actually the above is using logger's public interface, but suppose
        # we wanted to change the log file location from within OrderService.
        # That would reach into ErrorLogger's internals. We won't do that here.

# In practice, we keep coupling loose by having each module only call public
# methods and never accessing another module's internal variables.

if __name__ == "__main__":
    # This won't run because date is not imported, but it's just for illustration
    pass

```


## What the code does

The code shows two versions of the same functionality. The first `Utility` class has four unrelated methods — formatting dates, sending mail, calculating discounts, and logging. That's low cohesion. The second version splits each into its own class: `DateFormatter`, `EmailSender`, `PricingCalculator`, `ErrorLogger`. Each class has one job — high cohesion.

Coupling is shown in `OrderService` which takes an `EmailSender` and `ErrorLogger` as constructor arguments. That's okay — it's using their public interfaces. Tight coupling would be if `OrderService` reached into `ErrorLogger`'s internals, like changing the file path attribute directly. We avoid that.

The plain Python building blocks used: `class` to define a module, methods (functions inside a class), and the `__init__` method to set up an object. The `pass` statement just means 'do nothing' — used to satisfy the empty function.


## Common misconceptions

**Bias means the model is unfair** — High coupling is bad, but low coupling means modules are completely independent and never talk to each other.

It's not the fact of communicating that's bad; it's depending on internals. Two modules can communicate through a well-defined interface and still be loosely coupled. In fact: Loose coupling is not isolation. It's dependence on a small, stable, explicit interface rather than on hidden internals.

**Cohesion and coupling are opposites** — They're two ends of one scale — more cohesion automatically means less coupling, and vice versa.

They are two separate dimensions. It's possible to have high cohesion and high coupling (a focused module that's tightly interwoven with others), or low cohesion and low coupling (a junk drawer nobody uses). In fact: They correlate because high cohesion makes loose coupling easier, but they're independent qualities. You can still have a cohesive module with a leaky, entangled interface.

**Low cohesion is only about lines of code** — A module with fewer lines of code automatically has better cohesion.

Cohesion is about the relationship between the contents, not their volume. A 10-line module can contain two unrelated ideas; a 500-line module can be perfectly single-purpose. In fact: Judge cohesion by asking 'does every element in this module contribute to one clear purpose?' — not by size.

**Coupling is about which libraries you import** — If two modules don't import each other directly, they're loosely coupled.

Coupling can be hidden behind shared global state, a common database, or an event bus. Two classes can never reference each other but still be tightly coupled through the schema they both read. In fact: Coupling is about any shared dependency that makes one module's change ripple into another's behaviour — not just direct imports.

**The computer can measure cohesion and coupling** — There's a tool that can give a perfect numerical score for how well-designed my architecture is.

While there are some metrics (like fan-in/fan-out, or LCOM), they are rough proxies and don't capture the semantic 'relatedness'. Architecture is a human judgemental art. In fact: Use metrics as a hint, but rely on reading the code and asking 'does this module have a single clear responsibility?' — the human question has more power.


## In short

Cohesion is how related the things inside a module are; coupling is how dependent modules are on each other. Aim for high cohesion and loose coupling. They feed each other: high cohesion makes loose coupling natural. Master this and every architectural pattern — layering, ports and adapters, dependency inversion — becomes a tool for achieving it.


## Where this is used

**Large language models (LLMs)**: An LLM system like a chatbot is built from many modules: the tokenizer, the model itself, the prompt builder, the conversation history store, the API gateway, and the frontend. High cohesion means each module has one job — the tokenizer only turns text into tokens; it doesn't also store user preferences. Loose coupling means the frontend only talks to the API gateway through a clear interface (HTTP requests), not by importing the model's weights directly. If you change the model from GPT-4 to Llama, you only swap the model module, not the entire system — because the coupling is loose.

**Small models on devices**: When you run a model on a phone, you have to manage memory, battery, and latency. The model inference module should be highly cohesive — it does one thing (runs the model) and nothing else. Coupling becomes critical: if the mobile app is tightly coupled to a specific model format, upgrading the model breaks the UI, the camera module, and the storage layer. Instead, you define a port (an interface) that the app uses to ask for predictions, and an adapter that translates between that interface and whatever model you actually ship. This is textbook loose coupling.

**Retrieval-augmented generation (RAG)**: A RAG pipeline has modules for document ingestion, chunking, embedding, vector storage, query processing, and generation. If all those are in one god-class with low cohesion, you can't test the retrieval without also running the LLM. High cohesion puts each phase in its own module, with clear interfaces: one module returns embeddings for a text, another takes a query and returns top-5 chunks, another formats the context for the LLM. Coupling is loose because each module only depends on the interface of the next, not on its internals. A developer building RAG will write code that calls these modules through their public APIs, and if they need to swap the vector store from FAISS to Pinecone, they only change the adapter, not the whole pipeline.

**Agentic systems**: Agents plan, call tools, and act in loops. Each of those is a separate responsibility. The planner module decides what to do next; the tool-calling module knows how to invoke an API; the memory module stores conversation state. High cohesion keeps the planner from worrying about HTTP details, and the tool-caller from knowing the planner's internal decision tree. Loose coupling lets you replace the planning strategy (from a chain-of-thought to a different prompt) without touching the tool-caller. In practice, a developer builds an agent by wiring together modules through well-defined interfaces, and if they want to add a new tool, they don't rewrite the planner — they write a new adapter that fits the existing tool-calling interface.

**Everyday applied ML**: Fraud detection, recommendation systems, forecasting — all are built from data-loading, feature-engineering, model-training, and serving modules. If a data-loading module is tightly coupled to a specific database schema, and the feature-engineering module reaches into that schema's private tables, then changing the schema breaks everything. Instead, each module has a clear purpose and communicates through defined data sets (like a feature table). A data scientist working on a recommender system will write a feature-engineering function that outputs a DataFrame, and a separate training script that consumes that DataFrame. They don't let the training script know how features were computed. When the data source changes, only the feature-engineering module changes, not the training or serving code.

**What breaks when done badly**: Tight coupling and low cohesion lead to the 'shotgun surgery' anti-pattern — a single change forces changes in a dozen places. A new feature touches the UI, the business logic, the database, and the logging. Testing becomes impossible because you can't isolate a module from its dependencies. Deployments become risky because a one-line fix in a shared module has side effects across the whole system. The cost of the software is dominated by these integration and maintenance costs — they far outweigh the cost of initial development. That's why the 'ilities' — maintainability, testability, scalability — are the real requirements architecture trades between, and fixing cohesion and coupling is how you buy them.


---

Read this on the web: https://basavapurushottam.com/learn-ai/cohesion-and-coupling
