Catalogue / rules

Rules

Declarative predicates over a design. A rule reads a node's effective properties, its edges, its groups and the design's requirements. A finding never blocks a save; a structural problem always does. Open a rule for its rationale, the condition in plain English, and the reference designs that trip it.

Cache used as primary store without persistenceerrornode.kind = cache
cache-as-primary-store-without-persistence
Cache '{{node.id}}' is the primary store for its data but has persistence disabled.
This cache is the only copy of its data and has persistence turned off. A restart loses the data outright.
Synchronous call across cellserroredge
cross-cell-call
Synchronous edge '{{edge.from}}' -> '{{edge.to}}' crosses a cell boundary while the design requires cell isolation.
In a cell architecture each cell has to be able to fail on its own. A synchronous call that leaves the cell ties the two cells together and defeats the point of draining one.
Datastore without backup or retentionerrornode.kind = sql-database
datastore-missing-backup
Datastore '{{node.id}}' has no backup or no retention policy declared.
A system of record with backups off, or with no retention period stated, cannot be restored. The loss is discovered on the day it matters.
Single-region datastore behind a multi-region SLAerrornode.kind = sql-database
datastore-single-region
Datastore '{{node.id}}' is single-region with no replication, but the design requires multi-region availability.
The design promises multi-region availability, but this datastore sits in one region and does not replicate. A region outage takes the system of record with it.
Search index without an asynchronous feederrornode.kind = search-index
derived-store-without-cdc-or-rebuild
Search index '{{node.id}}' has no asynchronous feed from a stream, change data capture or worker. It cannot be rebuilt or kept in sync independently of the write path.
A search index is derived data and needs an asynchronous feed from a stream, change data capture or an indexing worker. Without one it is written inline on the request path, and it can never be rebuilt after it drifts.
Materialised cache without a rebuild patherrornode.kind = cache
materialised-cache-without-rebuild-path
Cache '{{node.id}}' holds materialised data but nothing durable or no worker feeds it. It cannot be rebuilt after loss.
A cache holding materialised views has to be rebuildable from a durable store or a worker. With nothing feeding it, a cold cache is a permanent gap rather than a slow start.
Money mutation without idempotency keyerroredge
money-mutation-without-idempotency-key
Edge '{{edge.from}}' -> '{{edge.to}}' moves money but does not declare idempotent, while the design requires exactly-once effect. Add an idempotency key to this hop.
A call that moves money is retried after a lost response. Without an idempotency key on that hop the retry becomes a second charge, which is exactly what the design's exactly-once-effect promise rules out.
Money store without double-entry ledgererrordesign
money-store-without-double-entry-ledger
Design '{{design.id}}' requires zero-sum money movement but has no ledger node. Record money in an append-only double-entry ledger.
The design promises that money movements sum to zero, but it has no ledger to enforce double entry. Balances kept in an ordinary table drift and cannot be audited.
Object store in fewer than three zones with a durability targeterrornode.kind = object-store
object-store-single-zone-with-durability-target
Object store '{{node.id}}' spans fewer than 3 zones (or declares none) while the design targets more than 10 nines of durability.
Eleven nines of durability assumes redundancy spread over at least three failure domains. An object store confined to fewer zones, or declaring none, cannot reach the target the design states.
Pub/Sub topic where replay is requirederrornode.kind = pubsub-topic
pubsub-replay-required
Topic '{{node.id}}' cannot replay messages, but the design requires replay. Use a stream or a replayable queue.
The design declares that consumers must be able to replay history. A fan-out topic that does not retain messages cannot deliver that.
Queue without dead-letter queueerrornode.kind = queue
queue-missing-dlq
Queue '{{node.id}}' receives async traffic but has no dead-letter queue.
A queue that receives asynchronous traffic and has no dead-letter queue either drops poison messages or retries them forever. Neither outcome leaves anything for a human to investigate.
Reconnect stampede without guarderrornode.kind = realtime-gateway
reconnect-stampede-without-guard
Realtime gateway '{{node.id}}' calls a coordination service synchronously on connect but has no reconnect stampede guard.
When a gateway holding millions of sockets restarts, every client reconnects at once and the gateway asks the coordination service about each of them. Without a stampede guard that lookup storm takes the coordination service down too.
Retry queue without dead-letter queueerrornode.kind = queue
retry-queue-without-dlq
Retry queue '{{node.id}}' has no dead-letter queue. Messages that keep failing must be parked for manual investigation.
A retry queue is where poison messages end up. With no dead-letter queue they are retried forever or dropped in silence, and nobody ever looks at the failed payment.
Retry without circuit breaker or load sheddingerroredge
retry-without-adaptive-shedding
Edge '{{edge.from}}' -> '{{edge.to}}' retries without a circuit breaker, and '{{edge.to}}' has no rate limiter in front of it. Retry storms will cascade.
Retries into a service that can neither shed load nor have its circuit opened produce a metastable failure: the retries keep the dependency saturated long after the original fault is gone. Either break the circuit at the caller or put an adaptive limiter in front of the callee.
Shared dependency without redundancyerrornode.kind = dns,coordination-service
shared-dependency-without-redundancy
Shared dependency '{{node.id}}' is not redundant, but the design requires redundant discovery.
The design requires redundant discovery, but this DNS or coordination service, which everything depends on, runs on a single provider or a single replica. Its failure is the whole system's failure.
Random channel placement under heavy-tail trafficerrornode.kind = realtime-gateway
shared-host-per-channel-without-isolation
Realtime gateway '{{node.id}}' assigns channels randomly while the design declares heavy-tail traffic. Hot channels will degrade unrelated tenants on the same host.
With heavy-tail traffic a few channels carry most of the load. Assigning channels at random lands those hot channels on arbitrary hosts, and every tenant sharing a host suffers for it.
Single-writer primary at peak QPSerrornode.kind = sql-database
single-writer-primary-at-peak-qps
Database '{{node.id}}' is a single unsharded writer declared at more than 5000 rps. Shard it or move to a multi-writer distributed store.
A single-writer, unsharded relational primary has a throughput ceiling. Declaring more than five thousand requests per second against it means the peak finds that ceiling in production.
Stateful engine pinned to a single zoneerrornode.kind = matching-engine,coordination-service,event-store
stateful-engine-pinned-to-single-zone
'{{node.id}}' is pinned to a single availability zone with no failover or replication.
A matching engine, coordination service or event store living in exactly one availability zone, with no failover and no replication, is lost with that zone. Everything that depends on it stops with it.
Stateful node without zone redundancyerrornode
stateful-node-without-zone-redundancy
Stateful node '{{node.id}}' lives in exactly one availability zone. A zone outage takes its state with it.
A stateful compute node pinned to exactly one availability zone disappears with that zone, and its state goes with it. Nodes inside a cell are exempt, because there the cell is deliberately single-zone and redundancy comes from the other cells.
Webhook without signature verification or idempotencyerrornode.kind = webhook-endpoint
webhook-without-signature-or-idempotency
Webhook '{{node.id}}' does not verify signatures or is not idempotent. Verify the caller's signature and deduplicate by event id.
A webhook is a public endpoint that its caller retries. Without signature verification anyone can post a fake event; without idempotency the provider's own retries apply the same event twice.