skip to content

Design Principles (Code Scale)

A link from architecture back down to the code-scale principles it rests on: SOLID, coupling and cohesion taxonomies, and information hiding. Read it to see how the same ideas repeat at each scale; the depth lives in the design-principles area.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

In software design, what do the terms "coupling" and "cohesion" mean, and why is the standard goal low coupling with high cohesion?

level: juniorimportance: must knowfreq 82%

answer

  1. Between modules = coupling; inside a module = cohesion
  2. Things that change together live together
  3. Bad boundary raises coupling AND lowers cohesion
  4. Depend on stable, not volatile; avoid cycles
  5. Indirection ≠ decoupling

basics

~20 s

Coupling is how much one module depends on another; cohesion is how well the things inside one module belong together. Low coupling means changing one module rarely forces changes elsewhere; high cohesion means each module does one clear job.

solid answer

~50 s

Coupling measures the strength of dependency **between** modules; cohesion measures the relatedness of elements **within** a module. They are the two halves of one question: where should the boundary go? Low coupling limits the blast radius of a change — if module A only knows a small, stable interface of B, B's internals can be rewritten without touching A. High cohesion means everything inside a module changes for the same reason, so a feature change lands in one place instead of being smeared across five. The two are linked: if you split a genuinely cohesive concept in half, you create a chatty, tightly coupled pair; if you merge unrelated concepts to "reduce" coupling, you get a low-cohesion blob that everyone must edit. The real target is not zero coupling (that's zero functionality) but *loose and intentional* coupling along boundaries that match how the system actually changes.

code

pseudocode · 12 lines
pseudocode
// Tight coupling + low cohesion: caller knows B's internals and B mixes concerns
report.rows = db.connection.rawQuery("SELECT ...")   // reaches through B
report.formatAsPdf()                                  // unrelated concern inside
report.emailTo("[email protected]")                    // and another

// Looser coupling + higher cohesion: narrow contract, one job per module
interface OrderQueries { fun monthlyTotals(month): List<Total> }

class MonthlyReport(private val queries: OrderQueries) {   // knows an abstraction
    fun build(month) = Report(queries.monthlyTotals(month)) // one job: build data
}
// rendering and delivery live in their own modules

go deeper

for a junior

Define both terms correctly and give one concrete example of each (a class that does one job; a class reaching into another class's fields). Say why it matters: changes stay local and code is easier to test.

for a middle

Connect the two: explain that a badly placed boundary raises coupling and lowers cohesion simultaneously, and use co-change in git history as evidence. Mention direction, stability, and cycles.

for a senior

Frame boundary placement as a change-cost optimization, name the taxonomies (content→data coupling, coincidental→functional cohesion), and note that indirection without variability is not decoupling. Give a refactor you actually did and its measured effect.

for a principal

Scale the argument: the same metric decides class boundaries, package boundaries, and service boundaries; team topology and release cadence are the real constraints. Discuss when you deliberately accept coupling (shared kernel, performance) and how you keep it explicit and reversible.

## The two words A **module** here is any unit with a boundary: a function, a class, a package, a library, a deployable service. The same two properties are evaluated at every scale, which is exactly why they are the bridge from code-level design to architecture. **Coupling** = the degree of interdependence *between* two modules. Concretely: how much does A need to know about B, and how likely is a change in B to force a change in A? Signals of tight coupling include reaching into another module's internal data, sharing mutable global state, depending on a concrete implementation rather than an abstraction, and depending on B's internal ordering or timing. **Cohesion** = the degree to which the elements *inside* one module belong together and serve one purpose. Signals of high cohesion: the functions operate on the same data, they are changed by the same kind of request, and you can name the module without using "and", "manager", "util", or "helper". ## Why the goals point in those directions 1. **Cost of change.** Most software cost is modification, not first authorship. Low coupling means a change stops at a boundary; high cohesion means a change is *localized* to one boundary rather than scattered. Together they minimize the number of places you must edit and re-verify per change. 2. **Comprehensibility.** You can reason about a loosely coupled module by reading it plus the interfaces it uses, instead of the whole system. 3. **Independent work.** Loose coupling lets two people (or two teams) work in parallel behind a stable contract. 4. **Testability.** A module with few, abstract dependencies can be exercised with simple substitutes; one wired to a database, clock, and network cannot. 5. **Reuse and replaceability.** A cohesive module with a narrow interface can be swapped or reused; a blob cannot. ## They are not independent knobs This is the part juniors usually miss. Coupling and cohesion are two views of the *same* boundary placement: - Split one cohesive responsibility across two modules → the two must now talk constantly about shared details → coupling goes **up** while cohesion goes **down**. (Classic symptom: two classes that are always edited in the same commit.) - Merge unrelated responsibilities into one module to remove a dependency → coupling between modules drops on paper, but the module now has low cohesion, and *everyone* has to touch it, so effective coupling across the team goes up. So the correct framing is: **choose boundaries so that things that change together live together, and things that change for different reasons are separated.** Coupling/cohesion are how you score a proposed boundary, not knobs to be turned independently. ## Common false targets - **"Minimum coupling" is not the goal.** A system with zero coupling does nothing. The goal is that each dependency is deliberate, narrow, one-directional where possible, and pointed at something more stable than the depender. - **Fewer files ≠ higher cohesion.** Cohesion is about *conceptual* relatedness, not physical proximity. - **An interface does not automatically decouple.** If the interface exposes the same internal details, or if there is exactly one implementation that both sides must agree on semantically, you have added indirection without removing dependency. Coupling is about *knowledge*, not about syntax. ## Direction and stability matter Two dependencies of equal "strength" are not equally bad. A dependency pointing at something **stable** (a well-defined, rarely-changing abstraction) costs little; a dependency pointing at something **volatile** (a specific vendor SDK, a UI layout, a wire format) costs a lot. And **cyclic** coupling (A → B → A) is worse than either direction alone, because neither module can be understood, tested, released, or replaced independently. Acyclic, one-directional dependencies pointed from volatile toward stable are the practical shape of "low coupling". ## How to spot it without theory - Look at commit history: files that always change together but live apart indicate a cohesion problem; files that must change whenever an unrelated feature ships indicate a coupling problem. - Try to describe the module in one sentence with no conjunctions. If you can't, cohesion is low. - Ask "what do I have to know about B to use it correctly?" The longer that list, the tighter the coupling — including things that aren't in the signature, like required call order or hidden shared state.

  • If low coupling is good, why not make every dependency go through an interface?
    Because indirection is not decoupling. An interface only helps when it hides something that can vary and is owned by the consumer's side of the boundary. Adding an interface with one implementation, that mirrors that implementation method-for-method, adds a file and a jump for the reader while leaving the same semantic dependency. Use abstraction where volatility or substitution actually exists.
  • Two classes are loosely coupled but you always edit both in the same pull request. What's the problem?
    That's a cohesion problem masquerading as good design: one responsibility has been split across two modules. Co-change in version control is the strongest empirical signal that a boundary is in the wrong place. Merge them, or move the shared reason-for-change into one of them.
  • Does high cohesion mean a module should be small?
    No. Cohesion is about singleness of purpose, not line count. A 500-line module implementing one algorithm can be highly cohesive; a 40-line 'Utils' class with four unrelated helpers is not.

Think of an office building. Cohesion is putting all of accounting on one floor instead of scattering accountants across five floors. Coupling is how often accounting must walk to legal to get anything done. You want each floor self-sufficient (high cohesion) and inter-floor trips rare and via a simple reception desk, not by wandering into people's drawers (low, narrow coupling).

saying these in an interview costs you the question

  • Saying the goal is "no coupling" — a system with no dependencies has no behavior.
  • Treating coupling and cohesion as independent dials rather than two scores of the same boundary placement.
  • Claiming that wrapping a class in an interface automatically reduces coupling, regardless of who owns the interface or whether anything varies.
  • Equating cohesion with file size or class length.
  • Ignoring dependency direction and cycles — A↔B is much worse than A→B even at equal "strength".
  • Using "Manager", "Helper", or "Utils" names and calling the result cohesive.

context

open as a page

What is the Dependency Inversion Principle (DIP), and how does a code-level rule about interfaces end up creating an architectural boundary?

level: middleimportance: must knowfreq 74%

basics

~20 s

DIP says high-level policy should not depend on low-level details — both should depend on an abstraction, and that abstraction should be owned by the policy side. In practice the business code declares the interface and the database or HTTP code implements it, so the dependency arrow points inward.

open as a page

What do the Liskov Substitution Principle and the Interface Segregation Principle actually require, and how do violations of each show up above the code level — in service contracts and public APIs?

level: seniorimportance: must knowfreq 58%

basics

~20 s

LSP: any implementation of a type must be usable wherever that type is expected, without callers needing to know which one they got. ISP: don't force a client to depend on methods it doesn't use — prefer several small, role-specific interfaces over one fat one.

open as a page

David Parnas's 1972 paper "On the Criteria To Be Used in Decomposing Systems into Modules" argues for information hiding. What criterion does it propose for drawing module boundaries, and how does that differ from encapsulation as a language feature?

level: middleimportance: should knowfreq 42%

basics

~20 s

Parnas says each module should hide one design decision that is likely to change — not represent one step of the processing flow. Encapsulation is the language mechanism (private fields, accessors) you might use; information hiding is the design decision about what to conceal and why.

open as a page

Structured design ranks coupling from content coupling down to data coupling, and cohesion from coincidental up to functional. Walk through those two scales and explain how you would actually use them when reviewing a module boundary.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Coupling, worst to best: content (reaching into another module's internals), common (shared global data), external, control (passing a flag that steers the callee's logic), stamp (passing a whole record when only a field is needed), data (passing just the needed values). Cohesion, worst to best: coincidental, logical, temporal, procedural, communicational, sequential, functional (one job).

open as a page

Meilir Page-Jones proposed "connascence" as a finer-grained replacement for the coupling/cohesion vocabulary. What is it, what are its three measures, and how does it guide refactoring decisions across a module boundary?

level: principalimportance: nice to knowfreq 16%

basics

~20 s

Connascence means two pieces of code are "born together": if one changes, the other must change to stay correct. It names the kind of agreement (name, type, position, meaning, algorithm, timing, order, identity) and rates it by strength, degree, and locality — giving concrete refactoring targets instead of a vague "too coupled".

open as a page