Skip to main content
Create your own
Lesson illustration

Coupling, Cohesion, and Maintainability

Hello! Welcome to the first lesson in our second module, Fundamental Design Principles.

In the previous module, we built a solid foundation in Java's Object-Oriented Programming features. You learned about the four pillars of OOP, how to structure classes with inheritance and interfaces, and how to use static members for class-level functionality. Now, we move from the "what" of language features to the "how" of good design.

This lesson addresses the learning outcome: Explain coupling and cohesion and their impact on system maintainability. These two concepts are arguably the most fundamental principles in software design. Understanding them is not just academic; it's a practical guide that will help you evaluate the quality of a design, make better decisions in your code, and articulate those decisions clearly—a key skill for system design interviews.

By the end of this lesson, you will be able to define coupling and cohesion, identify their presence in code, and explain why striving for high cohesion and low coupling is essential for building systems that are easy to maintain, extend, and test.

1. The Twin Pillars of Good Design

At its core, designing software is about managing complexity. Coupling and cohesion are two metrics that help us measure and control that complexity. They are like the yin and yang of software design; they are opposing forces that you must balance to create a harmonious and maintainable system.

Let's get a high-level overview of these concepts and why they matter.

Coupling and Cohesion Explained

This video, 'Coupling and Cohesion Explained' by Gui Ferreira, provides an excellent introduction. It explains the relationship between the two concepts and why achieving 'Loosely coupled and cohesive' systems is our goal.

Watch from the beginning to about a minute and a half in for an explanation of coupling and cohesion. Focus on understanding the core benefits mentioned: easier maintenance and increased flexibility.

As the video states, the ideal we're aiming for is High Cohesion and Low Coupling. Let's break down each of these, starting with cohesion.

2. Cohesion: Does It Belong Together?

Cohesion measures how closely related the elements within a single module (like a class or a package) are. Think of it as a measure of focus.

  • High Cohesion (Good): The module does one thing and does it well. All its methods and properties are related and work together to fulfill a single, clear purpose. For example, a JsonParser class where every method is related to parsing JSON.
  • Low Cohesion (Bad): The module is a "junk drawer" of unrelated functionalities. It tries to do too many different things. For example, a Utils class that has methods for sending emails, parsing dates, and validating user input.

High cohesion is directly related to the Single Responsibility Principle (SRP), which we will study in the next module. A class with a single responsibility will naturally be highly cohesive.

To see this in action, let's look at a concrete example of refactoring a class with low cohesion.

A Guide to High Cohesion and Low Coupling | by Teni Gada

This article, 'A Guide to High Cohesion and Low Coupling' by Teni Gada, provides clear Java examples. We'll start by focusing on its explanation of cohesion.

Read the section 'What is Cohesion'. Pay close attention to the low cohesion example of the UserAccountManager class, which handles authentication, profile management, and email. Then, study the refactored example showing how it's refactored into three separate, highly cohesive classes: AuthenticationService, UserProfile, and EmailService.

As you saw in the article, the benefits of high cohesion are significant:

  • Maintainability: When you need to change user authentication logic, you know to go directly to AuthenticationService. The change is localized and less likely to break unrelated features like profile management.
  • Readability: The purpose of EmailService is immediately obvious from its name. The code is easier to understand.
  • Reusability: You could easily reuse the EmailService in another part of your application that needs to send emails, without pulling in all the baggage of user authentication.

3. Coupling: How Tied Together Are They?

Coupling measures the degree of interdependence between different modules. It's about how much one module knows about, and relies on, another.

  • Low Coupling (or Loose Coupling - Good): Modules are largely independent. A change in one module has little to no impact on others. They communicate through stable, well-defined interfaces. This is the goal.
  • High Coupling (or Tight Coupling - Bad): Modules are heavily dependent on each other's internal details. A change in one class forces you to make changes in several other classes. This creates a "domino effect" that makes maintenance a nightmare.

Let's return to the article to see a practical example of reducing coupling.

A Guide to High Cohesion and Low Coupling | by Teni Gada

Now, let's examine the coupling part of the same article.

Find the section 'What is Coupling?'. Read the coupling overview. Notice how in the 'Bad' example, the OrderProcessor directly creates a new PaymentGateway(). This is a tight coupling to a concrete class. Then, see how the 'Good' example breaks this dependency by introducing a PaymentProcessor interface and using dependency injection.

The technique shown—depending on an interface (PaymentProcessor) rather than a concrete class (PaymentGateway)—is a cornerstone of good object-oriented design and a primary way to achieve low coupling. If you later decide to add a new payment method, like PayPalProcessor, the OrderProcessor class doesn't need to change at all, as long as PayPalProcessor also implements the PaymentProcessor interface.

This table provides a great summary of the differences.

This table contrasts the key characteristics of loosely and tightly coupled systems across dimensions like dependency, scalability, and maintainability. Tightly coupled systems, like monolithic apps, are hard to change, while loosely coupled systems, like microservices, offer greater flexibility.

4. The Ideal: High Cohesion and Low Coupling

Cohesion and coupling are two sides of the same coin. A system designed with highly cohesive modules often naturally results in low coupling between them. When a class is focused on a single responsibility, it has fewer reasons to be entangled with other classes.

This diagram visually contrasts a bad design (left) with a good design (right). The 'bad' design shows components with many dependencies between them (high coupling) and internally scattered elements (low cohesion). The 'good' design shows components with few external dependencies (low coupling) and closely related internal elements (high cohesion).

For a deeper dive into the relationship and a quick summary, the Baeldung article "Difference Between Cohesion and Coupling" is an excellent resource. You don't need to read it all now, but it's a great reference. It reinforces that high cohesion often leads to loose coupling, and vice-versa.

Test your understanding!

You are reviewing the following code for an e-commerce application. The Order class is responsible for managing order details and also handles notifying the customer and updating the warehouse inventory.

class Order {
    private List<Item> items;
    private Customer customer;

    public Order(Customer customer) {
        this.customer = customer;
        this.items = new ArrayList<>();
    }

    public void addItem(Item item) {
        items.add(item);
    }

    // Processes the order, notifies the user, and updates inventory
    public void processOrder() {
        // 1. Finalize order logic (e.g., calculate total)
        System.out.println("Order processed.");

        // 2. Send an email notification
        String email = customer.getEmail();
        EmailSender sender = new EmailSender("smtp.example.com");
        sender.send(email, "Your order is processed!");

        // 3. Update warehouse inventory
        Warehouse warehouse = new Warehouse("MainWarehouse");
        for (Item item : items) {
            warehouse.decrementStock(item.getId());
        }
    }
}

// Helper classes (implementations not important)
class Item { /* ... */ String getId() { /*...*/ } }
class Customer { /* ... */ String getEmail() { /*...*/ } }
class EmailSender { public EmailSender(String host) {/*...*/} public void send(String to, String body) {/*...*/} }
class Warehouse { public Warehouse(String id) {/*...*/} public void decrementStock(String itemId) {/*...*/} }

Identify the main issues related to coupling and cohesion in the Order class and suggest how you would refactor it.

Show answer

Issues:

  1. Low Cohesion: The Order class has low cohesion. Its primary responsibility should be managing the state of an order (items, customer, total price). However, the processOrder method is also responsible for two other distinct tasks: sending email notifications and updating warehouse inventory. These are separate concerns.
  2. High Coupling: The Order class is tightly coupled to the concrete EmailSender and Warehouse classes. It knows how to instantiate them (new EmailSender(...), new Warehouse(...)) and calls their specific methods. If the EmailSender's constructor changes, or if we want to use a different notification method (like SMS), we would have to modify the Order class. The same applies to the Warehouse.

Refactoring Suggestions:

To achieve high cohesion and low coupling:

  1. Improve Cohesion: Extract the unrelated responsibilities into their own dedicated classes.

    • Create a NotificationService to handle sending notifications.
    • Create an InventoryService to manage warehouse inventory.
  2. Reduce Coupling: Use interfaces and dependency injection. The Order class should not create its dependencies; they should be provided to it.

    • Define interfaces like NotificationService and InventoryService.
    • Implement these interfaces with concrete classes like EmailNotificationService and WarehouseInventoryService.
    • The class responsible for coordinating the process (e.g., an OrderProcessingService) would then use these components together. The Order object itself would just be a data container.

A better design might look like this:

// Interfaces for low coupling
interface NotificationService {
    void sendNotification(Customer customer, String message);
}
interface InventoryService {
    void updateStock(List<Item> items);
}

// The Order class is now highly cohesive (just holds order data)
class Order {
    // ... items, customer, etc.
}

// A new service to orchestrate the process
class OrderProcessingService {
    private final InventoryService inventoryService;
    private final NotificationService notificationService;

    // Dependencies are injected via the constructor
    public OrderProcessingService(InventoryService inventoryService, NotificationService notificationService) {
        this.inventoryService = inventoryService;
        this.notificationService = notificationService;
    }

    public void processOrder(Order order) {
        // 1. Finalize order logic (remains here)
        System.out.println("Order processed.");
        
        // 2. Delegate to the inventory service
        inventoryService.updateStock(order.getItems());

        // 3. Delegate to the notification service
        notificationService.sendNotification(order.getCustomer(), "Your order is processed!");
    }
}

This new design is much more maintainable. The Order class is simple, and the logic for notifications and inventory can be changed or replaced without touching the OrderProcessingService.

5. From Code to System Architecture

So far, we've discussed coupling and cohesion at the class level (LLD). However, these concepts scale up and are critical in high-level system design (HLD), especially when designing microservices.

SOLID Principles? Nope, just Coupling and Cohesion

The video 'SOLID Principles? Nope, just Coupling and Cohesion' by CodeOpinion offers a pragmatic view. It argues that these two principles are the ultimate guides for design decisions, from a single class to an entire architecture. This perspective is invaluable for system design interviews.

Starting about a minute in, watch the main discussion. This is a dense and insightful segment. Focus on: The core idea: thinking about coupling and cohesion is the main goal. Functional Cohesion: Grouping things by business tasks/capabilities, not just by data (like an 'entity service'). The example of a Product entity and how different services (Sales, Warehouse, Purchasing) have different behaviors and data needs related to it. This shows how to achieve highly cohesive service boundaries. How organizing code by features (/features/shoppingCart/addToCart) leads to higher cohesion than organizing by technical layers (/controllers, /models).

This video elevates the discussion from simple class design to architectural thinking. The key takeaway is to define module or service boundaries around business capabilities. A service that handles "all things Product" has low cohesion and will be highly coupled to many other services. In contrast, a "Pricing" service and an "Inventory" service are highly cohesive and can operate more independently, even though both relate to products.

Conclusion

In this lesson, we've established the importance of coupling and cohesion as the bedrock of good software design. They provide the "why" behind many of the principles and patterns you will learn throughout this course.

Key Takeaways:

  • Cohesion is about how well the parts inside a module belong together. Goal: High Cohesion.
  • Coupling is about how dependent modules are on each other. Goal: Low Coupling.
  • Impact on Maintainability: High cohesion and low coupling lead to systems that are easier to understand, cheaper to modify, and simpler to test. Changes are localized, and the risk of unintended side effects is reduced.
  • Application: These principles apply at all levels, from structuring methods within a class (LLD) to defining service boundaries in a microservices architecture (HLD).
  • Achieving the Goal: Use techniques like defining clear responsibilities (SRP), programming to interfaces, and dependency injection to achieve high cohesion and low coupling.

In our next lesson, we will dive into a specific, powerful technique for creating flexible systems: the 'composition over inheritance' principle. You will see how it directly helps in achieving lower coupling compared to traditional inheritance hierarchies.

Can't find a good explanation? Sign up and we'll make it for you

Sign up