Coupling and its Relation to the Single Responsibility Principle SRP
Coupling is the strength of the assumptions one module makes about another: every name a class references at compile time — a type, a method signature, a field, a constructor — is a wire soldered between them, and the more wires (and the more concrete the things on the other end), the more a change on one side forces a recompile, retest, or rewrite on the other. Reducing coupling is mechanically about cutting wires or replacing concrete wires with abstract ones so a change stays local.
This page fixes a common sloppiness: people call "injecting a DatabaseManager into Student" low coupling. It is better than hard-wiring the DB inside Student, but Student still imports and names the concrete DatabaseManager — that is only weak decoupling. The universal-remote analogy everyone draws actually requires an interface (the Dependency Inversion move), which the naive version never introduces.
Three levels of coupling, made precise
The word "coupling" hides at least three distinct degrees. Naming them stops the overstatement:
| Level | What Student depends on | Swap the DB without editing Student? | Test Student without a real DB? |
|---|---|---|---|
| L0 — Internal logic baked into Student.save() | JDBC, SQL strings, connection — all inside Student | No — edit Student | No |
| L1 — Concrete injected constructor takes DatabaseManager | the concrete class DatabaseManager (compile-time import) | No — Student still names the concrete type | Only if you can construct a real DatabaseManager |
| L2 — Abstraction injected constructor takes StudentRepository interface | an interface; the concrete impl is chosen elsewhere | Yes — pass any implementation | Yes — pass a fake/mock |
The original lesson moved L0 → L1 and called it the win. The real win — the one the universal remote depicts — is L1 → L2.
Why the naive version is wrong
Constructor injection of a concrete type is genuinely useful — it makes the dependency explicit and lets you reuse one DatabaseManager instance. But it does not deliver what the analogy promised. Concretely:
Student.javastill hasimport com.app.DatabaseManager;at the top. The source dependency arrow points fromStudentto the concrete class. ChangeDatabaseManager's package, rename it, or split it, andStudentrecompiles.- You cannot unit-test
Studentwithout a realDatabaseManager. A universal remote works with any TV precisely because it speaks to an interface (the IR protocol), not to one TV model. L1 is a remote hard-coded to one TV's part number that you happen to hand to the TV from outside. - It does not satisfy the Dependency Inversion Principle, which requires high-level policy (
Student) to depend on an abstraction, not a concretion.
The correct fix introduces an interface and inverts the arrow:
// The abstraction Student will depend on (the "IR protocol").
public interface StudentRepository {
void save(String studentId);
}
// One concrete implementation, chosen at the composition root, NOT by Student.
public class JdbcStudentRepository implements StudentRepository {
@Override
public void save(String studentId) {
System.out.println("Connecting to database...");
System.out.println("Saving student with ID: " + studentId);
// real JDBC here
}
}
// Student now names ONLY the interface — no import of any concrete DB class.
public class Student {
private String studentId;
private final StudentRepository repo;
public Student(StudentRepository repo) { // depends on abstraction
this.repo = repo;
}
public String getStudentId() { return studentId; }
public void setStudentId(String studentId) { this.studentId = studentId; }
public void save() {
repo.save(studentId); // delegates through the interface
}
}Now the only thing knitting Student to a database is a single method signature, save(String). Swapping Postgres for an in-memory store, or for a test fake, never touches Student.
Worked trace: a schema change ripples (or doesn't)
Suppose product asks: "store students in Postgres instead of the old MySQL helper, and add a tenantId column." Trace which files each design forces you to edit and recompile.
| Step / change | L0 (baked in) | L1 (concrete injected) | L2 (interface) |
|---|---|---|---|
| Rewrite the SQL / driver | edit Student.save() | edit DatabaseManager | edit JdbcStudentRepository |
Does Student recompile? | Yes — it holds the SQL | Yes — it imports the concrete class* | No — interface unchanged |
| Add a 2nd backend (e.g. cache) | fork Student logic | change Student's field type | add a new impl class only |
Unit-test save() with no DB | impossible | need real DatabaseManager | new Student(fakeRepo) |
| Files touched for the whole change | 1 (but high-risk) | 2 | 2 — and Student is not one of them |
*Even if the method signature is unchanged, Student is in the recompile set because the type it names was edited; in L2 the type it names (the interface) did not change, so it stays untouched and its tests stay green.
Coupling and SRP: two knobs, one habit
SRP says a class should have one reason to change. The mechanism linking it to coupling: each external concretion a class names is an extra reason to change. The L0 Student changes when student rules change and when DB rules change — two reasons, a direct SRP violation, caused by coupling DB code into it. Pulling DB logic out (any of L1/L2) restores one reason for Student. But only L2 also gives low coupling, because only L2 removes the concrete name. So: SRP is about how many reasons to change; coupling is about how far each change propagates. Splitting responsibilities fixes the first; depending on abstractions fixes the second. They reinforce each other but are not the same fix.
A second, larger instance makes the count concrete. A ReportGenerator that imports a PDF library, an email client, a DB handle, and the tax-rules module has four foreign concretions wired in — and at least three actors who can force an edit: a tax-law change, an email-template change, and a storage change all land in the same file. Counting the actors who can make you edit a class is the quickest SRP test in review: more than one and the class is a split candidate (here, into TaxCalculator, ReportRenderer, and Notifier, wired together by an application service). Notice the two knobs move together: each responsibility you extract also removes a foreign import, so the split lowers coupling at the same time.
Pitfalls
- Calling L1 "loose coupling". The headline error. Injecting a concrete type is explicit and reusable, but the compile-time arrow to the concretion remains. Look at the
importlines — if a concrete dependency is named there, you have not loosened that coupling. - Interface with exactly one impl, forever. Abstraction has a cost (an extra type, indirection, harder "jump to implementation"). If a dependency will only ever have one implementation and you never need to fake it in tests, an interface can be ceremony. Introduce it when a second impl or a test seam is real or near-certain — not reflexively.
- Leaky abstraction. A
StudentRepositorythat exposesjava.sql.ResultSetor throwsSQLExceptionhasn't decoupled anything — callers still depend on JDBC through the interface. The abstraction must speak the caller's language (domain types), not the DB's. - Moving the coupling, not removing it. Someone still has to choose
new JdbcStudentRepository(). Push that to a single composition root (main, a DI container, a factory). If every caller doesnew Student(new JdbcStudentRepository()), you've smeared the concrete dependency across the codebase instead of isolating it. - Mistaking afferent for efferent coupling. Being depended-on by many classes (high afferent coupling) is fine and often desirable for a stable core type; depending-on many volatile concretions (high efferent coupling) is the dangerous kind. Don't "reduce coupling" on a stable utility just because many things use it.
When to introduce the interface — and when not to
This is a decision, not a default. The signals that point toward L2 (interface + injection):
- You need to unit-test the high-level class without the real collaborator (DB, network, clock, payment gateway). A test seam is the single most common, most defensible reason.
- There is a real or imminent second implementation (Postgres + in-memory; SMS + email notifier; prod + sandbox payment).
- The collaborator is volatile — it changes for reasons unrelated to the caller (a vendor SDK, an external service).
Trade-offs vs. the alternatives:
- vs. L1 (inject the concrete class). L2 gains substitutability and a test seam; it costs one extra type, a layer of indirection, and a composition root that must wire impls. Choose L2 when you need to fake or swap the dependency; prefer L1 when the collaborator is a stable in-process helper with one implementation you control and never need to mock.
- vs. L0 (keep it in the class). L0 gains nothing but fewer files and is acceptable only for a tiny script or a true value object with no external I/O; it costs testability and SRP the moment real I/O appears. Choose L0 only for throwaway or genuinely self-contained logic; prefer L1/L2 the instant a class mixes domain data with I/O.
One-liner: choose the interface when you must substitute or test the dependency; prefer plain concrete injection when the collaborator is stable and singular; keep it inline only for trivial, I/O-free code.
Scenario: a 30-line CLI that prints one student to stdout — L0 or L1 is fine; an interface here is overengineering. The same Student in a service with a 200-test suite that must run without a database — L2 is mandatory, because the test seam alone pays for the extra type.
Takeaways
- Coupling = the assumptions one class bakes in about another; every concrete name in its
imports is a wire that propagates change. Loosening coupling means cutting wires or making them abstract. - Injecting a concrete
DatabaseManageris weak decoupling (L1), not low coupling —Studentstill names the concretion and still can't be tested without it. The universal-remote analogy needs an interface (L2 / Dependency Inversion). - SRP counts a class's reasons to change; coupling measures how far a change spreads. Splitting responsibilities fixes the first; depending on abstractions fixes the second.
- Abstraction isn't free — introduce the interface for a test seam or a real second implementation, not by reflex.
Sources: Robert C. Martin, Clean Architecture (2017) and Agile Software Development: Principles, Patterns, and Practices (SRP & DIP); Stevens, Myers & Constantine, "Structured Design" (IBM Systems Journal, 1974) for the original afferent/efferent coupling taxonomy; Martin Fowler, "Inversion of Control Containers and the Dependency Injection pattern" (2004) on composition roots; the Java code was compiled and traced against the worked example. Re-authored and deepened for this guide — the prior version mislabeled concrete constructor injection as "low coupling" and never introduced the interface its own analogy demanded; the decorative image was dropped for a mechanism diagram.
🤖 Don't fully get this? Learn it with Claude
Stuck on Coupling and its Relation to the Single Responsibility Principle SRP? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Coupling and its Relation to the Single Responsibility Principle SRP** (OO & Low-Level Design) and want to truly understand it. Explain Coupling and its Relation to the Single Responsibility Principle SRP from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Coupling and its Relation to the Single Responsibility Principle SRP** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Coupling and its Relation to the Single Responsibility Principle SRP** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Coupling and its Relation to the Single Responsibility Principle SRP** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.