Beyond Vibe Coding Patterns for Building Real Software with Claude Code

Principles That Outlast the Tool

On this page

Durable engineering judgment the reader should carry — framed as opportunities, not catches. These principles are true regardless of who or what writes the code; they predate AI and outlast any one tool. This collection is the positive complement to anti-patterns-catalog.md: the pathologies catalog names a specific way Claude departs from good practice and gives the recovery move; this collection names what good looks like in the first place, so the reader has the judgment to direct the work toward it.

Where the two strands meet: a principle tells you what the right design is; a pathology is one named way the default drifts from it. Some principles have a sibling pathology (this first one is adjacent to Sidecar Proliferation #3 and Natural-Key Bias #13, which share its Single-Source-of-Truth root); many won’t. A principle does not require a Claude failure to earn its place here — the point is to teach the judgment, not to catch a mistake.


Entry template

Each principle uses this shape so the collection reads consistently and each entry can stand alone (postable online ahead of the book, like the patterns):

  • The principle — one-sentence statement of the rule.
  • In plain language — the same idea for a reader who doesn’t yet have the vocabulary.
  • Why it matters — the failure the principle prevents; the reasoning, not just the rule.
  • The opportunity with Claude — where this decision surfaces when working with AI, framed as a place to direct the design. Claude will build it either way; the judgment is the reader’s to supply, the labor is Claude’s to do.
  • How to do it — the concrete contract or checklist that puts the principle into practice.
  • The question to carry — the portable, one-line handle the reader takes to every future project.
  • Classical anchor — the established engineering concept it maps to.
  • Audience tagAnyone can apply this vs Needs some code-reading.

The Principles

1. The Source of Truth for Logic-Driving Values Lives in Code

Audience tag: Needs some code-reading.

The principle: For any field whose values are finite and drive branching in code — types, statuses, phases, kinds — the canonical definition belongs in code (a typed const / enum). A database table holding those values exists only to give the SQL engine what code can’t do inside the database: foreign-key enforcement, JOINs, a queryable list of valid values. The table mirrors the code; the code is never derived from the table.

In plain language: The “list of allowed values” should have exactly one true home. When those values decide what your program does, that home is the code. The database gets a copy so it can do database things — but the copy follows the code, never the other way around.

Why it matters: If the table were the source of truth, two failures open up. First, someone could add a row and create a “valid” value the code has no branch for — a value the database blesses but the program can’t handle. Second, the code’s logic and the database’s list can silently drift apart over time, and nothing forces them back into agreement. Keeping the code canonical means the values that drive behavior and the behavior itself live together and cannot disconnect.

The opportunity with Claude: Ask Claude to model a set of types or statuses and it will reasonably propose a lookup table with foreign keys — good, normalized schema. That proposal is not a mistake; it’s an opening. The opportunity is to direct the design: tell Claude the enum is canonical and the table mirrors it, and Claude will build exactly that — including the integrity check most developers would forget. The principle is yours to supply; the implementation is Claude’s to carry out. This is the book’s thesis in its constructive form: your judgment is the scarce input, not the typing.

How to do it — the mirror contract:

  1. The enum / typed const in code is canonical.
  2. The table is seeded to match the enum’s values (the seed is hand-maintained to align).
  3. Adding a value means change the enum first, then add a migration row. Never an INSERT alone as the act of authorizing a value.
  4. A startup integrity check hard-fails (not warns) on any mismatch between enum members and table rows.

The question to carry: “Do these values drive logic in code? If so, the code owns them — the table only mirrors them.”

Classical anchor: Single Source of Truth; reference-data / configuration-as-code; the model owns its invariants and the database is a projection of the model, not its master.


2. Durable Decisions Belong in the Durable, Shared Record

Audience tag: Anyone can apply this.

The principle: A decision that constrains future work — an architectural choice, a convention, a rule others must follow — belongs in a durable, version-controlled artifact that ships with the project and is visible to everyone who clones it. Not in a chat log, not in someone’s head, not in a tool’s private memory.

In plain language: If a choice will shape future work, or anyone who later reads the code needs to understand it, write it down somewhere permanent that travels with the project. A private note or a passing remark in a conversation doesn’t count — it has to live where the next person will actually look.

Why it matters: A decision recorded only in an ephemeral or private place is invisible to collaborators, gone when the session or machine is gone, and impossible to cite or revisit. A decision recorded in the repository is an artifact others can read, challenge, trace to its rationale, and build on. The record is what turns a choice into shared, durable knowledge instead of a fact that lives in one person’s memory until they forget it or leave. It also records the why, so a future reader can tell a deliberate constraint from an accident.

The opportunity with Claude: Claude will offer to “remember” things — and it has a real memory feature, so the offer sounds equivalent to recording. It isn’t: that memory is machine-local, per-user, ephemeral, and invisible to your collaborators. The opportunity is to route durable decisions to the right home. Tell Claude that anything constraining future code goes into a version-controlled artifact — a decision record, an ADR, CLAUDE.md, or project docs — and it will write it there, in the proper format, with the cross-links and rationale most people skip. Claude’s memory is a fine scratchpad for your working context; it is not the project’s record of record, and your judgment is what keeps the two from being confused. (Its sibling pathology is Memory Substitution, #16 in anti-patterns-catalog.md — the default Claude drifts toward when this judgment isn’t supplied.)

How to do it:

  1. Decision constrains future code or behavior, or anyone else needs to understand it → it goes in the repository.
  2. Use the project’s decision-record mechanism (an ADR file, a decision tool, or a decisions section in the docs); failing that, CLAUDE.md.
  3. Record the rationale, not just the ruling — the “why” is what makes the decision reviewable later.
  4. A tool’s private memory holds personal working context only. Never let “I saved it to memory” stand in for “it’s recorded.” And never let a constraint being “a separate concern” be a reason to leave part of a decision out of the record (that deflection is catalogued as pathology #17, Separate-Concern Deflection).

The question to carry: “If this decision matters to anyone but me — or to me a year from now — is it written where they’ll find it?”

Classical anchor: Architecture Decision Records (Michael Nygard); single source of truth for decisions; the repository as the system of record; avoiding tribal / institutional knowledge and the bus-factor risk it creates.


3. Dissolve the Problem Before You Solve It

Audience tag: Anyone can apply this.

The principle: Before choosing among solutions to a problem, test whether the problem is real. Many apparent constraints — ordering hazards, chicken-and-egg dependencies, “we’d have to handle X” — dissolve under a simpler move that makes the constraint not apply. The cheapest, most reliable mechanism is the one you never build, because the problem it would have solved no longer exists.

In plain language: When you’re handed two ways to do something hard, first ask whether the hard part is actually necessary. Often there’s a third move — reuse something you already have, or do nothing new — that makes the whole difficulty vanish. The best fix erases the problem instead of managing it.

Why it matters: Effort spent solving a problem that didn’t need solving is pure accidental complexity — machinery you now own, test, and maintain forever, all to service a constraint a one-line move would have erased. Worse, the elaborate solution often looks better than the simple one (it’s symmetric, it’s general, it’s clever), so it gets built and defended while the trivial alternative is never even named. Removing a problem is permanently cheaper than solving it: nothing to maintain, nothing to break, nothing to explain to the next person.

The opportunity with Claude: Claude frames hard decisions as a choice among solutions — Option 1 vs Option 2 — and argues each one thoroughly and well. What it rarely does unprompted is challenge the premise the options share, especially when that premise is a constraint Claude itself inferred during the analysis. (Its sibling pathology is The Invented Constraint, #21 in anti-patterns-catalog.md.) The opportunity is to supply the move Claude skips: before engaging its options, ask whether the constraint beneath them is real, and whether reusing something that already exists makes the whole choice moot. Claude will then happily build the simple thing — it just needed to be told the hard thing was unnecessary.

How to do it:

  1. When handed N options, find the constraint they all assume and state it out loud. Ask: is this a discovered fact, or an inference Claude made a moment ago?
  2. Ask the additive-bias antidote: “Is there a move that reuses what we already have and builds nothing new?” If that option isn’t on Claude’s list, put it there and make Claude argue against it.
  3. Distrust elegance offered as justification (“it’s the symmetric twin of X,” “the consistent finish to Y”). Symmetry is a reason to like a solution, not evidence it’s needed. Prefer least-work over most-beautiful when the two conflict.
  4. If a term is doing the persuading (“it self-hosts,” “it’s idempotent by design”), make Claude restate the solution in plain words. Obfuscation often hides over-engineering.

The question to carry: “Is this a problem at all — or does a simpler move make it disappear?”

Classical anchor: “The best part is no part” / design-for-simplicity (the cheapest, most reliable component is the one that isn’t there); essential vs. accidental complexity (Fred Brooks); the TRIZ ideal final result (the strongest solution removes the problem rather than solving it); false dilemma as the rhetorical failure the principle guards against.


4. Reconcile, Don’t Discard — Prove Subsumption Before Destroying Work

Audience tag: Needs some code-reading.

The principle: When two pieces of work overlap — a merge conflict, a branch that collides with something already landed, two implementations of “the same” thing — the default is to reconcile them (preserve every unique contribution from both sides), not to pick a survivor and delete the other. Discarding a side is permitted only after proving the survivor subsumes it: that everything the discarded side does, the kept side already does too. Until that proof exists, treat both sides as carrying intent worth keeping.

In plain language: When two versions of something clash, don’t just throw one away because they look alike. First make a list of what each one does, and check that the one you’re keeping covers everything the one you’re deleting did. Overlapping “mostly” is not the same as overlapping “entirely” — and the part that doesn’t overlap is exactly the part you’re about to lose forever.

Why it matters: Deletion is the one move you usually can’t take back. Most design mistakes are additive — an extra layer, a redundant file — and a reversible mistake caught late is just a cleanup. A discard is subtractive and irreversible: once the work is gone, the cost shows up only afterward, and recovering it may mean reconstructing decisions from memory or from some other artifact that happened to record them. Worse, a partial overlap is the most seductive trap: because part of the duplication is real, the verdict “it’s redundant” feels evidenced — and the unique remainder rides the discard down unnoticed. The proof-of-subsumption is cheap insurance against an expensive, silent loss.

The opportunity with Claude: Integration friction — rebase errors, a conflict, a freshly-landed feature that overlaps your branch — pushes Claude toward the cheapest resolution, and “these are duplicates, collapse one into the other” is often the cheapest: it ends the analysis. Claude will frame it confidently, as the decisive cleanup a good engineer would make (“competing implementation,” “unnecessary indirection,” “a ~2-line addition”). That confidence is the opening to insert one requirement: before anything is deleted, ask Claude to enumerate what each side uniquely contributes and prove the survivor covers it all. Claude is fully capable of producing that inventory — it just won’t volunteer it, because the inventory is the expensive step the discard was avoiding. (Its sibling pathology is The False Duplicate, #22 in anti-patterns-catalog.md.)

How to do it:

  1. When Claude proposes discarding a side as a duplicate, don’t evaluate the recommendation — demand the inventory: “List everything each side does. What’s only on the side you want to delete?”
  2. Require explicit subsumption: the survivor must cover every item on the discarded side’s list. Anything left over is functionality you’re about to lose — fold it in first.
  3. Distrust “overlaps almost entirely / competing implementation.” Make Claude name the non-overlapping remainder out loud; that remainder is the whole risk.
  4. Scale scrutiny to reversibility, not to Claude’s confidence. A git reset --hard, a branch -D, a deleted file, a dropped side of a conflict — all earn the proof. An additive change does not.
  5. If the work being discarded encoded a decision reached earlier, make sure the decision is recorded somewhere durable before the code carrying it disappears (see Principle #2).

The question to carry: “Before we delete this side — what does it do that the other one doesn’t? Prove nothing is lost.”

Classical anchor: Chesterton’s Fence (understand what something does before removing it); textual vs. semantic merge (a conflict marks overlapping edits, not equivalent intent); three-way reconciliation over winner-take-all; the asymmetry of reversible vs. irreversible decisions (a discard is a one-way door).


5. Supervise, Don’t Review — Judge by Outcome, Delegate Implementation-Review to Gates

Audience tag: Anyone can apply this.

The principle: Two distinct jobs hide under “checking Claude’s work.” Reviewing is reading the implementation line by line to confirm the code is right (the white-box, implementation-altitude job). Supervising is judging the work against its specification and observable outcome — did it do what was asked, and did it touch only what it should (the black-box, specification-altitude job). The human’s job is supervision; review belongs to gates — tests, type checks, linters, CI, an automated review pass. Define what “correct” means and verify the behavior; let machines verify each line against that definition.

In plain language: You don’t have to read the code to supervise the coder. Your job is to know what you asked for and check whether you got it — the way you’d judge a meal by tasting it, not by watching every knife stroke. The line-by-line checking is real work, but it’s machine work: you set up the automatic checkers once, and from then on they read the changes so you don’t.

Why it matters: Conflating the two jobs produces the two classic failures of working with AI. Treat review as your job and you drown — reading every diff doesn’t scale, isn’t where your judgment adds value, and burns you out until you give up. Give up, and you flip to the opposite failure: accept whatever runs and trust blindly, letting quiet pathologies accumulate into debt discovered far too late. The middle path — supervise the outcome rigorously, delegate the implementation-review to gates — is the only one that both scales and stays safe. It also puts the irreplaceable human act (holding the intent, defining “correct”) where no machine can take it, and offloads the mechanical act (verifying lines against that definition) to where machines excel.

The opportunity with Claude: The instinct, especially for a careful person, is to prove diligence by reading everything Claude produces. That instinct is the opening to redirect. Instead of reading the diff, invest the same energy in the gates that read it for you: ask Claude to write the failing test, the type, the lint rule, the CI check, the auditor that encodes your convention. Claude builds the very machinery that reviews Claude — and does it well, because the review criteria are explicit and mechanical. Your scarce input is the specification and the judgment of the outcome; the labor of both the code and its verification is Claude’s to carry. (This is the constructive face of Correct by Design: convert “check the change” into code, and there’s nothing left to check by hand.)

How to do it:

  1. State the specification and the acceptance criteria before the work — that’s the thing you’ll supervise against.
  2. Judge the result by behavior and footprint: does it do what I asked, and did it touch only what it should? Run it; check the outcome; scan git status for an unexpected footprint.
  3. For anything you’d otherwise verify by re-reading code, build a gate instead — a test that must fail, a type, a linter, an auditor, a review pass — and let it do the white-box reading.
  4. Reserve hand-reading of diffs for two cases only: deliberately building your own instinct (apprenticeship), or a last-resort diagnostic when the gates disagree and behavior is wrong.
  5. Never invert the split: defining what correct means is always yours; verifying each line against it is always delegable.

The question to carry: “Am I supervising the outcome, or drowning in the implementation? If I’m reading lines, where’s the gate that should be reading them for me?”

Classical anchor: Black-box vs. white-box verification (supervision is black-box, review is white-box); separation of the QA/verification function from authorship; specification-based acceptance testing; the manager-of-a-capable-team stance (judge deliverables against intent, don’t redo the work). Its narrative home is the “Engineer, Not the Programmer” chapter, where it’s the operational form of Claude is the programmer, you are the engineer.


6. Fit the Mechanism to Its Scope

Audience tag: Needs some code-reading.

The principle: Choose a mechanism by whether its lifetime and reach match the need it serves — not by whether some tool already understands it. A value that configures one call belongs on that call; a value that must govern a whole session belongs in session scope; state that is per-process must not be promoted to ambient, inherited policy just because that was the cheapest way to make it visible. The scope of a mechanism is a design decision in its own right, decided separately from whether the mechanism “works.”

In plain language: Match the size of the tool to the size of the job. If a setting only needs to affect one command, don’t make it affect everything for the rest of the session — even if the broader setting was the quickest thing to reach for. “It’s already understood here” is a reason a choice is easy, not a reason it’s right.

Why it matters: Over-scoping a mechanism buys a working result now and an invisible liability later. The wider scope touches things the decision never weighed — every other process, tool, or session that also reads that variable, holds that lock, or sees that global — and because nothing exercises those other paths at decision time, the choice looks correct. It keeps looking correct right up until something finally uses the scope you quietly claimed, at which point the failure surfaces far from the decision that caused it and wearing no obvious connection to it. The narrow, well-fitted mechanism cannot fail that way: there is nothing else inside its reach to break. This is the constructive form of blast-radius discipline — you never have to contain a blast radius you declined to create.

The opportunity with Claude: This decision surfaces constantly, and almost always silently. Ask Claude to route config, pass a flag, share state, or set an environment variable, and it will reach for whatever the surrounding tools already resolve — the path of least implementation. That reach is the opening. Name the scope you actually need — “this configures this invocation, nothing else” — and Claude will build the tightly-scoped version just as readily as the ambient one: a per-call argument instead of a session-wide export, a passed parameter instead of a global. Claude optimizes for what makes this work; you supply the axis it under-weighs — what else does this now touch, and for how long? The scoping judgment is yours to hold; the plumbing is Claude’s to lay.

How to do it — the scope-fit check:

  1. Before accepting a mechanism, state its intended reach in words: this must affect exactly ____, for exactly the duration ____.
  2. Compare that to the mechanism’s actual reach. An env var written to a session’s environment block reaches every child process for the session’s whole life; a global reaches the whole process; a per-call argument reaches one call. If the actual reach is wider than the intended reach, the fit is wrong — narrow it.
  3. Treat “a tool here already reads this” as an ease signal, never a correctness signal. It tells you the mechanism is cheap to wire up, not that its scope matches the need.
  4. Prefer the narrowest mechanism that satisfies the need: per-call over per-process, per-process over per-session, explicit-and-passed over ambient-and-inherited.
  5. When a wide scope is genuinely warranted, make it a named, deliberate choice — “this is session-wide because X depends on it” — not a side effect of picking the cheapest injection point.

The question to carry: “What is the smallest scope that does the job — and does this mechanism reach any wider than that?”

Classical anchor: Principle of least privilege / least scope; narrowest-possible variable scope (locals over globals); dependency injection and explicit parameter-passing over ambient global state; the distinction between per-process configuration and system-wide policy; separating a value’s use from its reach. Sibling pathology: adjacent to Dev Infrastructure vs. Product Code Conflation (#1) on the portability/environment axis, and the constructive complement to book-level theme 5 (reversibility / blast radius). Its worked failure is Case 8 (case-studies.md) — a session-wide XDG_CONFIG_HOME chosen because Python already resolved it, over-scoped in exactly the way this principle warns against.


7. Make the Wrong State Unrepresentable

Audience tag: Needs some code-reading.

The principle: When a rule must always hold, prefer a design in which violating it cannot be expressed over a design that permits the violation and then checks for it. Constrain the type, the schema, or the interface so the invalid state has no way to exist; fall back to a runtime check only where the constraint genuinely cannot be encoded — and make that check hard-fail.

In plain language: There are two ways to keep something from going wrong: watch for it and complain, or build things so it can’t happen in the first place. The second is stronger, because a watchman can be bypassed, forgotten, or installed everywhere except the one place that mattered. A container that can’t hold a bad value doesn’t need watching.

Why it matters: A check protects exactly the path it sits on. Every new caller, every new code path, every refactor is a fresh opportunity to route around it, and nothing announces when that happens — the failure mode of a missing check is silence. A constraint fails differently: it fails at the earliest possible moment (compile time, or the moment of the write), it fails for every path at once including the ones written years later by people who never read the rule, and it fails loudly because the invalid thing cannot even be constructed. The second payoff is subtler and larger: constrained values delete work downstream. Code holding a value that cannot be malformed does not re-validate it, so the belt-and-suspenders defensive checks that otherwise accumulate around every unconstrained value never get written at all. Validation sprawl is the compound interest on a rule you declined to encode.

The opportunity with Claude: Claude validates readily and structurally constrains rarely — it will reach for the guard clause because the guard clause is local, obvious, and always available. Ask it to “make sure the port is valid” and you get an if at the call site: correct, and reproduced by hand the next time anyone needs a port. That reflex is the opening. Name the rung you actually want — “make an invalid Port unconstructible”, “put NOT NULL and a CHECK in the schema, not a guard in the handler” — and Claude will build the structural version just as readily, including the parts people skip: the migration, the smart constructor, the error type, the tests that prove the bad value can’t be made. What Claude under-weighs is that a check is per-path while a constraint is per-program. That axis is yours to supply; the encoding labor is Claude’s to do.

How to do it — the constraint ladder: prefer the highest rung the situation allows, and know which rung you’re on.

  1. Impossible to express. The type or schema admits only valid values: a sum type with exactly the legal cases, a NOT NULL / CHECK / UNIQUE column, a required parameter rather than a nullable field someone must remember to fill.
  2. Impossible to construct. One validating entry point and no public way around it — a smart constructor, a private field, a factory that returns either a value or an error. The type’s mere existence is the proof of validity.
  3. Impossible to reach. Order operations so the invalid state has no window in which to be observed: initialize fully, then publish — never publish, then fill in.
  4. Checked once, at the boundary. For anything arriving from outside (user input, network, file, environment), validate on the way in and convert immediately into the constrained representation — which puts the rest of the program back on rungs 1–2. Parse it into the right shape; don’t pass raw data inward and re-inspect it at every stop.
  5. Runtime assertion, hard-failing. For what genuinely cannot be encoded. Fail loudly and early; never warn. Principle 1’s startup integrity check is exactly this rung, chosen deliberately because the constraint spans two systems that no single type can cover.

The question to carry: “Am I checking for this bad state, or have I made it impossible? If I’m checking — what would it take to move it up one rung?”

Classical anchor: “Make illegal states unrepresentable” (Yaron Minsky); correctness by construction; Design by Contract and class invariants (Meyer); type-driven design and “parse, don’t validate” (Alexis King); database integrity constraints as against application-level validation; poka-yoke (mistake-proofing) from manufacturing, where the fixture is shaped so the part cannot be inserted backwards. Its vocabulary sibling is the Invariant entry in terms-claude-likes.md: this principle is what makes an invariant genuine rather than merely declared — a rule you can violate by skipping a check is a convention, a rule you cannot express is an invariant. Constructive kin to Chapter 6, which runs the same move one level up: that chapter designs the non-deterministic step out of the runtime, this principle designs the invalid state out of the data.


Principles Still in Collection

Placeholder for principles identified in future sessions.


Found something wrong, unclear, or plainly disagreeable? Open an issue