I don’t meet this bar every day. I wrote it down because writing it down is how I hold myself to it. This series lays out the practices I try to work by as an engineer: patterns and anti-patterns across data modeling, delivery, code quality, code structure, architecture, and performance.


1. Make impossible states unrepresentable — and parse, don’t validate

This is the single highest-leverage type-level habit. Every time you reach for boolean, number, or string to represent a domain concept, ask: can this type contain values that are invalid in the domain?

  • Multiple booleans → discriminated union (a type that’s exactly one of several named variants at a time, each optionally carrying its own data — the TransferStatus example further down is one)
  • Raw IDs → branded types (an int64 wrapped in a distinct type per meaning, so AccountID and TransactionID can’t be swapped by accident even though both are just an int underneath)
  • Status codes → named enum or literal union
  • Nullable timestamp pairs (cancelledAt, completedAt) → state variants that carry their own timestamps

Alexis King articulated the deeper version of this in “Parse, Don’t Validate”. A validation function like isValidIBAN(s string) bool returns a boolean and discards the knowledge it just computed. The moment after you call it, the type system no longer knows you checked. Pass the string somewhere else two lines later, and nothing stops it from being invalid. You validated. You didn’t parse.

The fix is to change the return type so that success is the proof. TypeScript has no nominal typing, but a branded type does the same job: a private, unexported symbol field the constructor stamps on, so the only way to produce a value with that brand is to go through the function that validated it.

// Validation: throws away the knowledge
function isValidIBAN(s: string): boolean { /* ... */ }

function initiateWireTransfer(to: string): void { /* ... */ }

const iban = req.body.iban;
if (!isValidIBAN(iban)) {
  throw new Error("invalid IBAN");
}
initiateWireTransfer(iban); // string — compiler doesn't know it's valid

// Parsing: the type IS the proof
declare const ibanBrand: unique symbol;
type IBAN = string & { readonly [ibanBrand]: true };

function parseIBAN(s: string): IBAN {
  if (!ibanPattern.test(s)) {
    throw new Error(`invalid IBAN: ${s}`);
  }
  return s as IBAN;
}

function initiateWireTransfer(to: IBAN): void { /* ... */ }

const iban = ledger.parseIBAN(req.body.iban); // throws on invalid input
initiateWireTransfer(iban); // IBAN — compiler enforces it everywhere, forever

Look at initiateWireTransfer’s signature in each version: it went from taking a string to taking an IBAN. The compiler enforces, at every call site in every file, for the lifetime of the codebase, that you can only pass a value that went through ParseIBAN. You can’t pass a raw string by mistake. The invalid state isn’t representable anymore.

King’s heuristic for finding parsing opportunities: if you call a validation function and then keep using the original value, you validated when you should have parsed. Write the types first, and let the shape of the type guide the implementation. If a type forces you to handle a case you know can’t happen, the type is too weak. Strengthen it.

Comments and validation functions rot. A compiler-enforced type doesn’t: it holds in every caller, at every call site, forever. (The brand is a compile-time fiction, not a runtime seal — as IBAN is a plain type assertion, so anything in the same file, or anywhere someone’s willing to cast, can still manufacture one directly. The guarantee is “the compiler will stop you from doing this by accident,” not “it’s impossible,” which is a slightly looser boundary worth knowing about.)

This doesn’t mean the shape is frozen forever. Say the domain grows: you now need to know whether an IBAN cleared sanctions screening. Add it to the parser and the type, not to the call sites:

interface IBANShape {
  value: string;
  sanctionsCheckedAt?: Date;
}
type IBAN = IBANShape & { readonly [ibanBrand]: true };

function parseIBAN(s: string): IBAN {
  if (!ibanPattern.test(s)) {
    throw new Error(`invalid IBAN: ${s}`);
  }
  return { value: s } as IBAN;
}

Every existing caller of initiateWireTransfer(iban) still compiles, because iban still went through parseIBAN — the compiler doesn’t care that the shape underneath grew a field. The places that do care (anything now reading .sanctionsCheckedAt) are exactly the places TypeScript forces you to update, since the field is optional and any code branching on it has to handle the undefined case explicitly. Compare that to the raw-string version: nothing would have told you which of the fifty call sites needed to change. The type isn’t a straitjacket — it’s the one place the shape actually lives, so it’s the one place you have to edit when the shape legitimately changes.

// One rule eliminates four anti-patterns.
type TransferStatus =
  | { kind: "pending" }
  | { kind: "settled"; settledAt: Date }
  | { kind: "reversed"; reason: string; reversedAt: Date };

type TransferID = string & { readonly __brand: "TransferID" };
type CustomerID = string & { readonly __brand: "CustomerID" };

A switch over status.kind has to handle "pending", "settled", and "reversed" by name — there’s no field to typo, no null to forget. Make the default branch assign to a variable typed never, and the compiler fails the build the moment a fourth variant is added and a switch somewhere isn’t updated to handle it — the exhaustiveness check is a language guarantee here, not an opt-in linter.


2. Define done before starting; articulate trade-offs upfront

Most engineers do these two things once, badly, and then stop.

Define done concretely. “It works” isn’t a completion test. Write the specific, observable condition before you touch the keyboard, for features, refactors, performance work, bug fixes, all of it. Skip this and the work expands to fill whatever time exists.

Vague: "Add a fraud hold on large transfers."

Concrete: "A transfer over $10,000 to a payee the customer hasn't paid
before is held for manual review. The customer sees a 'pending review'
status within 2 seconds of submitting. A reviewer in the ops dashboard
approves or rejects within their queue; approval releases the transfer
within 5 seconds, rejection notifies the customer with a specific
reason, not a generic decline."

The first version has no edge in it — you can’t tell from reading it whether “done” includes the new-payee condition, the review-latency requirement, or what the customer actually sees while waiting. The second version is a checklist. Either the system does those specific things or it doesn’t, and there’s no argument to have about it.

State trade-offs before you build, not after. “This approach is faster, but it assumes X holds; if it doesn’t, we need Y” is a statement you make before writing the code. “If only we’d had more time” is the retrospective version, cover-your-ass engineering (CYAE) dressed up as regret. Say the constraint upfront and your manager has options: adjust scope, add time, accept the risk. Say it after delivery and all that’s left is damage control.

Do both and you avoid over-engineering, because you have a stopping condition, and under-engineering, because the trade-offs were never hidden from anyone.

None of this means the definition of done can’t change. Requirements shift mid-project all the time — the hold should also freeze the receiving account until reviewed, say, once someone points out that a fraud ring was cycling money through newly opened payee accounts. The discipline isn’t “never change the target.” It’s “never let it change silently.” When the completion test changes, that’s a visible, callable-out event: you rewrite it, you say out loud that the target moved and why, and everyone recalibrates against the new one. What you’re avoiding isn’t change — it’s the version where “done” quietly drifts because nobody ever wrote it down in the first place, so there was never a fixed thing to notice moving.


3. Deliver vertically, not horizontally

The most common delivery failure I’ve seen: build all of one layer before starting the next. Every database table, then every endpoint, then every screen. The cost stays hidden until the deadline, when integration reveals the data model was wrong, the API contract meant something different to the two people who built each side, or a requirement was ambiguous the whole time.

The fix: no layer counts as done until a user can do something end to end. Build that vertical slice first, before anything else on the feature.

The same slice breaks analysis paralysis. When a design won’t converge, stop discussing it and build the smallest slice that lets reality answer the open questions.

It also caps gold plating. If the completion test is “a user can do X,” and they can, the feature is done. Nothing left to argue about.


4. Build Plan B before Plan A

There’s an instinct to reach for the clever solution first. It signals confidence in a design meeting. It’s also the riskiest move you can make on a project with a deadline. Build the boring version first instead, as your primary deliverable, not a fallback.

Something running early gets you real feedback against real constraints instead of imagined ones. You find the actual hard parts instead of guessing at them. Often the boring version turns out to be fast enough, flexible enough, good enough on its own, and the clever version was never necessary.

A clever solution built after a working simple version answers to real constraints. A clever solution built instead of a simple version answers to whatever you imagined at the whiteboard.

Skipping Plan B is status signaling dressed up as confidence: it’s risk-taking with no hedge. Build Plan B and the schedule conversation changes shape. “We’re going to miss the deadline” becomes “Plan B already shipped, do we still want Plan A?”


5. Measure before optimizing, form hypotheses before debugging

Performance work and debugging fail the same way: you act without a model of what’s actually true.

For performance, profile first. Your intuition about the bottleneck is wrong more often than it’s right. The bottleneck is almost always database queries (especially N+1) or network round trips, and you won’t know which without data.

For debugging, form a falsifiable hypothesis before you read any code. State what must be true in the system for this behavior to occur, and what evidence would prove you wrong. That’s the discipline that stops you from closing an investigation on a fix that happened to work.

If you can’t explain why the fix works, it isn’t fixed.


6. Characterization tests before touching unfamiliar code

Before changing code you don’t own completely, capture what it currently does in tests. Not what it should do. What it does, including the weird edge cases that might be load-bearing.

// Not a spec — a snapshot that makes behavioral changes visible
test("formatCurrency(null) returns empty string", () => {
  // current behavior: null cents in, empty string out
  expect(formatCurrency(null)).toBe("");
});

Then change one thing at a time: structure changes in one commit, behavior changes in another. Reviewable diffs, a git blame that still means something, and a regression you’ll actually see when it happens.

Characterize first. Then change one dimension at a time.


The underlying unity

All six come back to one idea: close the gap between action and consequence.

Impossible states make a type error fail at compile time instead of runtime. Defining done sets the stopping condition before the work starts, not after. Vertical slices make integration problems surface in week one instead of week six. Plan B makes schedule risk visible before the deadline, not at it. Measuring before optimizing sends your effort to a real bottleneck instead of an imagined one. Characterization tests make a behavioral regression visible the moment it happens.

Every expensive mistake I’ve seen shares a shape: an assumption went false, and a long stretch of time passed before anyone found out. These six principles shorten that stretch.


The rest of the series

The rest of the series works through each of these in more depth, plus a set of named patterns and anti-patterns, grouped by where you actually run into them:

Patterns & anti-patterns, by category

By developer activity

The Highest-Leverage Work Is Never Feature Work. How to find and pitch the infrastructure improvements that multiply team effectiveness instead of just personal output.