skip to content

Your team wants Redis to hold an invariant that spans several keys — for example an index that must never point at a missing object, or a balance that must never go negative. Redis offers no rollback and no constraints. How do you decide whether to enforce that invariant in Redis, and what do you put in place if you do?

level: principalimportance: should knowfreq 30%

answer

  1. one command or one same-slot script = real enforcement
  2. hash tags co-locate; long scripts block
  3. cost of violation decides authority
  4. detect + reconcile + metric the repairs
  5. MULTI/EXEC gives isolation, not enforcement

basics

~20 s

Decide by blast radius: enforce it in Redis only when one atomic unit (a single command or one Lua script over co-located keys) can express the whole check-and-write. Otherwise keep the authoritative invariant in a store with constraints and treat Redis as a derived view with TTLs and reconciliation.

solid answer

~60 s

Three questions decide it. **Can one atomic unit express it?** A single command (`SET … NX`, `INCRBY` with a bounds check, `HSETNX`) or one Lua script over keys in the same slot executes with nothing interleaved, so the check and the write cannot separate. That is the strongest guarantee Redis offers, and it is real — but only within one node and only if the script itself is idempotent, because an error mid-script still leaves its earlier writes. **What does a violation cost?** A dangling feed entry is a nil render and a log line. A negative balance is money. Cost decides whether Redis may be authoritative at all: keep money-shaped invariants in a store that can abort a transaction, and let Redis cache the result. **Is the anomaly detectable and repairable?** If a `SCAN`-based sweep can find and fix violations, accept the anomaly window and reconcile. If it cannot even be detected, do not put the invariant there. Then add the mechanics: TTLs so orphans expire, benign write ordering, metrics on repairs so silent drift becomes visible.

go deeper

for a junior

Recognise the constraint: Redis has no rollback and no constraints, so multi-key rules must be maintained by application code.

for a middle

Show the practical tool — one command or one Lua script over co-located keys — and the ordering plus TTL habits for the cases it does not cover.

for a senior

Weigh enforcement against reconciliation, name the cluster hash-tag requirement and script-blocking cost, and specify the detection query and repair job.

for a principal

Make it a placement decision with explicit tiers per invariant, tie the tier to the cost of a violation, and account for the blast radius the enforcement mechanism itself introduces.

## Why this is a placement decision, not a coding one Relational engines let you declare an invariant (`UNIQUE`, `CHECK`, a foreign key) and abort any statement that would violate it. Redis has none of that and, deliberately, no rollback: once a write lands it stays. So an invariant spanning multiple keys is not something you *declare* in Redis — it is something the application maintains, or gives up on. The engineering question is therefore where the invariant lives, not how to trick Redis into enforcing it. ## The one strong tool: a single atomic unit Redis executes one command, or one script, without interleaving anything else. That means any invariant you can express *entirely inside* one unit is genuinely enforceable: - **One command.** `SET lock:x owner NX EX 30` makes "only one holder" true by construction. `HSETNX`, `ZADD … NX/GT`, `SETRANGE` and the `INCR` family cover more cases than people expect. - **One script.** A Lua script (or a Redis Function in 7.0+) reads the current values, decides, and writes — no other client sees or changes anything in between. "Debit only if the balance stays non-negative" is a five-line script and is genuinely safe on one node. The constraints on this tool are what you must be able to state: all keys must live on the same node (in a cluster, same hash slot — use a hash tag such as `{acct:42}`), the script must stay short because it occupies the execution thread, and the script has no rollback either, so it should perform its writes only after all its checks pass and be safe to run twice. ## When the invariant does not fit in one unit It does not fit when the keys cannot be co-located (they belong to different tenants, or co-locating them creates a hot slot), when the invariant also involves an external system (Redis plus the primary database), or when the work is too long to run on the execution thread. At that point you have three honest options: 1. **Move the authority elsewhere.** Let the relational store own the invariant, with a real constraint and a real abort, and let Redis hold a derived copy. Redis then only needs *eventual* agreement, which TTLs and re-population handle. This is the right answer for anything with money or legal meaning in it. 2. **Accept the anomaly and reconcile.** Choose the benign write order (payload before pointer), give orphan-prone keys a TTL so most violations self-heal, and run a periodic `SCAN`-based sweep that detects and repairs the rest. Instrument the sweep: the count of repairs per run is your drift signal, and a sudden rise is an incident, not noise. 3. **Weaken the invariant to something local.** Often the multi-key invariant is a proxy for a simpler one. "Index never points at a missing object" can be replaced by "readers tolerate a missing object and lazily drop the index entry" — self-healing reads are cheaper than global consistency and remove the invariant entirely. ## The decision framework to say out loud - **Expressibility:** can one command or one same-slot script do the check and the write together? - **Cost of violation:** cosmetic, recoverable, or unacceptable? Unacceptable means Redis is not the system of record. - **Detectability:** can I write a query that finds violations? If not, I cannot operate the invariant at all. - **Repair cost:** is a sweep cheap relative to the data volume, and does it run often enough that the anomaly window is inside the product's tolerance? - **Blast radius of the enforcement mechanism itself:** hash-tagging keys to co-locate them concentrates load; a long script blocks every other client. Enforcement has a performance price and sometimes that price is worse than the anomaly. ## Anti-patterns to name Wrapping the sequence in `MULTI`/`EXEC` and calling it enforced is the common mistake: EXEC gives isolation, so nobody sees a half state, but if the second command errors the first stays applied and the invariant is broken exactly as it would have been without the transaction. `WATCH` helps only when the invariant is *check-then-write on watched keys* — it aborts on conflict, it does not undo. Distributed locks across nodes do not restore atomicity either; they reduce concurrency without giving you recovery. ## What good looks like A reviewed design says: this invariant is enforced by one script over `{acct:42}`-tagged keys, tested for idempotency; that invariant is best-effort with a 24-hour TTL and a nightly sweep that emits a `redis_repairs_total` metric; and that third one lives in Postgres because a violation is a money bug. Being explicit about which tier each invariant sits in is more valuable than any individual technique.

  • How do you enforce 'balance never goes negative' with Redis primitives?
    A short Lua script over the account key: read the balance, compare against the requested debit, and either write the new value or return a rejection — all in one atomic unit so no other client can interleave. If the balance is authoritative for real money, keep it in a store with real transactions and let Redis hold only a cached view.
  • What breaks this approach when you move to Redis Cluster?
    A script may only touch keys in one hash slot, so the keys of the invariant must share a hash tag such as `{acct:42}`. That co-location is a design commitment: it fixes the invariant's members forever and can create a hot slot if the account is popular. Invariants that cannot be co-located must move out of Redis or become reconciled rather than enforced.
  • How do you know an eventually-consistent invariant is actually holding in production?
    Only by measuring it. The reconciliation sweep that repairs violations should also count them and export the count; a steady low number is your baseline and a jump is an incident. An invariant with no detection query is one you cannot claim to hold at all, which is itself an argument for moving it elsewhere.

saying these in an interview costs you the question

  • Claiming MULTI/EXEC enforces a multi-key invariant because it is 'a transaction'
  • Proposing a distributed lock as a substitute for atomic recovery
  • Putting money-shaped invariants in Redis because it is faster
  • Co-locating keys with hash tags without acknowledging the hot-slot cost
  • Designing a reconciliation sweep with no metric, so drift is invisible
  • Writing a long Lua script and ignoring that it blocks all other clients while it runs

context