Skip to content

Cohesion and Coupling: How Do We Ruin Our Code?

I don't know either

Makonea·Apr 21, 2026·48 min

(A masterpiece among masterpieces, Code Complete)

Introduction...

Why do we always hear that 'modularity' is important,

people who fail to grasp the essence of modularity often say something like this.

'Splitting code is good,' they say.

Yet strangely, there are many cases where breaking code into smaller pieces actually makes things more complex.

For example, you clearly divide service classes by domain, but a single data flow ends up bouncing across multiple classes, becoming fragmented and harder to trace.

This post is about why that happens.

(Yourdon & Constantine's Structured Design paper, 1974)

Structured Design is a general set of program

design considerations and techniques aimed at making code easier, faster, and cheaper to write, debug, and modify by reducing complexity.

The key ideas are the result of nearly a decade of research by Mr. Constantine.

This paper presents those results, but does not cover the theory and derivations themselves.

These ideas were also referred to as Composite Design by Mr. Myers.

The authors believe these program design techniques are compatible with and complementary to the documentation techniques of HIPO6 and the coding techniques of structured programming.

These cost-reducing design techniques must always be balanced against other constraints of the system.

However, as the cost of programmer time continues to rise, the ability to create programs that are simple and easy to change will become increasingly important.

— Yourdon & Constantine, Structured Design paper, 1974

What Are Coupling and Cohesion, and How Did They Come About?

Discussions about Modularity existed as far back as the 1960s through the early 1970s.

Historically, Dijkstra advocated for structured programming and emphasized the importance of modular design, stressing it in his papers as well,

and in David Parnas's landmark 1972 paper, "On the Criteria To Be Used in Decomposing Systems into Modules," he proposed Information Hiding and the design of module boundaries.

Programmers were thus entering the early stages of building large-scale systems, and it was clear that modularity would be the tool for doing so.

In 1971, Niklaus Wirth described modularity as a process of Stepwise Refinement.

What Is Stepwise Refinement in Modularity?

It describes a top-down design approach for solving complex problems.

1. Start at an abstract level: define the problem to be solved first at a very high level, drawing the big picture of the problem.

2. Begin progressive concretization.

3. Then add details of the problem.

4. Repeat the implementation.

Wirth argued that this process naturally leads to modularity,

as the subproblems derived at each refinement step ultimately become independent functional units, or 'modules.'

Each module will solve a specific subproblem,

and the interfaces between modules are determined by the data and control flow defined at each step.

Therefore, the very process of moving from high-level abstraction to concrete implementation is the method for organizing a system into independent modules with clear responsibilities and interfaces.

So we understand that module boundary design and modular design are important.

That naturally brings us back to the following question.

Then, how should the quality of modularity be evaluated?

(A diagram illustrating cohesion and coupling well)

And in 1974, Yourdon & Constantine's Structured Design paper established criteria for quantitatively evaluating this.

Those criteria are

Cohesion

Coupling

Based on this, 'modularity' was established not merely as a way to split code but as a core tool for managing system complexity.

Around the same time, Object-Oriented Programming (OOP) emerged from Alan Kay's Smalltalk lineage and rose to industry prominence in the late 1980s alongside C++. But as inheritance, polymorphism, and dynamic binding came into full use, dependencies began to surface that could not be captured by the coupling metrics Yourdon and Constantine had established in the procedural era.

The OO metrics research of the 1990s filled that gap. Chidamber & Kemerer's CK Metrics (CBO, RFC, LCOM) quantified coupling, Page-Jones's Connascence reclassified types of coupling to fit OOP syntax, and Briand and others refined the classification framework for object-oriented coupling empirically.

The channel through which this accumulated body of discussion reached practicing programmers was Code Complete. McConnell distilled the academic classifications into practical terms,

reorganizing them into four context-centered categories: simple-data-parameter coupling, simple-object coupling, object-parameter coupling, and semantic coupling, rather than the traditional categories of content, common, control, stamp, and data coupling.

Meanwhile, these terms were also incorporated around the same period into standards documents such as IEEE 610.12 (1990), becoming common industry vocabulary.

What Are Cohesion and Coupling?

Cohesion arose from structured design and is usually discussed in the same context as coupling. Cohesion refers to how closely all the routines in a class or all the code in a routine support a central purpose—how focused the class is. Classes that contain strongly related functionality are described as having strong cohesion, and the heuristic's goal is to make cohesion as strong as possible. Cohesion is a useful tool for managing complexity because the more that code in a class supports a central purpose, the more easily your brain can remember everything the code does.

Thinking about cohesion at the routine level has been a useful heuristic for decades and is still useful today. At the class level, the heuristic of cohesion has largely been subsumed by the broader heuristic of well-defined abstractions, which was discussed earlier in this chapter and in Chapter 6. Abstractions are useful at the routine level, too, but on a more even footing with cohesion at that level of detail.

— Steve McConnell, *Code Complete*, 2nd Edition, "Aim for Strong Cohesion"

To state the conclusion first: it is advantageous to make Cohesion high and Coupling low.

Cohesion

A hierarchical classification of the functional relatedness of elements within a module.

Coupling

The strength of dependency between modules.

Now let's look at the indicators used to assess cohesion and coupling,

drawing directly from Code Complete and the Yourdon & Constantine paper.

Traditional Classification of Cohesion (Yourdon & Constantine) — 1974

Type of Cohesion

Description

Coincidental Cohesion

Tasks within the module have no relation to each other whatsoever; they are simply grouped together physically.

The worst form of cohesion.

Example: Utils.cs contains logging, date parsing, and a DB connection all in one place.

Logical Cohesion

Tasks are related but performed selectively; the behavior is determined by branching (switch/case) from a single entry point.

Example: a method like HandleCommand(CommandType type).

Temporal Cohesion

Tasks that are executed together at the same time are grouped.

Example: grouping actions that run at program startup or shutdown, like InitAll() and ShutdownAll().

Procedural Cohesion

Tasks within the module execute in a specific order but have no direct relationship to each other.

Example: opening a file, writing a log, and closing the UI happening sequentially.

Communicational Cohesion

All tasks within the module use the same data structure.

The relationship is clear, but the processing tasks can vary. Example: query, update, and delete operations on the same table are in a single module.

Sequential Cohesion

The output of one task becomes the input of the next.

Example: a pipeline that reads data, transforms it, then saves it.

Functional Cohesion

The module performs only one clear function, and all internal elements exist to serve that function. The most ideal form.

Example: functions like CalculateTax() and GenerateReport().

Code Complete discusses cohesion in terms of modules or routines, where a routine means 'an independent block of code that performs a specific task.' That is, not only classes but also individual methods are subject to cohesion evaluation.

Cohesion refers to how well the elements of a class or routine fit together. Code Complete places this evaluation on top of the seven-level classification examined above, criticizing Coincidental Cohesion most harshly and holding Functional Cohesion as the ideal.

So how can you detect that cohesion is declining?

Code Complete explicitly identifies the following symptoms.

A routine's name contains and or or. The name itself confesses 'it does this and also that.' Names like SaveAndNotify() or ValidateOrFallback() are a declaration that the routine already carries two responsibilities.

There is an excessive number of parameters. Because the context the routine must handle does not converge to a single concern internally, it ends up needing multiple streams of information injected from outside.

Complex control flow exists inside the routine. When multiple branches within a single routine scatter toward different purposes, cohesion has already broken down.

Consider the example below.

Cohesion — Code Example

// A low-cohesion routine: the name contains "And", and it has two responsibilities
public void ValidateAndSaveOrder(Order order, bool sendNotification)
{
    if (order.Items.Count == 0)
        throw new InvalidOperationException("No order items.");

    repository.Save(order);

    if (sendNotification)
        notificationService.Send(order.CustomerId, "Order has been received.");
}

(Example of low cohesion)

This routine crams three different concerns into a single method: validation, saving, and notification. Each of these can change independently for its own reason.

If the notification mechanism changes from email to push, this method must be modified. If the storage changes from a database to a cache, this method must be modified. If a validation rule is added, the same is true. The fact that a single method can change for three different reasons means this method carries three responsibilities.

The problem also surfaces from a testing perspective. Even if you only want to test the save functionality, the notification logic runs along with it. You can pass sendNotification as false to suppress the notification, but that itself is Control Coupling, and it is a signal that the caller is controlling an internal branch for the sake of testing.

// Routines with high cohesion: each carries only one responsibility
public void ValidateOrder(Order order)
{
    if (order.Items.Count == 0)
        throw new InvalidOperationException("No order items.");
}

public void SaveOrder(Order order)
{
    repository.Save(order);
}

public void NotifyCustomer(Order order)
{
    notificationService.Send(order.CustomerId, "Order has been received.");
}

(Example of high cohesion)

Each routine changes for only one reason. ValidateOrder changes only when the validation rules change, SaveOrder changes only when the storage implementation changes, and NotifyCustomer changes only when the notification mechanism changes. The causes of change have been separated.

Calling the three routines in sequence on the calling side may look like more code. However, when ValidateAndSaveOrder throws an exception, you cannot immediately tell from the stack trace alone at which step the failure occurred. With separated routines, the method name that threw the exception reveals the failure point right away.

The results of good cohesion that Code Complete describes are concrete. Code focuses on only one thing, routine names serve as their own documentation, modifications stay within a local scope, and the contextual relevance among lines of code within a routine increases.

However, pursuing Functional Cohesion does not mean that endlessly splitting methods into tiny pieces is always the right answer.

If the call flow becomes too dispersed and the overall narrative becomes hard to read, the attempt to increase cohesion can actually raise cognitive cost.

Conversely, if a single change spreads unexpectedly across multiple files, that is a signal that a cohesion problem has already transferred into a coupling problem.

(Structured Design, 1979, description of coupling)

(Coupling as described in Code Complete)

Traditional Classification of Coupling

Type of Coupling

Description

Content Coupling

When one module directly accesses the internals (implementation details) of another module. Creates a very strong dependency; modifications directly affect the external module. Example: Function A directly manipulates a local variable or private function inside Function B.

Common Coupling

When multiple modules share global data. It becomes hard to track who modifies the global state and when, making debugging difficult due to side effects. Example: Multiple modules simultaneously referencing and modifying a global variable globalConfig.

External Coupling

When a module depends on an external system, format, device, or communication protocol. Changes outside the system can affect internal behavior. Example: Code tightly bound to a file format, DB schema, or hardware protocol.

Control Coupling

When one module passes a control flag (condition) to another module to indirectly dictate its internal logic flow. Can be a precursor to Semantic Coupling. Example: The caller specifying an internal conditional branch, as in Process(flag).

Stamp Coupling

When an entire data structure (e.g., a struct or class) is passed but only some fields are used. Unnecessary information is transmitted, increasing coupling and the potential for misunderstanding. Example: Calling Process(User user) but only using user.name.

Data Coupling

The most ideal form of coupling, where only the necessary data is passed as arguments. Minimizes inter-module dependency and keeps interfaces clear. Example: Passing only the required values, as in CalculateTax(income, regionCode).

No Coupling

Completely independent modules that share no information with each other. Practically nonexistent in typical system design, but possible in test modules and similar contexts. Example: A utility function that runs independently.

Summary of Coupling Types Based on Code Complete

Type of Coupling

Description

Simple-data-parameter Coupling

When all data passed between modules is of primitive type and transmitted entirely through parameters. Generally the most ideal form of coupling.

Simple-object Coupling

When one module simply instantiates or directly uses another object. Considered safe coupling because it does not depend on internal behavior.

Object-parameter Coupling

When one object receives and uses a third object through another object. The intermediary object must know the existence and meaning of the object being passed, making the coupling tighter.

Semantic Coupling

Occurs when one module implicitly understands the behavior or internal logic of another module. The most dangerous form of coupling, as it cannot be used correctly without prior knowledge of internal behavior.

Coupling refers to the strength of dependency between modules.

There is one interesting fact. The 'Cohesion' metric introduced in Yourdon and Constantine's 1974 Structured Design is applied in essentially the same conceptual form even today in the object-oriented era. (Of course, in detail it has shifted from a routine-centric to a method-centric perspective, but the meaning has not changed significantly.)

This is because the philosophy of "do only one thing (Functional Cohesion)" holds regardless of whether you are dealing with functions or methods.

'Coupling,' however, is different. The traditional coupling of 1974 (data coupling, control coupling, etc.) is a relic of the procedural era that examined only data flow between subroutines. When the Object-Oriented Programming (OOP) era arrived and brought inheritance, interfaces, and polymorphism, the old yardstick became completely incapable of evaluating the terrible dependencies between classes.

Coupling classifications existed in both the 1974 original and the 1979 book,

covering data, control, stamp, common, and content coupling, among others,

but they were premised on data flow between subroutines. When the object-oriented era arrived, dependencies that could not be captured by the procedural model, such as inheritance, polymorphism, and dynamic binding, came to the forefront, and the OO coupling research that ran through Chidamber & Kemerer, Page-Jones, and Briand in the 1990s rebuilt the classification framework from the ground up.

McConnell's four-category classification is a compression of that academic trend into practical language,

and that is precisely why the acclaimed Code Complete presents entirely new classification schemes such as Object-parameter Coupling and Semantic Coupling rather than the traditional coupling categories. In the OOP ecosystem, it is 'the implicit context and collaboration between objects' rather than data flow that becomes the primary source of system decay.

Because Code Complete was written at the time OOP was becoming mainstream,

context-based coupling was considered more realistic, and new forms of coupling were introduced, covering areas such as inter-object collaboration and interface misuse.

These differences led to a reclassification of coupling with a focus on practical application.

If traditional coupling was a theoretical yardstick for design,

McConnell's classification better captures the kinds of incorrect interactions between code that arise in actual OOP-oriented development.

In short, traditional coupling focuses on 'what is exchanged and how much.'

Modern coupling also asks 'how much do you need to know about the other party's internal meaning and context.'

Traditional Coupling — Code Example

Content coupling and common coupling are addressed first, since they can be reproduced in modern C# through private accessors and global state.

// Content Coupling: A directly touches B's internal state
public class OrderProcessor
{
    public void Process(PaymentService paymentService)
    {
        // Directly manipulating PaymentService's internal fields
        paymentService.retryCount = 0;
        paymentService.lastError = null;
    }
}

(Example of high coupling)

OrderProcessor is directly initializing the internal fields of PaymentService. The problem is that PaymentService cannot manage its own state. When and where and why retryCount and lastError are initialized cannot be determined by looking at PaymentService's code alone; you have to look at OrderProcessor to find out.

If the internal implementation of PaymentService changes, for instance if retryCount is renamed to remainingAttempts or its type changes, then OrderProcessor must also be updated. The two classes have become bound together as one.

// Improvement: state initialization is handled internally within `PaymentService`
public class PaymentService
{
    private int retryCount = 0;
    private string lastError = null;

    public void Reset()
    {
        retryCount = 0;
        lastError = null;
    }
}

public class OrderProcessor
{
    public void Process(PaymentService paymentService)
    {
        paymentService.Reset(); // No need to know the internals
    }
}

(Example of low coupling)

All that needs to be known is the public contract Reset(). Whatever PaymentService initializes internally, OrderProcessor has no involvement.

The reason we end up in a hell of fragmentation even after splitting our code is not that we raised functional cohesion, but that we merely tore files apart and scattered 'Semantic Coupling, where you must implicitly know the order and context across classes' throughout the entire system.

// Common Coupling: multiple modules share global state
public static class GlobalConfig
{
    public static string DatabaseUrl = "localhost";
    public static int Timeout = 30;
}

public class OrderRepository
{
    public void Save(Order order)
    {
        var conn = new DbConnection(GlobalConfig.DatabaseUrl); // global reference
    }
}

public class ReportService
{
    public void Generate()
    {
        var conn = new DbConnection(GlobalConfig.DatabaseUrl); // same global reference
    }
}
// If `GlobalConfig.DatabaseUrl` changes, both modules are affected
// It is impossible to track when or by whom it was changed
// Control Coupling: the caller dictates internal branching
public void Process(Order order, bool isUrgent)
{
    if (isUrgent)
    {
        priorityQueue.Enqueue(order);
    }
    else
    {
        normalQueue.Enqueue(order);
    }
}

// Improvement: the caller does not need to know the internal logic
public void ProcessUrgent(Order order) => priorityQueue.Enqueue(order);
public void ProcessNormal(Order order) => normalQueue.Enqueue(order);
// Stamp Coupling: receiving an entire object when all you need is a name
public void SendWelcomeMail(User user)
{
    mailService.Send(user.Email, "Welcome"); // Only `user.Email` is used
}

// Improvement: receive only the data you need
public void SendWelcomeMail(string email)
{
    mailService.Send(email, "Welcome");
}

Coupling Based on Code Complete — Code Example

Semantic coupling is the most dangerous and hardest to detect, which is why the example comes with the most explanation.

// Simple data coupling: only primitive types are passed as parameters
public bool IsEligibleForDiscount(int purchaseCount, decimal totalAmount)
{
    return purchaseCount >= 10 && totalAmount >= 100_000m;
}
// Simple object coupling: instantiates and uses an object without needing to know its internals
public class OrderService
{
    private readonly TaxCalculator taxCalculator = new TaxCalculator();

    public decimal GetFinalPrice(Order order)
    {
        return order.BasePrice + taxCalculator.Calculate(order.BasePrice, order.Region);
    }
}
// Object parameter coupling: B passes C to A
// A must know about C's existence and type
public class ReportBuilder
{
    public Report Build(DataLoader loader)
    {
        var rawData = loader.GetDataSource(); // Dependency on a third type via DataSource
        return new Report(rawData);
    }
}
// Semantic Coupling: without comments, you cannot know the correct calling order
public class PaymentProcessor
{
    private bool isInitialized = false;

    public void Initialize()
    {
        isInitialized = true;
    }

    public void Process(Payment payment)
    {
        // The fact that the caller must invoke `Initialize()` first
        // cannot be determined from this signature alone
        if (!isInitialized)
            throw new InvalidOperationException("Initialization is required.");

        ExecutePayment(payment);
    }
}

// Improvement: enforce initialization in a constructor, or wrap it in a factory method
public class PaymentProcessor
{
    private PaymentProcessor() { }

    public static PaymentProcessor Create()
    {
        var processor = new PaymentProcessor();
        processor.Initialize();
        return processor;
    }

    public void Process(Payment payment) => ExecutePayment(payment);
}

The key point about semantic coupling is that the compiler cannot catch it. The code is syntactically perfectly valid, but without implicit knowledge of the call order or internal state, it will only blow up at runtime. As the team grows larger, this cost rises exponentially.

And cohesion and coupling interact with each other, becoming tools for understanding the quality of our code.

For example, a module with Logical Cohesion has a high likelihood of inducing Control Coupling, which shows how the two perspectives are mutually complementary.

The programmer, like the poet, works only slightly removed from pure thought-stuff. He builds his castles in the air, from air, creating by exertion of the imagination.

— Frederick P. Brooks Jr., *The Mythical Man-Month: Essays on Software Engineering*

Closing Thoughts

The history of software design can be described as the evolutionary history of modularization principles.

The cohesion/coupling framework proposed by Yourdon & Constantine in 1974 is not mere theory; it is insight earned through the blood and sweat of the engineers who ushered in the era of large-scale systems.

Splitting code is not decomposition; it is the act of creating a new layer of abstraction.

As Code Complete emphasizes, the success or failure of modularity comes down to

"Can a routine be described in a single sentence?" (Functional Cohesion)

"How few files need to be touched when a change is made?" (Low Coupling)

these practical questions.

The essence of modularity lies not in increasing the number of files but in designing boundaries so that cohesion is high and coupling is low.

And these concepts have spread deeply into countless areas, from microservice architecture to Docker.

The legacy left by the pioneers of the 1970s has not changed.

Modularity remains the most powerful weapon in software design,

and we have inherited the wisdom of our predecessors, which allows us to handle architectures that were enormous by the standards of the time.

And as a programmer, I add a short line to a remark from those who came before us:

"We build code in thin air. But that thin air must be something others can understand."