.net made easy[]

Coupling and Cohesion in C#: High Cohesion, Low Coupling

AdvancedLesson 213 of 315·Production .NET·26 min read·Updated

The vocabulary underneath every design principle you've learned so far: how tightly things stick together, and how well things inside one thing belong together.

You've now learned the Single Responsibility Principle, the Open/Closed Principle, Interface Segregation, and Dependency Inversion. Each one felt like a distinct rule with its own example. Here's the thing nobody told you yet: all four of them are really techniques for achieving two underlying properties — properties so fundamental to software design that they predate SOLID, predate design patterns, and show up in essentially every serious discussion of software architecture ever written. Those two properties are coupling and cohesion.

Once you can see code in terms of coupling and cohesion, the SOLID principles stop looking like five arbitrary rules to memorize and start looking like five specific, practical answers to the exact same two questions: how much does this thing know about that thing, and how well do the pieces inside this thing actually belong together?

In this lesson, you'll learn precisely what coupling and cohesion mean, why "high cohesion, low coupling" is the single most repeated goal in software design, how SRP is fundamentally a cohesion technique and Dependency Inversion is fundamentally a coupling technique, and you'll see a genuinely tightly-coupled, low-cohesion class refactored into its loosely-coupled, high-cohesion opposite.

What Is It?

The Simple Explanation

Coupling is how much one piece of code knows about — and depends on — the inner workings of another piece. Two classes are tightly coupled if a change inside one is likely to force a change inside the other. They're loosely coupled if they can change somewhat independently.

Cohesion is how closely related the things inside one class or module actually are. A class has high cohesion if everything in it genuinely serves one clear purpose. It has low cohesion if it's a grab-bag of loosely related behaviors that happen to live in the same file.

The Technical Definition

Coupling measures the degree of interdependence between modules — specifically, how much one module's design or implementation is bound up with another's. Coupling is inevitable (modules must collaborate to do anything useful) — the goal is never zero coupling, it's coupling to stable, abstract things (interfaces, contracts) rather than volatile, concrete things (another class's internal fields, a specific database library, a third-party SDK's exact shape).

Cohesion measures how strongly the responsibilities within a single module relate to one shared purpose. High cohesion means the fields and methods of a class work together toward one goal, are used together, and change together. Low cohesion means a class contains multiple unrelated clusters of behavior that just happen to share a namespace.

Tight Coupling

  • Class A directly instantiates a concrete Class B
  • Class A reaches into Class B's internal fields or private state
  • A change to B's implementation forces a change to A
  • Hard to test A without a real B
  • Hard to swap B for a different implementation

Loose Coupling

  • Class A depends on an abstraction (interface) that B implements
  • A never touches B's internals — only its public contract
  • B can change internally without touching A at all
  • Easy to substitute a test double for B
  • Easy to swap B's implementation entirely

High Cohesion

  • Every member of the class serves one clear purpose
  • Class members are used together, consistently
  • The class has a name you can describe in one honest sentence
  • Changes to the class tend to touch most of it, for one reason

Low Cohesion

  • Members cluster into unrelated groups with little overlap
  • Some fields are only used by some methods, never others
  • The class name is vague ("Manager", "Helper", "Utils")
  • Different changes touch entirely different, unrelated parts of the same class

Why Does It Exist?

The Problem — Change Ripples Through Tightly Coupled, Low-Cohesion Systems

Tight coupling means a change to one class ripples outward into every class that depends on its concrete details — you can't touch one thing without touching several others. Low cohesion means a single class becomes a magnet for unrelated changes — every unrelated feature request finds a reason to edit the same file, and every edit risks breaking behavior that had nothing to do with the change. Put those together and you get exactly the "eighteen months in" nightmare from Lesson 235's OrderManager: everything touches everything, and nobody can predict the blast radius of a one-line change.

The Solution — "High Cohesion, Low Coupling"

This phrase is arguably the single most repeated goal in all of software design, older than SOLID and referenced by essentially every architectural discipline that followed it. It names the target directly: pull related things together (raise cohesion) and push unrelated things apart, connected only through stable abstractions (lower coupling). Everything else you'll learn in this Part — SOLID, design patterns, Clean Architecture — is, underneath, a specific technique for moving code toward this one target.

Big Picture

HOW YOUR TOOLS MAP TO THE TWO GOALS
GOAL: HIGH COHESION — "belongs together, inside one class"
  • Single Responsibility Principle (Lesson 235) — one reason to change is, in practice, a direct test for high cohesion. A class with unrelated reasons to change is, by definition, low-cohesion.
+
GOAL: LOW COUPLING — "depends on the least possible, and only on stable things"
  • Dependency Inversion Principle (Lesson 081) — depending on an abstraction instead of a concrete class is the direct technique for keeping coupling loose.
  • Interface Segregation Principle (Lesson 079) — a narrow interface reduces exactly how much of another module you're coupled to.
  • Open/Closed Principle (Lesson 235) — adding behavior without editing existing code is only possible when that code was loosely coupled to specifics in the first place.
▼
RESULT
  • A system where each piece can change on its own schedule, for its own reason, without dragging the rest of the system along with it

How It Works

DIAGNOSING COUPLING
1. ASK: WHAT DOES THIS CLASS DEPEND ON?
  • Concrete classes (new SqlOrderRepository()) → tight. Interfaces (IOrderRepository) → loose.
▼
2. ASK: DOES IT REACH INTO INTERNAL STATE?
  • Touching another class's private/internal fields directly (even via reflection or shared mutable statics) → tight. Only calling its public contract → loose.
▼
3. ASK: IF THE DEPENDENCY CHANGES INTERNALLY, DO I CHANGE TOO?
  • If swapping the database library, or refactoring a class's private implementation, forces an edit here → tight coupling, regardless of what the code looks like on the surface.
DIAGNOSING COHESION
1. CAN YOU NAME THIS CLASS'S ONE PURPOSE, HONESTLY, IN ONE SENTENCE?
  • If the honest sentence needs "and" to join two unrelated concerns, cohesion is already suspect.
▼
2. DO ALL FIELDS GET USED BY MOST METHODS?
  • A field only touched by two of your class's twelve methods is a sign those two methods belong somewhere else.
▼
3. DO DIFFERENT FEATURE REQUESTS TOUCH DIFFERENT PARTS OF THE SAME CLASS?
  • If "add a discount rule" and "change the email template" both mean editing the same class, that class is doing two unrelated jobs.

Simple Example

Tight coupling in one line:

public class ReportService
{
    private readonly SqlServerConnection _connection = new("Server=prod-db;..."); //  tightly coupled to SQL Server, and even to this connection string
}

Loose coupling, same job:

public class ReportService(IReportDataSource dataSource) //  depends on an abstraction, not a database vendor
{
}

Low cohesion in one class:

public class StoreUtils //  vague name is often the first warning sign
{
    public decimal CalculateTax(decimal amount) { /* ... */ return amount * 0.08m; }
    public void SendWelcomeEmail(string email) { /* ... */ }
    public bool IsWarehouseOpen(DateTime time) { /* ... */ return true; }
}

High cohesion, same behaviors, correctly separated:

public class TaxCalculator { public decimal Calculate(decimal amount) => amount * 0.08m; }
public class WelcomeEmailSender { public void Send(string email) { /* ... */ } }
public class WarehouseScheduleChecker { public bool IsOpenAt(DateTime time) { /* ... */ return true; } }

Real-World Example — Refactoring a Tightly-Coupled, Low-Cohesion Order Class

Before — one class, tightly coupled to three concrete infrastructure pieces, with three unrelated clusters of responsibility:

public class OrderProcessor
{
    private readonly SqlConnection _db = new("Server=prod-db;...");        // tightly coupled — concrete SQL client
    private readonly SmtpClient _smtp = new("smtp.internal.local");        // tightly coupled — concrete SMTP client
    private readonly StripeClient _stripe = new("sk_live_...");            // tightly coupled — concrete Stripe SDK

    public void Process(Order order)
    {
        // Cluster 1 — payment concern
        var charge = _stripe.Charges.Create(new ChargeCreateOptions { Amount = order.TotalCents });

        // Cluster 2 — persistence concern
        var cmd = _db.CreateCommand();
        cmd.CommandText = $"INSERT INTO Orders (...) VALUES (...)";
        cmd.ExecuteNonQuery();

        // Cluster 3 — notification concern
        var mail = new MailMessage("noreply@shop.com", order.CustomerEmail, "Order Confirmed", "Thanks!");
        _smtp.Send(mail);
    }
}

The coupling problem: swap Stripe for PayPal, swap SQL Server for Postgres, or swap SMTP for a transactional email API, and every one of those swaps means editing OrderProcessor directly — because it's welded to the concrete SDK of each, not to an abstraction over what each does.

The cohesion problem: this class has three unrelated reasons to change — payment provider changes, database schema changes, email provider changes — bundled into one file. Nothing about "charging a card" is related to "formatting a SQL insert" other than that they both happen to be called from the same method.

After — loosely coupled to abstractions, each concern pulled into its own high-cohesion class:

public interface IPaymentGateway { Task ChargeAsync(Order order); }
public interface IOrderRepository { Task SaveAsync(Order order); }
public interface IOrderNotifier { Task NotifyConfirmedAsync(Order order); }

// Each implementation is high-cohesion — one job, one reason to change.
public sealed class StripePaymentGateway(StripeClient stripe) : IPaymentGateway
{
    public Task ChargeAsync(Order order) => stripe.Charges.CreateAsync(new ChargeCreateOptions { Amount = order.TotalCents });
}

public sealed class SqlOrderRepository(SqlConnection db) : IOrderRepository
{
    public Task SaveAsync(Order order) => /* INSERT ... */ Task.CompletedTask;
}

public sealed class EmailOrderNotifier(SmtpClient smtp) : IOrderNotifier
{
    public Task NotifyConfirmedAsync(Order order) =>
        Task.FromResult(smtp.Send(new MailMessage("noreply@shop.com", order.CustomerEmail, "Order Confirmed", "Thanks!")));
}

// OrderProcessor is now loosely coupled — depends on three abstractions, none of their concrete implementations —
// and high-cohesion — its one job is "orchestrate the checkout sequence," nothing more.
public sealed class OrderProcessor(IPaymentGateway payments, IOrderRepository orders, IOrderNotifier notifier)
{
    public async Task ProcessAsync(Order order)
    {
        await payments.ChargeAsync(order);
        await orders.SaveAsync(order);
        await notifier.NotifyConfirmedAsync(order);
    }
}

Swap Stripe for PayPal now, and only StripePaymentGateway changes — OrderProcessor is entirely untouched (that's low coupling in action, the same benefit Lesson 081 showed from the dependency-inversion angle). Add a discount rule, and it belongs nowhere near this class at all (that's high cohesion — OrderProcessor's one job is orchestration, not pricing).

Analogy

An Office Building's Departments

High cohesion is a well-organized office where Accounting sits together, does accounting things, and every desk in that room genuinely does accounting work. You could describe the whole room's purpose in one sentence. Low cohesion is a room where three accountants, two customer-support reps, and a warehouse scheduler all happen to share a floor for no functional reason — asking "what does this room do?" doesn't have a clean answer.

Loose coupling is Accounting talking to IT Support through a help-desk ticket system — a stable, well-defined interface — rather than walking directly to a specific IT employee's desk and asking them, by name, to personally fix something in a way only they know how to do. If that one employee quits, the ticket system still works; any qualified IT person can pick up the ticket. Tight coupling is Accounting's whole workflow silently depending on one specific person's private notebook of workarounds — if they leave, the department is stuck.

A well-run building keeps departments cohesive internally and connects them externally through stable channels (tickets, memos, scheduled meetings) rather than ad hoc personal dependencies. That's exactly "high cohesion, low coupling," out of code.

Under the Hood — The Design Reasoning

WHY THESE TWO PROPERTIES, SPECIFICALLY, PREDICT MAINTAINABILITY
1. COUPLING PREDICTS "HOW FAR DOES A CHANGE SPREAD?"
  • Low coupling means the set of files you must touch (and re-test) for a given change stays small and localized. High coupling means it grows unpredictably — you can't know the full blast radius without tracing every dependency by hand.
▼
2. COHESION PREDICTS "HOW SAFE IS IT TO CHANGE THIS ONE FILE?"
  • High cohesion means an edit for reason X only ever touches code related to X — there's no risk of accidentally breaking unrelated behavior Y that happened to live in the same class. Low cohesion means every edit is a small gamble on the other, unrelated things sharing the file.
▼
3. TOGETHER, THEY BOUND THE COST OF CHANGE
  • Low coupling bounds how far a change spreads outward. High cohesion bounds how much unrelated risk a change carries inward. A system strong in both has changes that are small, local, and predictable — the same target every SOLID principle, and the whole rest of this Part, is aimed at.

Common Confusion

1. "Zero coupling is the goal" — no, zero coupling means zero collaboration

Classes must talk to each other to do anything useful — a system with literally zero coupling can't function. The goal isn't eliminating coupling, it's making it point at stable abstractions instead of volatile concrete details, so the coupling that must exist is cheap to live with.

2. "Fewer classes means higher cohesion" — often the opposite

Cramming unrelated behaviors into fewer, larger classes to "simplify" the file count is exactly how low cohesion happens. More classes, each with one honest purpose, is usually the higher-cohesion outcome — the earlier StoreUtils example split into three small, focused classes.

3. "Coupling and cohesion are two names for the same idea" — they're deliberately opposite lenses

Coupling looks outward — how much does this class depend on other classes' details? Cohesion looks inward — how well do the pieces inside this one class belong together? A class can be internally cohesive and still tightly coupled to the outside world, or vice versa — they're independent measurements, which is exactly why the goal names both: "high cohesion, low coupling."

Common Mistakes

Mistake 1 — Wrapping every class in an interface, even ones with exactly one implementation that will never change

Creating ITaxCalculator, ITaxCalculatorFactory, and a registration entry for a tax rule that is genuinely fixed by law and has one, and only ever will have one, implementation — pure ceremony bought against a coupling risk that doesn't actually exist.

Introduce the abstraction when there's a real reason to expect more than one implementation, or a real testing need to substitute a fake — not reflexively, for every class.

Mistake 2 — Treating "static class full of unrelated helper methods" as harmless because it's small

public static class Helpers
{
    public static string Slugify(string input) { /* ... */ return input; }
    public static bool IsBusinessDay(DateTime date) { /* ... */ return true; }
    public static decimal RoundToCurrency(decimal value) { /* ... */ return value; }
}

It's not the size that's the problem — it's that Helpers grows without bound because there's no cohesive theme to say "no" to the next unrelated method someone adds. This is a classic low-cohesion magnet.

Group by genuine purpose: a SlugGenerator, a BusinessCalendar, a CurrencyRounder — each small, each cohesive, each with a name that tells you exactly what belongs inside it and, just as importantly, what doesn't.

Mistake 3 — Assuming an interface automatically means loose coupling

public interface IStripeChargeCreator //  named after, and shaped exactly like, one vendor's SDK
{
    Charge CreateStripeCharge(ChargeCreateOptions stripeOptions); // still Stripe's own types leaking through
}

An interface whose method signatures are shaped entirely around one vendor's SDK types is coupling in disguise — swapping Stripe for PayPal still means rewriting every consumer, because the "abstraction" never actually abstracted anything.

Design the interface around your domain's need ("charge a customer this amount"), not around one vendor's API shape — that's what makes the abstraction genuinely swappable.

When Should I Use It?

Mental Model

Coupling = how much this class knows about that class.
Cohesion = how well the things inside this class belong together.

The goal: HIGH cohesion (belongs together, stays together) + LOW coupling (depends on the least, and only on the stable).

The tools you already have: SRP raises cohesion. DIP, ISP, and OCP lower coupling. SOLID isn't five separate ideas — it's five specific techniques aimed at these two properties.

Key Takeaway


Check Your Understanding

You've seen the vocabulary underneath every design principle so far. Let's check it actually reframes what you already know.

1. A class named InventoryManager both adjusts stock counts and formats weekly sales reports as PDFs, using two entirely separate sets of fields that never interact. What does this describe?

Show answer

Correct: B

Why B is correct: Cohesion measures whether the things inside a class genuinely belong together and serve one purpose. Two unrelated feature sets with two unrelated field groups, sharing a class only by coincidence, is the textbook definition of low cohesion — this is the same shape of problem as the lesson's StoreUtils and OrderProcessor examples.

Why A is incorrect: Being in the same class doesn't create cohesion — cohesion is about whether the contents genuinely relate, not about file boundaries.

Why C is incorrect: The scenario describes nothing about dependencies on other classes — coupling and cohesion are different axes, and this question is purely about internal relatedness.

Why D is incorrect: "Not interacting" internally describes low cohesion, not loose coupling — coupling is about a class's relationship to other classes, not to itself.

Reinforcement: Cohesion looks inward at one class's own contents; coupling looks outward at its relationships to others. Keep the two axes separate.

2. Which SOLID principle from Lesson 235 is, per this lesson, best understood as a direct technique for achieving low coupling?

Show answer

Correct: B

Why B is correct: This lesson explicitly maps DIP (Lesson 081) as the coupling-focused principle — depending on IPaymentGateway instead of StripeClient is exactly what keeps a change to Stripe's SDK from forcing a change to OrderProcessor.

Why A is incorrect: SRP is mapped in this lesson to cohesion, not coupling — it's about what belongs together inside one class, not about how that class relates to others.

Why C is incorrect: LSP concerns behavioral substitutability of subtypes, a different concern from how tightly two classes' implementations are bound together.

Why D is incorrect: Several SOLID principles connect directly to coupling and cohesion — that connection is this lesson's central point.

Reinforcement: When a principle's technique is "depend on an interface, not a concrete class," it's serving low coupling. When its technique is "keep related things together, separate unrelated things," it's serving high cohesion.

3. A team decides "zero coupling" is the goal and starts having every class communicate only through raw byte arrays passed through a generic message bus, with no shared interfaces or types anywhere in the codebase. What is wrong with this approach?

Show answer

Correct: C

Why C is correct: This lesson explicitly warns against treating zero coupling as the goal — classes must collaborate to do anything useful. Replacing typed interfaces with raw byte arrays doesn't remove coupling; it just makes the coupling implicit, untyped, and much easier to break silently, trading a clear contract for a fragile informal one.

Why A is incorrect: This directly contradicts the lesson's explicit correction of the "zero coupling" misconception.

Why B is incorrect: Coupling absolutely can be reduced — from concrete implementations to abstractions, for example — the lesson's whole "before/after" example demonstrates exactly that reduction.

Why D is incorrect: Cohesion is an entirely separate concern (what belongs together inside one class) and is unaffected by how classes communicate externally.

Reinforcement: The goal is coupling to something stable and clear — an interface with a well-defined contract — not the complete absence of any dependency between classes.

4. In the lesson's before/after OrderProcessor example, why does swapping Stripe for PayPal, after the refactor, require touching only one new class?

Show answer

Correct: B

Why B is correct: This is loose coupling in direct action — because OrderProcessor's dependency is the interface IPaymentGateway, not the concrete StripeClient, a new implementation can be introduced without any change to the consumer. That's precisely the payoff the "after" version was built to demonstrate.

Why A is incorrect: The example never relies on the two SDKs having matching shapes — the abstraction is what makes the swap possible, regardless of how different the underlying SDKs are.

Why C is incorrect: sealed prevents further inheritance from OrderProcessor itself; it has nothing to do with how OrderProcessor depends on its collaborators.

Why D is incorrect: OrderProcessor still has dependencies after the refactor — three interfaces, in fact — just abstracted ones instead of concrete ones. Loose coupling isn't the absence of dependencies; it's depending on stable abstractions.

Reinforcement: The test for loose coupling is exactly this: can a dependency's concrete implementation change (or be swapped entirely) without forcing an edit to its consumer?

You now have the vocabulary underneath every design principle in this Part — coupling and cohesion are the lens everything else gets built on.