Carina · the Keel
Navigating by Invariants
The first question we ask of any new system isn’t what should it do? It’s what must always be true?
Those two questions feel similar and aren’t. The first produces a feature list — a moving target that grows for the life of the product. The second produces invariants: statements the system promises to keep true no matter what happens to it. Features are what you build. Invariants are the keel underneath — the thing that keeps you upright when the weather turns.
What makes a good invariant
A good invariant is short, checkable, and load-bearing. “The system is fast” is none of those. “Every write is durable before we acknowledge it” is all three: it’s unambiguous, you can test it, and an enormous amount of design follows from it for free.
Here are a few we hold across our services:
- A request is either fully applied or not applied at all. No partial writes.
- Identifiers are opaque and never reused.
- A reader never observes a value that was never written.
Notice what these don’t mention: frameworks, languages, latency budgets. An invariant outlives all of those. We’ve rewritten the code under these promises twice. The promises didn’t move.
They turn decisions into deductions
This is the part that surprised us. Once the invariants are fixed, a startling number of “decisions” stop being decisions at all — they’re just consequences.
If every write must be durable before acknowledgement, then an in-memory queue with no fsync is not a trade-off. It’s a violation. The conversation ends.
We felt this most when adding caching. The team debated write-through versus write-back for an afternoon until someone restated the invariant: a reader never observes a value that was never written. Write-back caches can acknowledge a write that a crash then loses — so a subsequent reader could see a value the durable store never recorded. The invariant didn’t prefer write-through. It required it. The debate was already over; we just hadn’t noticed.
Encode them where they can’t be ignored
An invariant that lives only in a design doc is a wish. Push it down to the lowest layer that can enforce it, ideally where the type system or the database does the enforcing for you:
// The invariant "an order total is never negative" shouldn't be a code
// review comment. Make it unrepresentable.
public readonly record struct Money
{
public long Cents { get; }
public Money(long cents)
{
if (cents < 0)
throw new ArgumentOutOfRangeException(nameof(cents), "Money cannot be negative.");
Cents = cents;
}
}
Now no amount of downstream cleverness can construct a negative total. The invariant is enforced by construction, not by vigilance. Vigilance is a resource that runs out at 4pm on a Friday. Construction never gets tired.
The test
Before a system ships, we write down its invariants and try to violate each one on purpose — pull the plug mid-write, replay a stale message, hand it a malformed identifier. If the system holds, we have something we can build on for years. If it doesn’t, we’ve found the real design work, and we’ve found it now instead of at 3am during an incident.
Features tell you what a system does today. Invariants tell you what it will still be doing after you’ve forgotten why you built it. Choose them first, and choose them like you’ll have to live under them — because you will.