When we build software, the difference between a system that evolves easily and one that becomes a burden rarely lies in the specific algorithm we write. It lies in how we organize the pieces and how they depend on one another. Coupling and cohesion are the two most important conceptual metrics for reasoning about that organization: they tell us how entangled our modules are and how focused each piece is on a single task. Mastering these concepts is the first step toward making conscious architectural decisions instead of accumulating accidental complexity. In this lesson we will study them in depth, see how they manifest in real Java code and how to refactor to improve them, ending with a fundamental practical rule: the Law of Demeter.

Contents

  1. Coupling: what it is and why it matters
  2. Types of coupling (from worst to best)
  3. Cohesion: high versus low
  4. The relationship between coupling and cohesion
  5. Separation of Concerns (SoC)
  6. Guided refactoring: from high coupling to low coupling
  7. The Law of Demeter (principle of least knowledge)
  8. Common mistakes and tips
  9. Exercises
  10. Conclusion

  1. Coupling: what it is and why it matters

Coupling measures the degree of interdependence between two modules: how much one module needs to know about the internal details of another in order to function. When two components are tightly coupled, a change in one forces a change in the other.

Key concepts:

  • Low coupling (desirable): modules communicate through stable, minimal interfaces. An internal change in one module does not affect the others.
  • High coupling (problematic): modules depend on the internal details, concrete types, or data structures of other modules.
  • Coupling can never be zero: if modules did not communicate, they would not form a system. The goal is to minimize it and make it explicit.

Consequences of high coupling:

  • Fragility: localized changes cause cascading failures in unexpected places.
  • Rigidity: it is hard to modify the system because every change drags others along with it.
  • Low reusability: you cannot extract a module without dragging its dependencies along.
  • Difficulty testing: you cannot test a module in isolation.

  1. Types of coupling (from worst to best)

Historically (Stevens, Myers, and Constantine, 1974) coupling is classified along a scale. This table orders the types from the most harmful to the healthiest:

Type Description Rating
Content One module modifies or depends on another's internal data Very bad
Common Several modules share mutable global state Bad
External Dependence on an externally imposed format or protocol Bad
Control One module controls another's flow by passing it flags Fair
Stamp A whole structure is passed when only part of it is needed Improvable
Data Only the strictly necessary data is passed Good
Message Communication only through messages/interfaces with no internal parameters Optimal

Example of control coupling (one of the most frequent and avoidable):

// BAD: the caller controls the internal flow through a flag
public class ReportGenerator {
    public String generate(Data data, boolean isPdf) {
        if (isPdf) {
            return generatePdf(data);
        } else {
            return generateHtml(data);
        }
    }
}

Explanation of the problem: the boolean isPdf parameter is a control flag. The calling code has to know the method's internal logic to understand what each value does. Moreover, each new format (CSV, XML) forces you to add parameters or branches, breaking the existing method.

// GOOD: data/message coupling through polymorphism
public interface ReportGenerator {
    String generate(Data data);
}

public class PdfGenerator implements ReportGenerator {
    public String generate(Data data) { /* ... */ return "pdf"; }
}

public class HtmlGenerator implements ReportGenerator {
    public String generate(Data data) { /* ... */ return "html"; }
}

Explanation of the improvement: now the caller picks a concrete implementation and uses it through the ReportGenerator interface. It knows nothing about the internal details. Adding a new format means creating a new class, without touching the existing ones.

  1. Cohesion: high versus low

Cohesion measures how related and focused the elements within a single module are. A class with high cohesion does one thing well; one with low cohesion mixes disparate responsibilities.

Types of cohesion (from worst to best):

Type What it groups Rating
Coincidental Unrelated elements grouped by chance Very bad
Logical Tasks that are similar by category but different in purpose Bad
Temporal Things that run at the same moment (e.g., startup) Fair
Procedural Steps of a procedure, in a certain order Improvable
Communicational Operations on the same data Good
Functional Everything contributes to a single, well-defined task Optimal

Example of low cohesion:

// BAD: a class that does everything (a "God" class)
public class Utilities {
    public void saveUser(User u) { /* database access */ }
    public String formatDate(Date d) { /* formatting */ return ""; }
    public void sendEmail(String recipient) { /* SMTP */ }
    public double calculateTax(double base) { return base * 0.21; }
}

Explanation of the problem: this class groups database access, formatting, email sending, and tax calculation. There is no relationship among these tasks: this is coincidental cohesion. Any developer who needs just one of these functions ends up coupled to the whole class.

// GOOD: each class has a single responsibility (functional cohesion)
public class UserRepository {
    public void save(User u) { /* database access */ }
}

public class EmailService {
    public void send(String recipient, String body) { /* SMTP */ }
}

public class TaxCalculator {
    public double calculate(double base) { return base * 0.21; }
}

  1. The relationship between coupling and cohesion

These two metrics tend to move in opposite directions and constitute the guiding principle of good modular design:

Goal: high internal cohesion and low external coupling.

graph LR
    subgraph "Poor design"
        A1[Module A] <--> B1[Module B]
        A1 <--> C1[Module C]
        B1 <--> C1
    end
    subgraph "Good design"
        A2[Module A] --> I[Interface]
        B2[Module B] --> I
        C2[Module C] --> I
    end

Explanation of the diagram: on the left, all modules know one another directly (a dense web = high coupling). On the right, modules depend only on a stable interface: dependencies are few and directed. If a module changes internally, as long as it respects the interface, the others never notice.

  1. Separation of Concerns (SoC)

Separation of Concerns is the principle that underpins both high cohesion and low coupling: each part of the system should deal with a single concern and nothing more.

Typical concerns worth separating:

  • Presentation (user interface, response serialization).
  • Business logic (domain rules).
  • Persistence (data access).
  • Cross-cutting concerns (logging, security, transactions).

A classic SoC pattern is the layered architecture:

// Presentation layer: it only orchestrates and translates HTTP <-> domain
@RestController
public class OrderController {
    private final OrderService service;
    public OrderController(OrderService service) { this.service = service; }

    @PostMapping("/orders")
    public OrderResponse create(@RequestBody OrderRequest request) {
        Order order = service.createOrder(request.getItems());
        return OrderResponse.from(order);
    }
}

// Business layer: domain rules, knowing nothing about HTTP or SQL
public class OrderService {
    private final OrderRepository repository;
    public OrderService(OrderRepository repository) { this.repository = repository; }

    public Order createOrder(List<Item> items) {
        Order order = new Order(items);
        order.validate();
        return repository.save(order);
    }
}

Explanation: the OrderController knows only the web protocol and delegates immediately. The OrderService contains the rules and does not know whether it is invoked by a REST controller, a message queue, or a test. Each layer can evolve and be tested independently.

  1. Guided refactoring: from high coupling to low coupling

Let's walk through a complete refactoring. We start from a class that creates its own dependencies internally:

// BEFORE: strong coupling to concrete implementations
public class NotificationService {
    private final SmtpClient smtp = new SmtpClient("smtp.company.com");

    public void notify(String user, String message) {
        String email = new MySqlUserRepository().findEmail(user);
        smtp.send(email, message);
    }
}

Problems detected:

  • It creates SmtpClient and MySqlUserRepository with new: impossible to replace them with test doubles.
  • It depends on concrete classes, not on abstractions.
  • It cannot be tested without a real SMTP server and a real database.
// AFTER: dependency injection against interfaces
public interface EmailGateway {
    void send(String recipient, String body);
}

public interface UserRepository {
    String findEmail(String user);
}

public class NotificationService {
    private final EmailGateway email;
    private final UserRepository users;

    // Dependencies are received from outside (inversion of control)
    public NotificationService(EmailGateway email, UserRepository users) {
        this.email = email;
        this.users = users;
    }

    public void notify(String user, String message) {
        String recipient = users.findEmail(user);
        email.send(recipient, message);
    }
}

Step-by-step explanation of the improvement:

  1. We define interfaces (EmailGateway, UserRepository) that describe what is needed, not how it is implemented.
  2. The service receives its collaborators through the constructor (dependency injection). It no longer uses new.
  3. In production we will inject the real implementations; in tests, lightweight doubles. This reduces coupling to its message form and enables isolated testing.

  1. The Law of Demeter (principle of least knowledge)

The Law of Demeter is a heuristic rule that limits coupling: "talk only to your close friends, not to strangers." A method of an object should only invoke methods of:

  • the object itself (this),
  • objects it receives as parameters,
  • objects it creates itself,
  • its direct attributes.

The most visible symptom of a violation is the chaining of calls (a.getB().getC().do()), also known as a "train wreck."

// VIOLATES the Law of Demeter: we navigate the internal structure of other objects
public class DiscountCalculator {
    public double calculate(Order order) {
        String country = order.getCustomer().getAddress().getCountry();
        if (country.equals("ES")) return 0.10;
        return 0.0;
    }
}

Explanation of the problem: DiscountCalculator knows the internal structure of Order, Customer, and Address. If any of those classes changes its structure, this method breaks. The coupling is transitive and hidden.

// COMPLIES with the Law of Demeter: each object exposes what is needed
public class Order {
    private final Customer customer;
    public boolean isDomestic() { return customer.isDomestic(); }
}

public class Customer {
    private final Address address;
    public boolean isDomestic() { return address.isInCountry("ES"); }
}

public class DiscountCalculator {
    public double calculate(Order order) {
        return order.isDomestic() ? 0.10 : 0.0;
    }
}

Explanation of the improvement: instead of requesting data and deciding outside ("ask"), we tell the object to answer the business question that concerns it ("tell"). Each class hides its internal structure. This is known as the "Tell, Don't Ask" principle and is the practical face of the Law of Demeter.

An important nuance: the Law of Demeter applies to objects with domain behavior. It should not be applied dogmatically to fluent APIs or builders (new Builder().with(x).with(y).build()), where chaining is intentional and each method returns the same type.

Common Mistakes and Tips

  • Confusing few lines with high cohesion. A small class can still be incoherent if it mixes concerns. Cohesion is semantic, not a matter of size.
  • Creating interfaces for everything "just in case." An unnecessary abstraction is accidental complexity. Introduce interfaces when there is a real variation or a testing boundary.
  • Hiding coupling in global state or singletons. Common coupling (shared mutable state) is among the hardest to debug; prefer passing explicit dependencies.
  • Applying the Law of Demeter to pure data structures (DTOs). Accessing the fields of a DTO with no logic does not violate it; the law protects objects with behavior.
  • Tip: when faced with a change, observe how many files you have to touch. If it's many for a conceptually small change, you have a coupling or cohesion problem.
  • Tip: use the "Tell, Don't Ask" rule as a quick detector of Demeter violations.

Exercises

Exercise 1. Identify the type of coupling and refactor:

public class Calculator {
    public double operate(double a, double b, int type) {
        if (type == 1) return a + b;
        if (type == 2) return a - b;
        if (type == 3) return a * b;
        return 0;
    }
}

Exercise 2. The following class has low cohesion. Split it into classes with a single responsibility:

public class StoreManager {
    public void processPayment(double amount) { /* ... */ }
    public void updateInventory(String product, int quantity) { /* ... */ }
    public void writeLog(String message) { /* ... */ }
}

Exercise 3. Rewrite this method so that it complies with the Law of Demeter:

public double priceWithShipping(Invoice invoice) {
    return invoice.getOrder().getTotal() + invoice.getOrder().getShipping().getCost();
}

Solutions

Solution 1. It is control coupling (the int type drives the flow). We refactor to polymorphism:

public interface Operation {
    double apply(double a, double b);
}
public class Addition implements Operation {
    public double apply(double a, double b) { return a + b; }
}
public class Subtraction implements Operation {
    public double apply(double a, double b) { return a - b; }
}
public class Multiplication implements Operation {
    public double apply(double a, double b) { return a * b; }
}
// Usage: Operation op = new Addition(); double r = op.apply(2, 3);

Now adding operations does not require modifying existing code, and the flags disappear.

Solution 2. We split into three classes with functional cohesion:

public class PaymentService {
    public void processPayment(double amount) { /* ... */ }
}
public class InventoryService {
    public void update(String product, int quantity) { /* ... */ }
}
public class LoggingService {
    public void log(String message) { /* ... */ }
}

Each service can evolve, be tested, and be reused independently.

Solution 3. We encapsulate the calculation inside the objects that own the data:

public class Order {
    private double total;
    private Shipping shipping;
    public double totalPriceWithShipping() { return total + shipping.getCost(); }
}
public double priceWithShipping(Invoice invoice) {
    return invoice.totalPriceWithShipping(); // Invoice delegates to its Order
}

The caller no longer navigates the internal structure; it directly asks for the business value.

Conclusion

In this lesson we have seen that good design comes down, to a large extent, to a single idea: maximize cohesion and minimize coupling. We classified the types of coupling and cohesion, understood that separation of concerns is the principle that enables them, and practiced real refactorings using interfaces and dependency injection. Finally, the Law of Demeter gave us an operational rule for detecting hidden dependencies. These concepts are the foundation on which the SOLID principles are built, which we will study in the next lesson and which are nothing more than concrete, actionable formulations of these same ideas.

Application Architecture Course

Module 1: Fundamentals of Application Architecture

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved