Cohesion and Coupling in Software Design
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.
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:
- Draw a class diagram with 3-4 classes
- Identify one coupling issue
- Explain how you’d fix it with an interface or dependency injection
- 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.