Approach

Principles, and the code that proves them.

Most engineering-principles pages are assertions. These name the pattern the way the literature names it, then point at the thing in a production system that would fail if I stopped doing it — and say what each one costs. None of them belong to a language: I have written the same decisions in C#, Kotlin, Rust, Go and TypeScript over twenty-five years, and the languages turned over while these did not.

The shape of the code

Failure is a value, and the framework stays at the door.

Four decisions that determine what the rest of a codebase is allowed to look like — in any language, which is the point. They are cheap on day one and expensive to retrofit in year three, and that asymmetry is most of the reason to have opinions about them at all.

Errors as values

railway-oriented programming, after Scott Wlaschin

Expected failure is part of the return type, not an exception thrown past you.

Services return a result type rather than throwing for a state the caller should handle, and the shape reaches all the way down: the database layer translates constraint violations into typed errors instead of exceptions, and one place at the edge maps the error union onto status codes.

In other stacksRust ships it in the standard library, which is where most people meet it. Kotlin expresses it with a sealed hierarchy, C# with a result type or a discriminated union, Go with the error return it has always had, TypeScript with a tagged union. Only the spelling changes.

What it costsYou write mapping code a framework-native codebase gets for free. And transactions are the deliberate exception: inside one, a rollback still throws.

Functional core, imperative shell

Gary Bernhardt; ports and adapters, after Alistair Cockburn

The domain takes values and returns values. Everything that talks to the world stays at the edge.

Domain packages contain zero framework imports. When the APIs moved from one HTTP framework to another, the migration was a week of route files and not one line of domain logic. The same boundary is why the payment providers could be removed wholesale without changing settlement semantics.

In other stacksIt is the same rule Clean Architecture states for .NET, where the domain project references no web framework and the handlers sit outside it, and the same one hexagonal architecture states for the JVM. A dependency rule pointing inwards is not a language feature.

What it costsYou give up the ergonomic parts of modern frameworks — decorators that inject services into handlers, plugins that stash state on the request — or confine them to the edge where they cannot reach the domain.

Make illegal states unrepresentable

Yaron Minsky

The dangerous call should be a sentence you cannot write, not a bug you have to catch.

Whatever scopes a request — the tenant, the account, the authenticated actor — is a construction dependency, resolved once at the edge and closed over, never a parameter each method restates. There is then no argument through which the wrong scope can be passed, so the review question "did we remember the filter" stops existing.

In other stacksThe tools differ and the move does not: sealed interfaces and exhaustive `when` in Kotlin, enums with payloads and an unrepresentable invalid variant in Rust, records plus a closed hierarchy in modern C#, discriminated unions in TypeScript. Where a type system cannot express it, a constructor that refuses invalid input gets most of the way.

Parse, don’t validate

Alexis King

Untrusted input becomes a typed value once, at the boundary, or it does not get in.

Schemas at every entry point, and configuration behind a single validated door: the application refuses to start rather than discovering a missing variable at 3am under load. Derived schemas keep the second definition from existing at all — add a config key in one place and the API accepts it.

In other stacksZod in TypeScript, serde with typed structs in Rust, model binding plus validation attributes in .NET, kotlinx.serialization on the JVM. The principle is that the parsed value has a different type from the raw one, which every one of those can express.

How the rules survive

A rule you can break is not a rule.

This is the part I would want to be judged on. Every intention above is also a mechanism that fails loudly when someone violates it — a test, a constraint, a database policy, a trigger. Not a paragraph in a wiki that a deadline overrules. The mechanism is whatever the stack makes cheapest; that it exists is not negotiable.

Architectural fitness functions

Neal Ford, Building Evolutionary Architectures

The boundary is the test that fails when someone crosses it. Everything else is a note about one.

Tests named for the rule they hold: no HTTP framework inside the domain; every tenant-scoped table carries FORCE ROW LEVEL SECURITY; one service owns writes to the append-only ledger; no eager heavy imports; a test double that cannot drift from the client it stands in for. The RLS scan even models table renames, so it does not quietly rot into a green test that checks nothing.

In other stacksArchUnit on the JVM and NetArchTest or ArchUnitNET in .NET do this as a library; elsewhere it is a unit test that reads the source, the schema or the manifests and asserts. The mechanism is unglamorous on purpose — what matters is that it runs in CI, not what it is written in.

What it costsA fraction of a percent of the suite, and the discipline to write the guard in the same commit as the rule.

Put the invariant where it cannot be routed around

Isolation should hold on the day the application layer forgets.

The scoping predicate lives in the service and again underneath it, in the store: in PostgreSQL that is row-level security, applied per request, with a named system scope as the only way past it. Two independent layers, and the lower one does not depend on anybody remembering the upper one.

In other stacksThe database is the usual place because it is the last one every query passes through, but the shape is general: constraints and triggers over validation code, a gateway that authenticates before any service sees the call, filesystem permissions over a check in a script. Choose the layer that cannot be bypassed, not the most convenient one.

Crypto-shredding and blind indexes

Personal data is erasable by destroying a key, and still searchable while it exists.

Personal data is encrypted at rest under a key that belongs to whoever the data is about, not to the database, with the master key behind a swappable provider and keyed blind indexes so an encrypted column can still be looked up. A deletion request is then answered by destroying a key rather than by hunting rows across a schema and its backups.

What it costsBlind indexes buy exact-match lookup, not fuzzy search. Anything cleverer has to be designed for, not assumed.

Append-only ledger, single writer

The facts a system will later be asked to justify are recorded, not edited.

Every product has at least one of these — what was charged, what was granted, what an automated actor did on someone’s behalf. Each gets the same treatment: the table rejects updates at the SQL level, exactly one service owns the insert, and a static test stops a second service from learning to write to it. Current state is derived from the sequence rather than kept alongside it, which is what turns swapping the system that produced the events into a question about adapters rather than about history.

What it costsReads get more expensive, and a mistake is corrected by a compensating entry rather than by fixing the row — so the correction is visible, which is the point and also the argument you will have with whoever wanted it to disappear.

Where it meets the world

Other people’s systems will be down. Plan for that, not around it.

Integrations, infrastructure and the parts of a platform you do not control. The recurring decision here is to own the seam: keep foreign models out of the domain, and keep the operational surface small enough to actually run.

Anti-corruption layer

Eric Evans, Domain-Driven Design

A vendor’s model does not get to become your model.

Five external SaaS products behind a single REST API, so callers meet one interface and one vocabulary, and the domain never learns five. In a payments integration, the same treatment for the providers themselves — which is why two of them could later be removed outright.

Store-and-forward, backoff with jitter

the transactional outbox pattern

Write the intent down before you make the call, and the outage becomes a delay.

A dedicated service persists inbound requests ahead of the call to the third-party system that fulfils them and retries with backoff, so nothing is lost while that system is unavailable. The certificate solver takes the same care in a smaller place: request timeouts, retry with jitter, concurrent challenges serialised.

Boring infrastructure

choose boring technology, after Dan McKinley

Every dependency is an operational commitment. Most are not worth it.

A durable workflow engine that needs only the Postgres you already run: the queue is Postgres, the timers are Postgres, there is no broker, no control plane and nobody to sign a contract with. The same instinct further down — one database engine rather than a specialist store per use case, a cron entry where a minute of latency costs nothing, a container image plus a manifest rather than a platform. This site is static and loads exactly one script, a cookieless counter that stores nothing on the device, so there is still no consent banner and no session store to run.

What it costsBoring choices have ceilings, and picking one means agreeing to notice when you reach it: Postgres is a good queue at thousands of jobs a minute and the wrong one at millions. The commitment is to move deliberately at that point, not to pre-buy the ceiling on day one.

Programs as values

declarative over imperative

A workflow you can read before it runs is worth more than a trace of what it did.

The workflow engine’s DAG is a plain value: you can walk it, test it and show it before anything executes, where an imperative engine can only tell you afterwards. Idempotency keys, deadlines, retries and compensating steps are declared on it rather than coded around it.

What it costsA declared graph cannot express arbitrary control flow. Where the shape of the work genuinely is not knowable up front, an imperative engine is the better tool — and the project’s own docs say so.

GitOps and a signed supply chain

The deployed state is what the repository says, and the image can prove where it came from.

Declarative infrastructure with Flux, Terraform and encrypted secrets in git; a push produces an image tag and the image tag is the entire interface to the cluster. Published containers run distroless and non-root on a read-only filesystem, signed with SBOM and provenance, and rescanned weekly so a dependency that goes bad after release is caught rather than left to sit.

What gets written down

Documentation that admits what the thing is bad at.

The parts of the practice that are about the next person — including the version of me that comes back in two years having forgotten why.

Negative documentation

Every project says when to use something else.

Each open-source project ships a "when not to use this" section and a comparison table naming what it loses to the alternatives. Trust is built by being right about your own weaknesses, and it is much cheaper than being caught being wrong about them.

Intent-revealing commits and a work ledger

History should say what changed for the user, not which files moved.

Conventional prefixes with subjects written in behaviour — "notice when the model declines instead of failing" — and a documents folder where proposals move from open to done, so the reasoning behind a decision outlives the conversation that produced it.

Encoded procedures

A pattern used three times should generate code, not be described in a wiki.

The repeated shapes of the codebase — a CRUD route, a payment provider, a data backfill, an integration test — are written down as executable playbooks that a person or an agent follows. That is also what makes AI-assisted work safe at volume: the agent is handed the pattern, and the fitness tests catch it if it strays.

The through-line

Every architectural intention becomes a mechanism that fails loudly when it is violated — a type, a test, a database policy, a trigger. Errors become values, rules become tests, isolation becomes a policy, procedures become playbooks. It is the reason one person can carry a platform this size, and the reason AI agents can write a great deal of it without the quality going quietly soft.

Want this applied to your system?

A review is the fastest way to find out which of these your codebase already has, and which of the missing ones will hurt first.