← Open in the full interactive course (progress tracking, search & more)

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

Loose Coupling

High Cohesion

Low Cohesion

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"
+
GOAL: LOW COUPLING — "depends on the least possible, and only on stable things"
RESULT

How It Works

DIAGNOSING COUPLING
1. ASK: WHAT DOES THIS CLASS DEPEND ON?
2. ASK: DOES IT REACH INTO INTERNAL STATE?
3. ASK: IF THE DEPENDENCY CHANGES INTERNALLY, DO I CHANGE TOO?
DIAGNOSING COHESION
1. CAN YOU NAME THIS CLASS'S ONE PURPOSE, HONESTLY, IN ONE SENTENCE?
2. DO ALL FIELDS GET USED BY MOST METHODS?
3. DO DIFFERENT FEATURE REQUESTS TOUCH DIFFERENT PARTS OF THE SAME CLASS?

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?"
2. COHESION PREDICTS "HOW SAFE IS IT TO CHANGE THIS ONE FILE?"
3. TOGETHER, THEY BOUND THE COST OF CHANGE

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.


dotnetmadeeasy.com — Learn C# and .NET, the right way.