TL;DR: High cohesion means classes focus on one job. Low coupling means classes don’t depend too much on each other. When I refactor messy code, these two principles guide every decision I make.

Why I Focus on Cohesion and Coupling When Refactoring

After 10+ years of building enterprise applications, I’ve inherited my fair share of messy codebases. The pattern is always the same: tangled classes doing too much, components that break when you touch something seemingly unrelated, and tests that require half the application to run.

When I approach refactoring messy code, I always start with the same two questions:

  • Does this class have a clear, single purpose? (cohesion)
  • How many other parts of the system would break if I changed this? (coupling)

Cohesion measures how closely related a class’s responsibilities are. I think of it as the “focus score” of your class.

Coupling measures how dependent one class is on others. It’s the “independence score” between your components.

These principles aren’t just theory, they’re my practical roadmap for turning spaghetti code into maintainable systems.

My Approach to Identifying Low Cohesion

What Good Cohesion Looks Like

When I find a class with high cohesion, it’s a breath of fresh air. Here’s what I consider a well-focused class:

public class Customer
{
    private readonly string _name;
    private readonly string _email;

    public Customer(string name, string email)
    {
        _name = name;
        _email = email;
    }

    public string GetName() => _name;
    public string GetEmail() => _email;

    public void UpdateEmail(string newEmail)
    {
        // Validation logic would go here
        _email = newEmail;
    }
}

This Customer class has high cohesion because:

  • It focuses solely on customer identity
  • Every method works with the customer’s core data
  • It doesn’t try to handle database operations or business logic

The Messy Reality: Low Cohesion in Action

Here’s the kind of mess I encounter when refactoring legacy code:

class Student {
    name: string;
    grade: number;
    isEnrolled: boolean = false;

    constructor(name: string, grade: number) {
        this.name = name;
        this.grade = grade;
    }

    enroll(): void {
        this.isEnrolled = true;
    }

    getGPA(): number {
        return this.grade / 100;
    }

    // This shouldn't be here!
    save(): void {
        console.log(`Saving ${this.name} to database...`);
        // Database code
    }

    // Neither should this!
    sendEmail(): void {
        console.log(`Sending email to ${this.name}...`);
        // Email code
    }
}

This class violates the Single Responsibility Principle by mixing student data, database operations, and communication logic. When I see code like this, I know it’s time to refactor.

How I Refactor for Better Cohesion

Here’s my systematic approach to breaking down that messy Student class:

// Focused on student data only
class Student {
    constructor(
        public readonly name: string,
        private grade: number,
        private isEnrolled: boolean = false
    ) {}

    getGPA(): number {
        return this.grade / 100;
    }

    enroll(): void {
        this.isEnrolled = true;
    }

    isCurrentlyEnrolled(): boolean {
        return this.isEnrolled;
    }
}

// Handles all database operations
class StudentRepository {
    saveStudent(student: Student): void {
        console.log(`Saving ${student.name}...`);
        // Database logic here
    }
}

// Manages communications
class StudentNotifier {
    sendEmailToStudent(student: Student, message: string): void {
        console.log(`Sending "${message}" to ${student.name}`);
        // Email logic here
    }
}

Now each class has a single, clear responsibility.

Managing Coupling in Object-Oriented Design

High Coupling Problem

public class Order
{
    private readonly Product _product;
    private readonly int _quantity;

    public Order(Product product, int quantity)
    {
        _product = product;
        _quantity = quantity;
    }

    public decimal CalculateTotal()
    {
        return _product.Price * _quantity;
    }

    public void Ship()
    {
        // Tightly coupled to specific implementations
        var shipper = new UPSShipper();
        shipper.Ship(this);

        EmailService.SendConfirmation(this);
    }
}

This creates tight coupling because:

  • Order directly creates UPSShipper instances
  • Changes to shipping providers require Order class modifications
  • Testing becomes complex due to concrete dependencies

Low Coupling Solution with Dependency Injection

public class OrderProcessor
{
    private readonly IShippingProvider _shippingProvider;
    private readonly INotificationService _notificationService;

    public OrderProcessor(
        IShippingProvider shippingProvider,
        INotificationService notificationService)
    {
        _shippingProvider = shippingProvider;
        _notificationService = notificationService;
    }

    public void ProcessOrder(Order order)
    {
        // Use interfaces, not concrete classes
        _shippingProvider.ShipOrder(order);
        _notificationService.NotifyCustomer(order);
    }
}

public interface IShippingProvider
{
    void ShipOrder(Order order);
}

public class UPSShippingProvider : IShippingProvider
{
    public void ShipOrder(Order order)
    {
        // UPS-specific implementation
    }
}

public class FedExShippingProvider : IShippingProvider
{
    public void ShipOrder(Order order)
    {
        // FedEx-specific implementation
    }
}

This achieves low coupling by:

  • Depending on interfaces rather than concrete classes
  • Injecting dependencies instead of creating them
  • Allowing easy substitution of implementations

Benefits of High Cohesion and Low Coupling

High Cohesion Benefits

BenefitDescription
MaintainabilityChanges are isolated to single classes
TestabilityFocused classes are easier to unit test
ReusabilitySingle-purpose classes work in multiple contexts
ReadabilityClear class purpose improves code understanding

Low Coupling Benefits

BenefitDescription
FlexibilityEasy to swap implementations
Parallel DevelopmentTeams can work independently
Fault IsolationBugs don’t cascade across components
TestingMock dependencies easily

Practical Implementation Strategies

Achieving High Cohesion

  1. Apply Single Responsibility Principle: Each class should have one reason to change
  2. Use meaningful class names: If you can’t describe a class in one sentence, it’s probably doing too much
  3. Group related data and behavior: Keep data and the methods that operate on it together
  4. Extract specialized classes: When methods don’t use class fields, consider moving them

Reducing Coupling

  1. Program to interfaces: Depend on abstractions, not concrete implementations
  2. Use dependency injection: Pass dependencies through constructors
  3. Apply the Dependency Inversion Principle: High-level modules shouldn’t depend on low-level modules
  4. Minimize class knowledge: Classes should know as little as possible about their collaborators

Real-World Angular Example

Here’s how these principles apply in an Angular service:

// High cohesion - focused on user data management
@Injectable({
  providedIn: 'root'
})
export class UserService {
  private readonly apiUrl = '/api/users';

  constructor(private http: HttpClient) {}

  getUser(id: string): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }

  updateUser(user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
  }
}

// Low coupling - depends on abstractions
@Component({
  selector: 'app-user-profile',
  template: '...'
})
export class UserProfileComponent {
  user$ = signal<User | null>(null);

  constructor(
    private userService: UserService,
    private notificationService: INotificationService
  ) {}

  async saveUser(user: User): Promise<void> {
    try {
      const updatedUser = await this.userService.updateUser(user);
      this.user$.set(updatedUser);
      this.notificationService.showSuccess('Profile updated successfully');
    } catch (error) {
      this.notificationService.showError('Failed to update profile');
    }
  }
}

Measuring Code Quality

Use these metrics to evaluate your code:

  • Cohesion indicators: Can you describe a class’s purpose in one sentence?
  • Coupling indicators: How many classes need changes when requirements change?
  • Test complexity: How much setup is required for unit tests?

Common Pitfalls to Avoid

  1. God classes: Classes that know or do too much
  2. Feature envy: Methods that use data from other classes more than their own
  3. Inappropriate intimacy: Classes that know too much about each other’s internals
  4. Shotgun surgery: Small changes requiring modifications across many classes

Conclusion

High cohesion and low coupling aren’t just theoretical concepts, they’re practical tools that directly impact your development experience. When you build systems with focused classes and minimal dependencies, you create code that’s easier to understand, test, and maintain.

Start applying these principles in your next feature. Ask yourself: “Does this class have a single, clear purpose?” and “Can I change this component without breaking others?” The answers will guide you toward better software design.

The investment in understanding cohesion and coupling pays dividends every time you need to modify, debug, or extend your code. Your future self, and your teammates, will thank you.

About the Author

Abhinaw Kumar is a software engineer who builds real-world systems: from resilient ASP.NET Core backends to clean, maintainable Angular frontends. With over 11+ years in production development, he shares what actually works when you're shipping software that has to last.

Read more on the About page or connect on LinkedIn.

References

Frequently Asked Questions

What is high cohesion in software design?

High cohesion means that a class or module focuses on a single responsibility. All its methods and properties should contribute to one clearly defined purpose, making the code easier to maintain and test.

What is low coupling in object-oriented programming?

Low coupling refers to minimizing dependencies between components. When classes know little about each other and interact through well-defined interfaces, changes are less likely to cause cascading failures.

How do you identify low cohesion in a class?

A class has low cohesion when it tries to do too many unrelated things, like handling data, business logic, and infrastructure concerns. If its methods don’t operate on common data or logic, that’s a red flag.

How can I reduce coupling in my code?

Use dependency injection, program to interfaces, and follow the Dependency Inversion Principle. These techniques ensure that high-level logic remains independent of low-level details.

Can I apply these principles in frontend frameworks like Angular?

Yes. In Angular, cohesion means keeping services focused (e.g., a UserService that only handles user data), and coupling is reduced by depending on interfaces or injection tokens instead of concrete classes.

Why are high cohesion and low coupling important?

Together, they improve code maintainability, testability, and scalability. Systems built with these principles are easier to modify, extend, and debug without introducing new bugs.

Related Posts