Field Guide

A field guide

The fundamentals you keep forgetting.

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.