Architecture Deep Dive · 2 of 3

Design Principles — SOLID and Friends

Principles aren't rules; they're heuristics that, applied with judgment, keep code changeable for years. Applied dogmatically, they generate the over-engineered abstractions they were meant to prevent. The trick is knowing when to reach for one and when to ignore it.

SOLIDDRYKISSYAGNICoupling
← Back to Architecture
Quick Facts

Why Principles Exist

Basic Concepts

  • Code is read more than written. Optimize for the next person — usually you, six months from now.
  • Change is the constant. Software lives by being modified. Principles aim at making that cheap.
  • Coupling vs cohesion: low coupling between modules, high cohesion within them. Most principles are restatements of this.
  • Heuristics, not laws. A principle that produces a worse design in this specific case is being misapplied.
  • Trade-offs are the work. "When does this rule stop applying?" is more useful than the rule.
SOLID

The Five Principles, One Letter at a Time

Coined by Robert C. Martin in the early 2000s. Originally aimed at OO design, but the underlying ideas — small responsibilities, stable abstractions, depend on interfaces — apply everywhere.

S — Single Responsibility Principle

"A class should have one reason to change." Not "do one thing" — that's too easy to misread. The real test: who would ask for this code to change? If billing and reporting both ask for changes to the same class, it has two responsibilities and should split.

Common smell: a 2,000-line UserService that handles auth, profile edits, email, and analytics. Split by axis of change.

Don't: shatter every class into single-method classes. Cohesion matters too.

O — Open/Closed Principle

"Open to extension, closed to modification." You should be able to add new behavior without editing tested, working code. The classic mechanism: polymorphism. New payment provider? Add a new PaymentProvider implementation; don't add a new else if in a switch statement.

Caveat: applied prematurely, this produces speculative interfaces with one implementation. Wait until the second variant exists before extracting the abstraction.

L — Liskov Substitution Principle

If S is a subtype of T, you should be able to use an S anywhere a T is expected without breaking the program. Subclasses must honor the contract of the parent — same preconditions or weaker, same postconditions or stronger, no surprise exceptions.

Classic violation: Square extends Rectangle. Setting width on a Rectangle leaves height alone; on a Square it changes both. Code that worked on Rectangle now misbehaves on Square. The "is-a" of math doesn't match the "is-a" of behavior.

I — Interface Segregation Principle

"Clients shouldn't be forced to depend on methods they don't use." Many small focused interfaces beat one fat interface. A Printer that needs only print() shouldn't have to depend on a MultiFunctionDevice interface with scan(), fax(), and staple().

In practice: if you find yourself stubbing methods with throw new NotSupportedException(), your interface is too wide.

D — Dependency Inversion Principle

High-level modules shouldn't depend on low-level modules; both should depend on abstractions. The order processor depends on an EmailSender interface, not the concrete SmtpClient. Wire the concrete one in at the edge (composition root, DI container).

This is what makes hexagonal architecture work: the domain depends on a port; an adapter implements it. Swap adapters without touching the domain.

Don't confuse with Dependency Injection — DI is one mechanism for achieving DIP, not the principle itself.

The Acronym Shelf

DRY, KISS, YAGNI, and Their Friends

DRY — Don't Repeat Yourself

"Every piece of knowledge must have a single, unambiguous, authoritative representation." It's about knowledge, not code shape. Two functions that happen to have similar code today but represent different business rules will diverge — merging them now creates a fake abstraction that fights every future change.

Rule of three: wait until you see the same idea three times before extracting. Two might be coincidence.

Counter-principle: WET — "Write Everything Twice." Sometimes duplication is cheaper than the wrong abstraction. "Duplication is far cheaper than the wrong abstraction." — Sandi Metz.

KISS — Keep It Simple, Stupid

The simplest design that solves today's problem usually wins. Cleverness costs the next reader minutes; clarity costs the writer minutes. Spend the cost on the side that happens once.

YAGNI — You Aren't Gonna Need It

Don't build what isn't asked for. Speculative generality (extra config knobs, plugin hooks, generic types nobody uses) almost always costs more than a future refactor would have. Build the concrete thing; generalize when a second case actually shows up.

Law of Demeter — "Only Talk to Your Friends"

An object should only call methods on itself, its parameters, objects it creates, or its direct components. order.customer.address.zip reaches through three objects — change any of them and the caller breaks. Prefer order.shippingZip().

Sometimes called "no train wrecks" because of the dot-chains.

Composition Over Inheritance

Inheritance binds you to a base class's whole shape, forever. Composition lets you assemble behavior from small pieces. Most "is-a" relationships are better modeled as "has-a" — a Robot has a WheelDrive, not is a WheeledVehicle.

Modern languages encourage this — Go has no inheritance at all; Rust uses traits; even Java/C# style guides now lean composition-first.

Tell, Don't Ask

Don't pull data out of an object to make a decision; tell the object to make the decision. Instead of if (account.balance > amount) account.debit(amount), write account.tryDebit(amount). Behavior lives with the data.

Principle of Least Astonishment

Code should do what a reasonable reader expects. A function called getUser() shouldn't also send an email. A method that says it returns a list shouldn't return null. Surprise is a bug in the API design.

Fail Fast

If something is wrong, crash at the boundary where it can be caught and diagnosed — not silently five layers later when the corrupted state finally explodes. Validate inputs, assert invariants, and let bugs be loud during development.

Cohesion & Coupling

The Underlying Game

Almost every principle on this page is a restatement of one rule: maximize cohesion, minimize coupling. A module is cohesive when its parts belong together (single responsibility). It's loosely coupled when changes elsewhere don't ripple through it (interface segregation, dependency inversion, Law of Demeter).

Types of Coupling, Worst to Best
  • Content coupling: module A reads/writes module B's internals. Worst.
  • Common coupling: shared global state. Hard to test, hard to reason about.
  • Control coupling: A passes a flag telling B how to behave. Smells like missing polymorphism.
  • Stamp coupling: A passes a big object when B only needs two fields.
  • Data coupling: A passes the exact data B needs. Acceptable.
  • Message coupling: A sends a message; B decides what to do. Best — used in events, actor models.
Connascence

A more precise vocabulary than "coupling." Two pieces of code are connascent if a change in one requires a change in the other. Connascence of name (rename a variable) is mild; connascence of position (argument order) is fragile; connascence of timing (B must run within 50ms of A) is brittle. Push connascence inward — keep tightly-connascent code close together; only weak forms cross module boundaries.

Architecture-Level

Beyond the Class

Separation of Concerns

Different concerns — UI, business logic, persistence, transport — live in different places. Lets you change one without disturbing the others. The macro version of single-responsibility.

Encapsulate What Varies

Identify the parts of your design likely to change and isolate them behind an interface. Stable parts depend on the interface; volatile parts hide behind it. Pricing rules, tax tables, third-party integrations are classic candidates.

Stable Abstractions Principle

The more a module is depended upon, the more stable (slow to change) it should be. A core domain module shared by ten services should be near-frozen; a leaf adapter can change every sprint. Volatile modules at the bottom of the dependency graph cause cascading breakage.

Architecture Decision Records (ADRs)

Short markdown files in the repo: title, context, decision, consequences. Capture why a choice was made — the constraints, the alternatives considered, the trade-offs accepted. Six months later, someone asks "why isn't this Postgres?" and the answer is committed history, not folklore.

Cautionary Notes

Where Principles Mislead

Premature Abstraction Is Worse Than Duplication

Extracting a "general" abstraction from one example almost always overfits to that example. The next caller has slightly different needs; you bolt parameters onto the abstraction; eventually it becomes harder to use than three direct copies would have been.

SOLID Doesn't Replace Domain Knowledge

You can write code that obeys every SOLID principle and still solves the wrong problem. Principles shape how you build something; they don't tell you what to build. Domain modeling and clear thinking come first.

Context Beats Dogma

A 50-line script doesn't need DIP. A throwaway prototype doesn't need ISP. A library used by thousands does. Apply principles in proportion to the change-cost they're meant to reduce.

Continue

More Architecture