Coupling and Cohesion Explained with Java Examples

Introduction

This tutorial explains coupling and cohesion, the two measures of design quality that most other design principles are built on. We will start with what each term means and why the goal is high cohesion with low coupling. We will then look at the different kinds of cohesion and coupling, from best to worst. Next, we will refactor a Java class with low cohesion, and a Java example with tight coupling, and see the difference in behaviour. Lastly, we will see how to spot and measure both in a codebase.

What is cohesion

Cohesion is how closely the parts inside one module belong together. A module can be a method, a class, a package or a service. In a highly cohesive class, the fields and methods all work towards one purpose; in a class with low cohesion, they do unrelated things that happen to share a file.

A class named EmployeeUtils that calculates tax, validates email addresses and formats dates has low cohesion. A class named TaxCalculator whose every method is about tax has high cohesion. Cohesion is the idea behind the Single Responsibility PrincipleClick to read: Single Responsibility Principle.

What is coupling

Coupling is how much one module depends on other modules. Two classes are tightly coupled when a change in one forces a change in the other, or when one cannot be used, compiled or tested without the other. They are loosely coupled when they interact through a small, stable interface and know little about each other's internals.

Coupling cannot be zero - modules that never interact do not form a program. The aim is fewer connections, and connections that are narrow and explicit.

The goal - high cohesion, low coupling

The two measures are linked. When related code is grouped together (high cohesion), most interactions happen inside a module, so fewer have to cross module boundaries (low coupling). When related code is scattered (low cohesion), every change touches several modules, and those modules need many connections to each other.
Coupling and cohesion - modules with low cohesion and high coupling versus modules with high cohesion and low coupling

Kinds of cohesion and coupling

Larry Constantine and his co-authors classified both in the 1970s, in the work that became Structured Design. The names are still useful because they tell you why a design is weak and what to change. The diagram lists them from best to worst.
Kinds of cohesion from functional to coincidental and kinds of coupling from data coupling to content coupling
The ones you will meet most often in Java code are -
  • Coincidental cohesion: a Utils or Helper class collecting unrelated methods.
  • Logical cohesion: one method doing different things depending on a parameter, such as process(type, data) with a switch inside.
  • Common coupling: classes reading and writing the same public static mutable field.
  • Control coupling: passing a boolean or enum flag that tells the method which of its behaviours to run.
  • Stamp coupling: passing a whole object when the method uses only one field, which ties the method to that object's type.
  • Content coupling: reaching into another object's internals, such as modifying a list returned by its getter.

Example 1 - improving cohesion

Here is a typical low-cohesion class. Its three methods have nothing in common except that they involve employees.
An EmployeeUtils class with coincidental cohesion
public class EmployeeUtils {
  public static double incomeTax(double annualSalary) { ... }      // payroll rules
  public static boolean isValidEmail(String email) { ... }          // input validation
  public static String formatJoiningDate(LocalDate date) { ... }    // presentation
}
Each method changes for a different reason, is used by different callers, and is tested separately. Anyone who needs isValidEmail() now depends on a class that also contains tax rules. Splitting it by purpose gives three cohesive types -
The same methods regrouped into cohesive classes
public final class IncomeTax {                       // everything about tax
  public static double forAnnualSalary(double salary) { ... }
  public static double marginalRate(double salary) { ... }
}

public record EmailAddress(String value) {           // validation lives with the data
  public EmailAddress {
    if (!value.matches("[^@\\s]+@[^@\\s]+\\.[^@\\s]+"))
      throw new IllegalArgumentException("Invalid email: " + value);
  }
}

public final class EmployeeFormats {                 // presentation only
  public static String joiningDate(LocalDate date) { ... }
}
Notice that EmailAddress is not just a renamed helper. Moving the validation into the type that holds the data means an invalid email address cannot exist anywhere in the program - the most cohesive place for a rule is next to the data it protects.

Example 2 - reducing coupling

The next example calculates employee bonuses. The first version has three kinds of coupling: it reads a public static rate that any class can change (common coupling), it takes a flag from the caller that decides the calculation (control coupling), and it takes a whole Employee although it needs only the salary (stamp coupling). The second version removes all three.
Java code showing tight coupling and loose coupling in a bonus calculator
//CouplingExample.java
public class CouplingExample {

  record Employee(String name, double salary, boolean manager) {}

  // ---------- Tightly coupled version ----------
  static class Settings {
    static double bonusRate = 0.10;           // global and mutable: common coupling
  }

  static class TightBonusCalculator {
    // the flag from the caller decides the calculation: control coupling
    double bonus(Employee e, boolean doubleIt) {
      double rate = Settings.bonusRate;
      if (doubleIt) rate = rate * 2;
      return e.salary() * rate;
    }
  }

  // Unrelated code elsewhere that "tweaks" the shared setting
  static class YearEndCampaign {
    void start() { Settings.bonusRate = 0.25; }
  }

  // ---------- Loosely coupled version ----------
  interface BonusPolicy { double bonusFor(double salary); }

  record FlatRate(double rate) implements BonusPolicy {
    public double bonusFor(double salary) { return salary * rate; }
  }

  static class BonusCalculator {
    private final BonusPolicy staffPolicy;
    private final BonusPolicy managerPolicy;

    BonusCalculator(BonusPolicy staffPolicy, BonusPolicy managerPolicy) {
      this.staffPolicy = staffPolicy;
      this.managerPolicy = managerPolicy;
    }

    double bonus(Employee e) {
      BonusPolicy policy = e.manager() ? managerPolicy : staffPolicy;
      // passes only what the policy needs: data coupling
      return policy.bonusFor(e.salary());
    }
  }

  public static void main(String[] args) {
    Employee tom = new Employee("Tom Jones", 50_000, false);
    Employee nancy = new Employee("Nancy Smith", 80_000, true);

    TightBonusCalculator tight = new TightBonusCalculator();
    BonusCalculator loose = new BonusCalculator(new FlatRate(0.10), new FlatRate(0.20));

    System.out.printf("Before campaign - tight: %.2f, %.2f | loose: %.2f, %.2f%n",
        tight.bonus(tom, false), tight.bonus(nancy, true),
        loose.bonus(tom), loose.bonus(nancy));

    new YearEndCampaign().start();

    System.out.printf("After campaign  - tight: %.2f, %.2f | loose: %.2f, %.2f%n",
        tight.bonus(tom, false), tight.bonus(nancy, true),
        loose.bonus(tom), loose.bonus(nancy));
  }
}
 OUTPUT of the above code
Before campaign - tight: 5000.00, 16000.00 | loose: 5000.00, 16000.00
After campaign  - tight: 12500.00, 40000.00 | loose: 5000.00, 16000.00

Explanation of the code

  • TightBonusCalculator reads Settings.bonusRate, a global that YearEndCampaign changes. The calculator's results changed even though no one touched the calculator - the two classes are coupled through shared data that neither of them owns.
  • The caller of tight.bonus() must know that managers get double, and pass true for them. That rule is now spread across every caller, which is what control coupling does.
  • BonusCalculator receives its policies through the constructor. Its behaviour is fixed when it is created, and nothing outside can change it later, so the campaign has no effect on it.
  • BonusPolicy.bonusFor() takes a double, not an Employee. Policies can be reused for anything with a salary, and tested with plain numbers.
  • The manager rule now lives in one place, inside BonusCalculator, instead of in every caller. Code that changes together is kept together - higher cohesion as a side effect of lower coupling.

How to spot coupling and cohesion problems

  • The "and" test: if a class needs "and" to describe what it does, its cohesion is probably low.
  • Fields used by few methods: if each group of methods uses its own group of fields, the class is really two or more classes. Metrics such as LCOM (lack of cohesion of methods) measure this.
  • Shotgun surgery: one small change needs edits in many classes - coupling is high, or cohesion is low.
  • Long import lists and constructors: a class with many dependencies is coupled to all of them.
  • Hard to test: if a unit test needs a database, a clock and three other services, the class is tightly coupled to them.
  • Package dependencies: the JDK's jdeps tool lists the packages each class depends on, and cycles between packages are a strong sign of tight coupling.

Techniques that lower coupling

Summary

In this tutorial we first defined cohesion, how well the parts inside a module belong together, and coupling, how much a module depends on others, and saw why the goal is high cohesion with low coupling. Next we looked at the kinds of each, from functional cohesion down to coincidental, and from data coupling down to content coupling. We then split a Utils class into cohesive types, and refactored a bonus calculator to remove common, control and stamp coupling - and saw the tightly coupled version change its results because of unrelated code. Lastly, we listed the signs of coupling and cohesion problems and the techniques that fix them.