Cohesion and Coupling in Software Design

Updated 2026-03-11

TL;DR

Cohesion measures how focused a class is on a single responsibility, while coupling measures how dependent classes are on each other. High cohesion and low coupling create maintainable, testable code that’s easier to change and understand.

Prerequisites: Understanding of classes and objects, basic OOP concepts (methods, attributes, encapsulation), familiarity with class relationships (composition, inheritance). You should be comfortable reading and writing basic Python classes.

After this topic: Identify high and low cohesion in class designs, recognize tight and loose coupling between classes, refactor code to improve cohesion and reduce coupling, and apply GRASP principles to assign responsibilities effectively during design interviews.

Core Concept

What is Cohesion?

Cohesion refers to how closely related and focused the responsibilities of a single class are. A highly cohesive class does one thing well — all its methods and attributes work together toward a single, well-defined purpose.

Think of cohesion as the “togetherness” within a class. High cohesion means everything in the class belongs together. Low cohesion means the class is doing too many unrelated things.

Why it matters: High cohesion makes classes easier to understand, test, and maintain. When a class has a clear purpose, you know exactly where to look when something needs to change. Low cohesion creates “god classes” that are hard to debug and modify.

What is Coupling?

Coupling measures how much one class depends on another. When Class A uses Class B, they are coupled. The question is: how tightly?

  • Tight coupling: Class A knows intimate details about Class B’s implementation. Changes to B force changes to A.
  • Loose coupling: Class A only knows about B’s public interface. Changes to B’s internals don’t affect A.

Why it matters: Loose coupling makes systems flexible and testable. You can swap implementations, test classes in isolation, and change one part without breaking others. Tight coupling creates fragile systems where one change cascades through many classes.

The Relationship

These concepts work together. High cohesion within classes naturally leads to lower coupling between classes. When each class has a focused responsibility, classes don’t need to know about each other’s internal details.

GRASP Connection: The General Responsibility Assignment Software Patterns (GRASP) provide guidelines for achieving high cohesion and low coupling. Information Expert, Low Coupling, and High Cohesion are core GRASP principles that guide where to place responsibilities in your design.

Visual Guide

High vs Low Cohesion

classDiagram
    class LowCohesion {
        +send_email()
        +calculate_tax()
        +validate_credit_card()
        +generate_report()
        +log_to_database()
    }
    
    class EmailService {
        +send_email()
        +validate_email_format()
    }
    
    class TaxCalculator {
        +calculate_tax()
        +get_tax_rate()
    }
    
    class PaymentProcessor {
        +validate_credit_card()
        +process_payment()
    }
    
    note for LowCohesion "Low Cohesion: Unrelated responsibilities\nin one class"
    note for EmailService "High Cohesion: Focused on\nemail operations only"

Low cohesion puts unrelated responsibilities in one class. High cohesion groups related operations together.

Tight vs Loose Coupling

classDiagram
    class TightlyCoupled {
        -database: MySQLDatabase
        +save_user()
    }
    
    class MySQLDatabase {
        +connect()
        +execute_query()
        +close()
    }
    
    TightlyCoupled --> MySQLDatabase : depends on concrete class
    
    class LooselyC oupled {
        -database: DatabaseInterface
        +save_user()
    }
    
    class DatabaseInterface {
        <<interface>>
        +save()
        +fetch()
    }
    
    class MySQLImpl {
        +save()
        +fetch()
    }
    
    class PostgreSQLImpl {
        +save()
        +fetch()
    }
    
    LooselyCoupled --> DatabaseInterface : depends on interface
    MySQLImpl ..|> DatabaseInterface
    PostgreSQLImpl ..|> DatabaseInterface
    
    note for TightlyCoupled "Tight: Hard to swap database\nHard to test"
    note for LooselyCoupled "Loose: Easy to swap implementations\nEasy to mock for testing"

Tight coupling depends on concrete implementations. Loose coupling depends on abstractions (interfaces).

Examples

Example 1: Low Cohesion Problem

class UserManager:
    """Low cohesion: doing too many unrelated things"""
    
    def __init__(self):
        self.users = []
    
    def add_user(self, user):
        self.users.append(user)
    
    def send_welcome_email(self, user):
        # Email logic here
        print(f"Sending email to {user['email']}")
    
    def calculate_user_discount(self, user):
        # Business logic for discounts
        if user['orders'] > 10:
            return 0.15
        return 0.05
    
    def generate_pdf_report(self, user):
        # PDF generation logic
        print(f"Generating PDF for {user['name']}")
    
    def log_to_database(self, message):
        # Logging logic
        print(f"LOG: {message}")

# Usage
manager = UserManager()
user = {'name': 'Alice', 'email': 'alice@example.com', 'orders': 12}
manager.add_user(user)
manager.send_welcome_email(user)  # Why is UserManager sending emails?
manager.calculate_user_discount(user)  # Why is it calculating discounts?

Output:

Sending email to alice@example.com

Problem: UserManager handles user storage, email, business logic, reporting, and logging. These are unrelated responsibilities. Low cohesion.

Example 2: High Cohesion Solution

class UserRepository:
    """High cohesion: focused only on user data storage"""
    
    def __init__(self):
        self.users = []
    
    def add(self, user):
        self.users.append(user)
        return user
    
    def find_by_email(self, email):
        return next((u for u in self.users if u['email'] == email), None)
    
    def get_all(self):
        return self.users.copy()


class EmailService:
    """High cohesion: focused only on email operations"""
    
    def send_welcome_email(self, user):
        print(f"Sending email to {user['email']}")
        return True
    
    def send_notification(self, user, message):
        print(f"Notifying {user['email']}: {message}")
        return True


class DiscountCalculator:
    """High cohesion: focused only on discount logic"""
    
    def calculate_for_user(self, user):
        if user['orders'] > 10:
            return 0.15
        elif user['orders'] > 5:
            return 0.10
        return 0.05


# Usage
repo = UserRepository()
email_service = EmailService()
discount_calc = DiscountCalculator()

user = {'name': 'Alice', 'email': 'alice@example.com', 'orders': 12}
repo.add(user)
email_service.send_welcome_email(user)
discount = discount_calc.calculate_for_user(user)
print(f"Discount: {discount * 100}%")

Output:

Sending email to alice@example.com
Discount: 15.0%

Solution: Each class has a single, focused responsibility. Easy to test, understand, and modify.

Example 3: Tight Coupling Problem

class OrderProcessor:
    """Tightly coupled to MySQLDatabase"""
    
    def __init__(self):
        # Directly creates a concrete database instance
        self.db = MySQLDatabase()
    
    def process_order(self, order):
        # Business logic
        total = sum(item['price'] for item in order['items'])
        
        # Directly calls MySQL-specific methods
        self.db.connect()
        self.db.execute_query(
            f"INSERT INTO orders VALUES ({order['id']}, {total})"
        )
        self.db.close()
        return total


class MySQLDatabase:
    def connect(self):
        print("Connecting to MySQL...")
    
    def execute_query(self, query):
        print(f"Executing: {query}")
    
    def close(self):
        print("Closing MySQL connection")


# Usage
processor = OrderProcessor()
order = {'id': 1, 'items': [{'price': 10}, {'price': 20}]}
processor.process_order(order)

Output:

Connecting to MySQL...
Executing: INSERT INTO orders VALUES (1, 30)
Closing MySQL connection

Problems:

  • Can’t switch to PostgreSQL without changing OrderProcessor
  • Can’t test OrderProcessor without a real database
  • OrderProcessor knows MySQL-specific details (connect, close)

Example 4: Loose Coupling Solution

from abc import ABC, abstractmethod

class Database(ABC):
    """Abstract interface - the key to loose coupling"""
    
    @abstractmethod
    def save_order(self, order_id, total):
        pass


class MySQLDatabase(Database):
    def save_order(self, order_id, total):
        print(f"MySQL: Saving order {order_id} with total {total}")


class PostgreSQLDatabase(Database):
    def save_order(self, order_id, total):
        print(f"PostgreSQL: Saving order {order_id} with total {total}")


class MockDatabase(Database):
    """For testing - no real database needed"""
    def __init__(self):
        self.saved_orders = []
    
    def save_order(self, order_id, total):
        self.saved_orders.append({'id': order_id, 'total': total})
        print(f"Mock: Saved order {order_id}")


class OrderProcessor:
    """Loosely coupled - depends on abstraction, not concrete class"""
    
    def __init__(self, database: Database):
        # Dependency injection: receives database through constructor
        self.db = database
    
    def process_order(self, order):
        total = sum(item['price'] for item in order['items'])
        # Only knows about the interface method
        self.db.save_order(order['id'], total)
        return total


# Usage - easy to swap implementations
print("=== Using MySQL ===")
mysql_db = MySQLDatabase()
processor1 = OrderProcessor(mysql_db)
order = {'id': 1, 'items': [{'price': 10}, {'price': 20}]}
processor1.process_order(order)

print("\n=== Using PostgreSQL ===")
postgres_db = PostgreSQLDatabase()
processor2 = OrderProcessor(postgres_db)
processor2.process_order(order)

print("\n=== Using Mock for Testing ===")
mock_db = MockDatabase()
processor3 = OrderProcessor(mock_db)
processor3.process_order(order)
print(f"Saved orders: {mock_db.saved_orders}")

Output:

=== Using MySQL ===
MySQL: Saving order 1 with total 30

=== Using PostgreSQL ===
PostgreSQL: Saving order 1 with total 30

=== Using Mock for Testing ===
Mock: Saved order 1
Saved orders: [{'id': 1, 'total': 30}]

Solution Benefits:

  • OrderProcessor doesn’t know which database it’s using
  • Easy to swap MySQL for PostgreSQL
  • Easy to test with MockDatabase
  • Changes to database implementation don’t affect OrderProcessor

Java/C++ Note: In Java, use interface instead of ABC. In C++, use pure virtual functions (abstract base classes). The principle is identical.

Try It Yourself

Refactor this low-cohesion class into multiple high-cohesion classes:

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def calculate_discount(self, customer_type):
        # Discount logic
        pass
    
    def send_price_alert(self, email):
        # Email logic
        pass
    
    def save_to_database(self):
        # Database logic
        pass

Hint: Identify at least 3 separate responsibilities.

Common Mistakes

1. Confusing Cohesion with “Small Classes”

Mistake: Thinking high cohesion means classes must be tiny.

# Not necessarily better
class UserNameValidator:
    def validate(self, name):
        return len(name) > 0

class UserEmailValidator:
    def validate(self, email):
        return '@' in email

class UserAgeValidator:
    def validate(self, age):
        return age >= 18

Why it’s wrong: These validators are related — they all validate user data. Splitting them creates unnecessary complexity.

Better approach:

class UserValidator:
    """High cohesion: all validation logic together"""
    def validate_name(self, name):
        return len(name) > 0
    
    def validate_email(self, email):
        return '@' in email
    
    def validate_age(self, age):
        return age >= 18
    
    def validate_all(self, user):
        return (self.validate_name(user['name']) and 
                self.validate_email(user['email']) and 
                self.validate_age(user['age']))

2. Creating Coupling Through Shared Mutable State

Mistake: Multiple classes modifying the same global or shared object.

# Global state creates hidden coupling
user_cache = {}

class UserService:
    def add_user(self, user):
        user_cache[user['id']] = user

class ReportGenerator:
    def generate(self):
        # Tightly coupled to UserService through shared state
        for user_id, user in user_cache.items():
            print(user['name'])

Why it’s wrong: ReportGenerator is coupled to UserService through user_cache. Changes to how UserService manages the cache break ReportGenerator.

Better approach: Pass data explicitly or use dependency injection.

3. Thinking Interfaces Alone Guarantee Loose Coupling

Mistake: Creating an interface but still depending on implementation details.

class PaymentProcessor(ABC):
    @abstractmethod
    def process(self, amount):
        pass

class CreditCardProcessor(PaymentProcessor):
    def process(self, amount):
        return {'status': 'success', 'transaction_id': '12345'}

class OrderService:
    def __init__(self, processor: PaymentProcessor):
        self.processor = processor
    
    def place_order(self, amount):
        result = self.processor.process(amount)
        # Still coupled! Assumes specific return format
        if result['status'] == 'success':
            print(f"Order placed: {result['transaction_id']}")

Why it’s wrong: OrderService depends on the specific dictionary structure returned by CreditCardProcessor. If another processor returns a different format, OrderService breaks.

Better approach: Define the return type in the interface contract.

4. Over-Engineering with Too Many Layers

Mistake: Adding abstraction layers “just in case” you might need them.

# Over-engineered
class IUserRepository(ABC):
    pass

class IUserRepositoryFactory(ABC):
    pass

class IUserRepositoryFactoryProvider(ABC):
    pass

Why it’s wrong: Adds complexity without benefit. Follow YAGNI (You Aren’t Gonna Need It). Add abstractions when you have a concrete reason (multiple implementations, testing needs).

5. Ignoring the “Information Expert” Principle

Mistake: Putting methods in classes that don’t have the data needed.

class Order:
    def __init__(self, items):
        self.items = items

class OrderCalculator:
    def calculate_total(self, order):
        # Has to reach into Order's data
        return sum(item.price for item in order.items)

Why it’s wrong: Creates coupling. OrderCalculator needs to know Order’s internal structure.

Better approach: Put the method where the data lives.

class Order:
    def __init__(self, items):
        self.items = items
    
    def calculate_total(self):
        # Information Expert: Order has the data, so it calculates
        return sum(item.price for item in self.items)

Interview Tips

What Interviewers Look For

1. Can you identify cohesion and coupling problems in existing code?

Interviewers often show you poorly designed code and ask: “What’s wrong with this design?” Practice identifying:

  • Classes doing too many things (low cohesion)
  • Classes that know too much about each other (tight coupling)

Example question: “This UserManager class handles authentication, email, and database operations. What would you change?”

Strong answer: “This class has low cohesion — it’s handling three unrelated responsibilities. I’d split it into AuthenticationService, EmailService, and UserRepository. Each would have high cohesion, focused on one responsibility.”

2. Can you refactor toward better design?

Be ready to refactor code on the spot. Common scenarios:

  • Extract classes from a god class
  • Introduce interfaces to reduce coupling
  • Apply dependency injection

Practice this: Take any 50-line class and identify how to split it into 2-3 focused classes.

3. Can you explain the tradeoffs?

Interviewers want to see you think critically. Sometimes tight coupling is acceptable (e.g., a class and its private helper class).

Example question: “When might you accept tighter coupling?”

Strong answer: “When two classes are always used together and unlikely to change independently, like a TreeNode and Tree. The coupling is localized and doesn’t spread through the system. However, I’d still keep the coupling one-directional.”

Specific Phrases to Use

  • “This class violates the Single Responsibility Principle, leading to low cohesion.”
  • “I’d introduce an interface here to reduce coupling and make this testable.”
  • “By applying dependency injection, we can invert the dependency and achieve loose coupling.”
  • “This follows the Information Expert principle — the class that has the data should have the methods that use it.”

Red Flags to Avoid

  • Don’t say “coupling is always bad” — some coupling is necessary
  • Don’t over-engineer — explain why you’re adding abstraction
  • Don’t confuse cohesion with class size — focus on relatedness of responsibilities

System Design Connection

In system design interviews, these principles scale up:

  • High cohesion → Microservices with focused responsibilities
  • Low coupling → Services communicate through well-defined APIs
  • Loose coupling → Services can be deployed and scaled independently

Mention this connection to show you understand how design principles scale.

Practice Exercise

Before your interview, practice this 5-minute drill:

  1. Draw a class diagram with 3-4 classes
  2. Identify one coupling issue
  3. Explain how you’d fix it with an interface or dependency injection
  4. Explain the benefit in terms of testing or flexibility

Being able to do this quickly shows deep understanding.

Key Takeaways

  • High cohesion means a class has a single, focused responsibility. All its methods and attributes work together toward one purpose. This makes classes easier to understand, test, and maintain.

  • Low coupling means classes depend on abstractions (interfaces) rather than concrete implementations. This allows you to swap implementations, test in isolation, and change one class without breaking others.

  • Use dependency injection to achieve loose coupling. Pass dependencies through constructors or methods rather than creating them inside the class. This inverts control and makes code flexible.

  • Follow the Information Expert principle from GRASP: assign responsibility to the class that has the information needed to fulfill it. This naturally leads to high cohesion and low coupling.

  • Balance is key: Some coupling is necessary. Focus on reducing coupling between modules/subsystems while accepting coupling within closely related classes. Don’t over-engineer with unnecessary abstraction layers.