High Cohesion, Loose Coupling
Master the fundamental principles that separate maintainable, scalable software from tangled spaghetti code. Learn how to build systems that embrace change.
Why This Matters More Than Ever
We live in a time where businesses change their minds constantly. And they're right to do so—it gives them a competitive edge. Your job as a software engineer is to build systems that can adapt to this change without falling apart.
Here's the truth: most software development effort happens after the initial release. If you want your software to succeed long-term, you must design it to respond easily to change.
There is no silver bullet in software development.
But there are fundamental principles that consistently lead to better outcomes.
High Cohesion: Keep Related Things Together
Cohesion measures how strongly related the responsibilities of a single module (class, function, service) are. High cohesion means everything in the module works together toward a single, well-defined purpose.
Cohesion Visualized
Low Cohesion Class
Methods barely use the class fields - unrelated functionality grouped together
Key Insight: In low cohesion classes, methods operate independently and could be split into separate classes. This makes the code harder to maintain and understand.
Types of Cohesion: From Worst to Best
Functional Cohesion
Parts grouped because they all contribute to a single, well-defined task. The BEST type of cohesion!
class EmailValidator {
private email: string;
isValidFormat() { }
hasValidDomain() { }
checkDNSRecords() { }
validate() { }
}Cohesion in TypeScript Classes
Low Cohesion - Utility Mess
class UtilityClass {
private elementA: number;
private elementB: number;
private elementC: string;
// This method only uses elementA
calculateTax(): number {
return this.elementA * 0.15;
}
// This method only uses elementB
formatCurrency(): string {
return `$${this.elementB}`;
}
// This method doesn't use any fields!
sendEmail(to: string, subject: string): void {
// Email logic...
}
// Another unrelated method
parseXML(xml: string): object {
// XML parsing...
}
}High Cohesion - Single Responsibility
// High cohesion: All methods work with user account data
class UserAccount {
private username: string;
private email: string;
private balance: number;
// All methods use the class fields
updateProfile(name: string, email: string): void {
this.username = name;
this.email = email;
this.notifyProfileUpdate();
}
sendNotification(message: string): void {
// Uses email field
emailService.send(this.email, message);
}
processPayment(amount: number): boolean {
// Uses balance and email
if (this.balance >= amount) {
this.balance -= amount;
this.sendNotification(`Payment of $${amount} processed`);
return true;
}
return false;
}
private notifyProfileUpdate(): void {
this.sendNotification('Profile updated successfully');
}
}The good example shows high cohesion: all methods are focused on managing a user account and use the class fields. The bad example has low cohesion: methods are unrelated and barely use the class state.
🎯 How to Improve Cohesion:
- •Split utility classes - If methods don't share state, they shouldn't share a class
- •Extract hidden objects - If some methods only use subset of fields, extract a new class
- •Single Responsibility Principle - Each class should have one reason to change
- •Use metrics - Tools like SonarQube (LCOM4) can identify low cohesion
Loose Coupling: Minimize Dependencies
Coupling measures how much one component knows about the inner workings of another. Loose coupling means components depend on each other to the least extent possible—they communicate through well-defined interfaces, not implementation details.
💡 The iPod Analogy: iPods are tightly coupled—when the battery dies, you might as well buy a new one because the battery is soldered in. A loosely coupled music player would let you easily swap the battery. Same principle applies to software.
Coupling Visualized
Tight Coupling
ClassA directly depends on ClassB's implementation details
Key Insight: Tight coupling makes your code rigid and hard to change. Any modification to ClassB requires changes to ClassA, creating a maintenance nightmare.
Coupling in TypeScript
Tight Coupling
// Bad: ClassA directly creates and depends on ClassB
class ClassA {
private elementA: boolean;
methodA(): number {
if (this.elementA) {
// Directly instantiating ClassB - tight coupling!
return new ClassB().elementB;
}
return 0;
}
printValues(): void {
// Creating another instance - tightly coupled
new ClassB().methodB();
}
}
class ClassB {
public elementB: number = 42;
methodB(): void {
console.log(this.elementB);
}
}
// What if ClassB constructor changes?
// What if we need a different implementation?
// ClassA breaks!Loose Coupling via Dependency Injection
// Good: ClassA depends on interface, not implementation
interface IService {
getData(): number;
printData(): void;
}
class ClassA {
// Dependency injected through constructor
constructor(private service: IService) {}
methodA(): number {
// Uses the interface, not the concrete class
return this.service.getData();
}
printValues(): void {
this.service.printData();
}
}
// Implementation can change without affecting ClassA
class ClassB implements IService {
private elementB: number = 42;
getData(): number {
return this.elementB;
}
printData(): void {
console.log(this.elementB);
}
}
// Easy to swap implementations!
class MockService implements IService {
getData(): number { return 100; }
printData(): void { console.log('Mock!'); }
}
// Usage - dependency injection
const service = new ClassB();
const classA = new ClassA(service); // Loose coupling!
// For testing
const mockService = new MockService();
const testClassA = new ClassA(mockService); // Same interface!Loose coupling through interfaces enables you to swap implementations without changing dependent code. This makes testing easier, refactoring safer, and your code more flexible.
🎯 How to Reduce Coupling:
- •Depend on abstractions - Use interfaces/types, not concrete classes
- •Dependency Injection - Pass dependencies in rather than creating them
- •Event-driven communication - Components react to events, not direct calls
- •API contracts - Define stable interfaces, hide implementation
The Law of Demeter
The Law of Demeter (LoD), also known as the Principle of Least Knowledge, is a special case of loose coupling. It can be summarized in one sentence:
"Don't Talk to Strangers"
Or more simply: "Use only one dot"
Law of Demeter: "Don't Talk to Strangers"
Also known as the Principle of Least Knowledge. A simple rule: "Use only one dot"
Violating the Law
// TypeScript - Too many dots!
class DogWalker {
walkDog(dog: Dog) {
// Reaching through multiple objects
dog.getBody().getLegs().walk();
// ^ ^ ^ ^ = 4 method calls!
// Even worse - chain of dependencies
const speed = dog
.getOwner()
.getHouse()
.getAddress()
.getCity()
.getTrafficLevel();
}
}Problem: DogWalker knows too much about Dog's internals. If the internal structure changes, this breaks! You're "talking to strangers" (Body, Legs, Owner, House, etc.)
🎯 Analogy: Imagine asking your friend for their wallet, then opening it yourself to take money out. Awkward! Instead, you'd just ask them for the money.
A method may only call methods of:
- The object itself (this.someMethod())
- Objects passed as arguments
- Objects it creates locally
- Its direct properties/fields
Modern Real-World Examples
These principles aren't just theory—they're essential in modern software architecture. Let's see how they apply to React, microservices, and API design.
Modern Examples: High Cohesion & Loose Coupling in 2024
React Components: Cohesion & Coupling
Low Cohesion, Tight Coupling
// Bad: "God Component" doing everything
function UserDashboard() {
// Handles auth, data fetching, UI, analytics...
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [analytics, setAnalytics] = useState({});
useEffect(() => {
// Direct database calls
fetch('/api/user').then(r => r.json()).then(setUser);
fetch('/api/posts').then(r => r.json()).then(setPosts);
fetch('/api/analytics').then(r => r.json()).then(setAnalytics);
// Analytics tracking
window.gtag('event', 'page_view');
}, []);
return (
<div>
{/* Mixing concerns: data, UI, business logic */}
<h1>{user?.name}</h1>
{posts.map(post => (
<div onClick={() => {
// Business logic in UI
fetch(`/api/posts/${post.id}/like`, {method: 'POST'});
window.gtag('event', 'like', {post_id: post.id});
}}>
{post.title}
</div>
))}
</div>
);
}High Cohesion, Loose Coupling
// Good: Separated concerns with custom hooks & services
// Custom hook - high cohesion (all about user data)
function useUser() {
const [user, setUser] = useState(null);
useEffect(() => {
userService.getCurrentUser().then(setUser);
}, []);
return user;
}
// Service layer - loose coupling via interfaces
interface IPostService {
getPosts(): Promise<Post[]>;
likePost(id: string): Promise<void>;
}
// Component - focused only on UI
function UserDashboard() {
const user = useUser();
const { posts, likePost } = usePosts();
const analytics = useAnalytics();
return (
<div>
<UserHeader user={user} />
<PostList posts={posts} onLike={likePost} />
<AnalyticsWidget data={analytics} />
</div>
);
}
// Each component has ONE job
function PostList({ posts, onLike }: Props) {
return posts.map(post => (
<PostCard key={post.id} post={post} onLike={onLike} />
));
}Key improvements: Custom hooks for data logic, service layer for loose coupling, components focused on single responsibilities.
The Path to Maintainable Software
Building maintainable software is part science, part art. You need to predict the future—where will changes come from? But you don't need to predict perfectly.
Key Takeaways
- ✓High cohesion keeps related functionality together, making code easier to understand and maintain
- ✓Loose coupling minimizes dependencies, making code easier to change and test
- ✓Law of Demeter prevents reaching through objects, reducing brittleness
- ✓These principles scale from functions to microservices—they're fundamental
When components are loosely coupled and highly cohesive, you can:
- Change implementations without breaking dependents
- Test components in isolation
- Develop and deploy independently
- Scale teams and systems effectively
- Respond to business changes quickly
"The only thing that separates good software from bad is the value it provides to the business at any particular point in time. To prolong that value, make your software respond easily to change."