There is a category of requirement that sounds like a rule and is actually an invariant. “Expired accounts cannot write.” “Audit entries are never modified.” The difference between those two framings shows up six months in, when someone adds the ninetieth endpoint.
The version that depends on memory
The obvious implementation of an access rule is a check at the top of every handler that needs it. It works, it is explicit, and it is one forgotten line away from a paid feature being free.
Nobody skips it deliberately. They add an endpoint, they are thinking about the feature, and the check is simply not in their head at that moment. Multiply by a year and the rule is true in most places, which is another way of saying it is false.
The version that does not
Move the check into the request pipeline so it runs for everything by default, and opt individual routes out explicitly. Now a new endpoint is covered the moment it exists, and skipping the rule requires writing something down — which is exactly when someone might question it.
Ordering in that pipeline matters more than it looks. Cheap rejections belong first: rate limiting before any credential work, so a flood costs you nothing. Origin checks before authentication, so a cross-site write is refused before a token is even examined. Identity before anything that needs to know who is asking. Get the order wrong and every layer is still present and quietly less useful.
Push it further down when you can
Audit tables have a specific failure mode: they are trustworthy right up until someone runs an UPDATE. A code review convention does not stop a migration, a console session, or a well-meaning cleanup script.
CREATE RULE audit_no_update AS
ON UPDATE TO audit_log DO INSTEAD NOTHING;
CREATE RULE audit_no_delete AS
ON DELETE TO audit_log DO INSTEAD NOTHING;Now the guarantee is a property of the table. It holds for the ORM, for a psql session, for a migration, and for code written by someone who never read your convention. That is the difference between an invariant and a rule.
The general form
- 01Write the requirement as a sentence with no actor in it. “Audit entries are never modified”, not “developers should not modify audit entries”.
- 02Ask which layer can enforce it without anyone's cooperation — the database, the framework, the type system, the deployment.
- 03Push it down to that layer and accept whatever observability cost comes with it.
- 04Write the cost down next to the enforcement, so the next person finds an explanation instead of a mystery.