Coupling and cohesion are two ideas that show up everywhere in software design, and once you have them in your head you start seeing them in every codebase you touch. They sound abstract, but the everyday version is simple: how tangled-up are the different parts of your system, and how focused is each individual part?
Think of a well-organized kitchen. The drawers are cohesive — cutlery in one, baking tins in another — so you always know where a thing lives. And the drawers are loosely coupled — you can reorganize the cutlery drawer without touching the baking tins. Good code works the same way: each module has a clear job, and you can change one without disturbing the others.
Coupling: how much modules depend on each other
Coupling is the degree to which one module relies on the inner workings of another. Two modules are tightly coupled when one reaches deep into the other — using its private data, assuming how it stores things, or breaking the moment the other changes. They are loosely coupled when they talk only through a small, agreed-upon contract and otherwise mind their own business.
The goal is low coupling. When modules barely depend on each other, you can change, replace, or test one of them without dragging the rest of the system along for the ride. Low coupling is what lets a large codebase stay workable as it grows, because a change stays local instead of spreading.
Cohesion: how well a module's parts belong together
Cohesion is about what lives inside a single module. A module is highly cohesive when everything in it works toward one clear, single responsibility — an InvoiceFormatter that only formats invoices, for example. It has low cohesion when it's a grab-bag of unrelated things that happen to sit in the same file.
The goal here is high cohesion. When a module does one thing well, it's easy to name, easy to understand, easy to find, and easy to reuse. High cohesion and low coupling tend to reinforce each other: when each module owns one job cleanly, it naturally needs fewer tangled connections to everything else.
The problem: tight coupling
When modules are tightly coupled, they form a tangle of direct connections — each one reaching into the others, knowing their internals, and depending on exactly how they behave. The trouble is that a single change now ripples outward: fix a bug or rename a field in one module, and three others break because they were quietly relying on the old behavior.
This kind of code is also painful to test and to reuse. You can't exercise one module on its own because it drags half the system along with it, and you can't lift it into a new project because it's wired to neighbors it can't live without. The animation below shows this tangle: several modules with many connections crisscrossing between them, and a change in one node lighting up and rippling across all the others.
- ModuleA self-contained unit of code — a class, file, package, or service.
Loose, not zero. The aim is low coupling, never no coupling. If two modules never talked at all, your program wouldn't do anything — the parts have to cooperate somehow. Some coupling is necessary and healthy; the goal is to make it minimal, explicit, and routed through clean contracts rather than hidden dependencies on each other's internals.
The solution: loosen the connections
You loosen coupling by changing how modules talk to each other. Instead of reaching into a module's internals, you make each one expose a small, clear interface — a short list of operations it promises to support — and have callers go only through that. The module is free to change everything behind the interface, and nobody else notices.
Three habits do most of the work. Define clear interfaces so the contract between modules is explicit and narrow. Hide internals (encapsulation) so the messy details stay private and can't be depended on by accident. And depend on abstractions, not concrete implementations, so a module asks for some logger or some payment gateway rather than one specific class — a principle that powers both SOLID and dependency injection. The animation below shows the same modules from before, but now they communicate through one small, clean interface, with just a few connections instead of a tangled web.
- ModuleA self-contained unit of code — a class, file, package, or service.
- InterfaceA small, stable contract modules depend on instead of each other.
A quick gut-check. Ask, 'If I change this module, what else do I have to change?' If the honest answer is 'lots of things in lots of places,' coupling is too tight. Ask also, 'Could I describe this module's job in one short sentence?' If you need 'and… and… and,' cohesion is too low and the module is doing too much.
High cohesion vs the 'god module'
A highly cohesive module is the opposite of a junk drawer. Its functions all revolve around one purpose, share the same data, and read as a coherent story — a UserAuthenticator that signs people in and out, and nothing else. You can hand it to a teammate and they'll grasp it in a minute.
The failure mode is the god module (or god class): one giant blob that handles authentication and billing and email and report generation, all jammed together. It has low cohesion because its parts are unrelated, and it inevitably ends up tightly coupled to everything, because everything has to go through it. Splitting a god module into several focused, cohesive modules is one of the most common and most rewarding cleanups in everyday refactoring.
The rule to remember
If you take away one phrase, make it the classic one: low coupling, high cohesion. Keep the connections between modules few and clean, and keep the contents within each module tightly focused on a single job. Together they're the quiet foundation of code that's easy to change, easy to test, and easy to reuse.
Nearly every other design idea you'll meet is really a tool for serving these two goals. SOLID gives you principles for organizing responsibilities and dependencies, and dependency injection is a concrete technique for depending on abstractions instead of hard-wired implementations — both of them, at heart, just ways of keeping coupling low and cohesion high.