Patterns and architectures get the spotlight, but they sit on top of
a smaller set of design principles that don't change much from
decade to decade. This page is the layer below — the things that
determine whether the patterns above succeed or rot.
10 principles·~12 min read·Updated 2026
How to read this guide
Each principle follows the same shape: the problem it answers, how
it shows up in the design, and the failure modes when it's
misapplied. The "When it fits" line at the end is the
scope where the principle earns its keep.
Knuth: "We should forget about small efficiencies, say
about 97% of the time: premature optimization is the root of all evil.
Yet we should not pass up our opportunities in that critical 3%." The
full quote licenses measured optimization; the tagline often cited in
isolation does not.
01
SOLID
Five object-oriented principles that age well.
Problem
Classes accumulate responsibilities, depend on concretions, and grow in ways that punish change. Every modification ripples further than it should.
Shape
Five principles, each addressing a different failure mode. SRP: a module should have one reason to change — gather what changes for the same reason, separate what changes for different reasons. OCP: open for extension, closed for modification. LSP (Liskov, 1987): a subtype must be substitutable for its supertype without breaking callers' expectations. ISP: clients should not depend on methods they don't use — prefer many small interfaces over one fat one. DIP: depend on abstractions, not concretions; both ends depend on the interface in the middle.
Watch for
SOLID becomes cargo culting if applied to procedural code or simple scripts — ceremony without value. Use it where mutation pressure on the design is real.
When it fits:
Long-lived OO codebases with multiple developers; library and
framework design where the API surface needs to outlive its first
use case.
02
Coupling & Cohesion
Loose coupling, strong cohesion — the central design metric.
Problem
Components that know too much about each other become impossible to change, test, or replace. Components that do unrelated work become impossible to understand.
Shape
Aim for low coupling (few cross-component dependencies) and high cohesion (each component does one related thing well). Boundaries align with axes of change.
Watch for
Pure decoupling can mean over-abstracted indirection. Pure cohesion can mean monoliths within. The right balance shifts with the system's age.
When it fits:
Every design decision. The first thing to ask when something feels
hard to change.
03
Separation of Concerns
Each module owns one concern; concerns don't bleed between modules.
Problem
When business logic, persistence, and presentation tangle together, every change touches every layer. Testing requires the whole stack stood up.
Shape
Identify orthogonal concerns (business rules, persistence, transport, presentation) and give each its own module/layer. Cross-cutting concerns (logging, auth) live in one place.
Watch for
Over-separation creates ceremony — five files to add a field. The separation only earns its keep when concerns actually evolve independently.
When it fits:
Anywhere the same code is changed for multiple unrelated reasons.
The smell is "I changed X and now Y broke."
04
Idempotency
Repeated application leaves the system in the same state as a single application.
Problem
Network calls fail in ways that leave the caller unsure whether the operation succeeded. Retrying may double-charge, double-send, or double-create.
Shape
Idempotency is a property of the operation, not the response: DELETE /resource/42 is idempotent even though the second call returns 404. Build it in by making the side effect a no-op when the desired state is already reached, or by using an idempotency key — a client-provided token; the server records the outcome under the key and returns the stored outcome on retries instead of re-executing.
Watch for
Idempotency keys must outlive retries — short TTLs reintroduce the bug. Side effects in dependencies (emails, charges) must respect the same key.
When it fits:
Any write that crosses a network. Mandatory for payments, signups,
anything where double-execution costs real money or user trust.
05
API Design
Versioning, pagination, idempotency keys, and predictable error contracts.
Problem
APIs evolve faster than their consumers can update. Without discipline, a single breaking change can stall a hundred clients.
Shape
Version explicitly (URL or header). For pagination, prefer cursors when datasets are large or mutate during paging; offsets are fine for small, stable, jump-to-page UIs. Use idempotency keys for writes. Define error responses as first-class as success ones — same shape, machine-readable codes.
Watch for
Versioning everything from day one is over-engineering. Versioning nothing is undermining tomorrow. Pick the boundary where compatibility actually matters.
When it fits:
Any API consumed by code you don't control — public APIs, partner
APIs, the API behind your own mobile app.
06
Error Handling
Exceptions vs results; fail fast vs fail safe.
Problem
Errors are an afterthought, leading to swallowed exceptions, silent corruption, or panic-on-anything code that crashes the wrong layer.
Shape
Distinguish recoverable failures (return as values/results) from programming errors (throw, crash). Decide the layer where each error type is handled. Never swallow what you can't recover from.
Watch for
Errors-as-values is verbose without good ergonomics. Exceptions silently flow through code that doesn't expect them. Mixing both is the worst of all worlds.
When it fits:
Every codebase. The error model should be a deliberate design
choice, not a side effect of which language you picked.
07
Domain-Driven Design
Model the domain explicitly; align code structure with business reality.
Problem
Code structured around technical concerns (controllers, services, DAOs) hides the business domain. Adding a new business concept means scattering changes across layers.
Shape
Identify bounded contexts — boundaries inside which a term has one unambiguous meaning. Use aggregates as the unit of transactional consistency (one aggregate, one transaction). Speak the domain's vocabulary in code. Push business rules into entities and value objects, not service methods.
Watch for
Full DDD is heavyweight — events, repositories, factories. CRUD apps don't need it. Apply it where business rules are complex enough to be the dominant source of bugs.
When it fits:
Domain-heavy systems with non-trivial business rules — finance,
healthcare, logistics, anywhere the business model itself is complex.
08
Testing Strategy
Pyramid (unit / integration / end-to-end) and the right kind of test doubles.
Problem
All-unit-test suites pass while integration breaks. All-end-to-end suites are slow and flaky. Inverted pyramids burn time and erode confidence.
Shape
Many fast unit tests (logic), fewer integration tests (boundaries, persistence, contracts), few end-to-end tests (user-visible flows). Use mocks deliberately: a mock asserts that a call happened (interaction-based testing), a stub returns a canned value (state-based testing). Mock-heavy suites tend to over-couple to internal call sequences — they confirm yesterday's wiring, not behavior.
Watch for
Mocking what you don't own creates tests that pass while reality breaks. Coverage % is a noisy proxy — chase the dangerous code paths, not the easy ones. The pyramid is one shape; the "testing trophy" (heavier on integration than unit) is a credible alternative for UI-heavy apps where most bugs live at boundaries.
When it fits:
Every codebase. The shape of your test suite is the shape of your
confidence in changes.
09
Naming & Modularity
The cheapest, most underrated lever in the design toolkit.
Problem
Bad names mislead more than no names. Unclear modules force readers to memorize implementation to understand intent.
Shape
Name things for what they mean in the domain, not how they're implemented. Modules expose intent through their public surface and hide everything else. Renaming is a first-class refactor, not a cosmetic one.
Watch for
Premature naming locks in the wrong abstraction. Don't name something Manager, Helper, Service, or Util unless you've thought hard about why no better name exists.
When it fits:
Every line of code. The half-life of a name in a long-lived
codebase is years; the cost of getting it wrong compounds.
10
Technical Debt
Recognize it, manage it, pay it down deliberately.
Problem
Shortcuts taken under deadline pressure accumulate. Eventually the cost of every change exceeds the cost of building the feature itself.
Shape
Fowler's quadrant: deliberate / prudent ("we ship now and refactor"), deliberate / reckless ("we don't have time for design"), inadvertent / prudent ("now we know how we should have done it"), inadvertent / reckless ("what's layering?"). Track it. Pay down the high-interest debt first — the parts that slow every change.
Watch for
Not all debt is worth paying. Stable, working code that's ugly is fine. The debt that matters is the debt that compounds with every new feature.
When it fits:
Continuously. Every team has debt; the difference between teams is
whether they manage it or pretend it doesn't exist.
Principles compound
These ten things sound obvious, and they are — until you skip one
and find out which one. The compounding effect of getting them
right shows up not in any single feature, but in how cheaply the
tenth feature ships compared to the first.
Architecture and patterns are how the system is shaped. Principles
are how the shape stays shapely as the system grows.