skip to content

One service must run against two in-memory stores whose lifetime handling differs, one taking a deadline only at write. How do you design the expiry handling once?

level: principalimportance: should knowfreq 34%

answer

  1. intersection, not union
  2. attach-at-write is the floor
  3. capability check at startup
  4. duration or instant decides whose clock
  5. name what a deadline may not guard

basics

~20 s

Design from the intersection: a lifetime attached at write is the floor both stores share, and every other operation goes behind one narrow interface with per-store fallbacks. Decide once whether callers send a duration or a computed instant.

solid answer

~50 s

Start from the intersection, not the union. Attaching a lifetime as part of the write is the capability every store of this class offers; attaching one afterwards, reading the remaining lifetime, replacing a value while leaving the deadline standing, and clearing the lifetime are each present on some stores and absent on others. Put all of them behind one narrow interface whose contract is the intersection, and let each implementation decide how to honour the rest — a standalone attach where one exists, a rewrite where it does not, an explicit "unsupported" where neither is safe. Two decisions cannot be hidden: whether a deadline is sent as a duration the store counts down or as an instant a caller computed, which decides whose clock governs, and what may be entrusted to a deadline at all. Then publish the contract, including what an entry with no lifetime means.

go deeper

for a junior

The takeaway is narrower than the question: stores of this class differ in what they let you do with a deadline, and attaching one at write time is the part you can count on everywhere.

for a middle

Be able to list the capabilities that vary — attaching later, reading how long is left, keeping a deadline through a value replacement, clearing it — and name a fallback for each.

for a senior

Show the operational consequences: a fallback that costs a read plus a write, a diagnostic that cannot run on one store, and an approximation that is racy where a design depends on it.

for a principal

Own the contract — the intersection as the guarantee, a capability check that stops a bad configuration at startup, one decision on duration against computed instant, and a written rule for what may never rest on a deadline alone.

## Design from the intersection The instinct is to write for the richer store and shim the poorer one. That inverts the risk: every call site quietly assumes the richer posture, and the shim has to fake capabilities it cannot provide. Design instead from what both stores certainly do. | Capability | How common | Portable fallback when absent | |---|---|---| | Attach a lifetime as part of a write | Effectively universal in this class | — this is the floor | | Attach or replace a deadline on an existing entry | Present on some stores | Write the value again with a lifetime, paying a read and a race | | Read the remaining lifetime | Present on some stores | Carry the deadline inside the value, for reporting only | | Replace a value and leave the deadline standing | Present on some stores | Read the remaining lifetime, write with it — approximate and racy | | Clear the lifetime so an entry becomes permanent | Present on some stores | Write the value again with no lifetime | | A deadline below entry granularity | Rare | Give the part its own entry, or check it in the reader | The floor row is the contract. Everything below it is an implementation detail your interface may expose, but only with a stated fallback and a stated cost. ## One interface, two implementations A small surface is enough: write a value with a lifetime, attach a lifetime to an existing entry, read how long is left, extend a deadline, clear a lifetime. Each store implements it; each implementation is allowed to be slower, and none of them is allowed to be silently wrong. Three rules keep it honest: 1. **A fallback is visible in the signature or the metrics, never only in the code.** If "attach to an existing entry" costs a read plus a write on one store, that has to be measurable, because the load it adds is invisible at the call site. 2. **An unsupported operation fails loudly at startup, not at the first call.** Check the capabilities of the configured store when the service starts, and refuse to run a configuration whose fallback you decided was unacceptable. 3. **No caller branches on which store is configured.** The moment a call site asks, the abstraction has failed and both postures are now in the application. ## The two decisions that cannot be hidden **Duration or instant.** If a caller sends a duration, the store counts it down and the store's clock governs. If a caller sends an instant it computed, the caller's clock governs, and every application host's clock is now part of the correctness of your expiry. Not every store accepts both forms. Pick one for the whole service, prefer the form both stores share, and if the computed-instant form is necessary, say which clocks are in play in the design note rather than discovering it during an incident. **What may be entrusted to a deadline.** A deadline bounds how long an entry is served. It does not promise anything about when memory returns, it is not a durability mechanism, and it is not an erasure guarantee. So the rule to write down is about consequences: what is the worst outcome if this entry disappears one second after it was written, or survives an hour longer than intended? Anything whose answer is unacceptable does not belong behind a deadline alone, on either store. ## What degrades, and how you will notice - **The fallback path costs more than the native one.** A repair implemented as read-plus-write turns one round trip into two and puts the value on the wire twice; on the store with the native operation the same code path is cheap. Size the tier against the worse implementation. - **The remaining-lifetime read may not exist.** Any diagnostic, admin screen or health check that assumes it will work on one store and not the other. Build those on top of the interface, and let them report "not available here" rather than failing. - **Approximating a value replacement that keeps the deadline is racy.** Reading how long is left and writing with that number is two moments; the entry may have been given a new deadline in between. Where a design actually depends on the deadline surviving a value replacement, that dependency belongs in the capability check, not in a comment. ## The contract you publish Consuming teams need four sentences, not a document: every write carries a lifetime unless a named exception applies; an entry with no lifetime is a deliberate, reviewed decision with an owner; the deadline governs what is served and nothing else; and these operations may be slower on one configured store, with the fallback named. That contract is what makes a second store a configuration change rather than a migration, and it is the answer this question is really asking for.

  • What do you tell consuming teams about entries that carry no lifetime?
    That a permanent entry is a decision, not a default: it needs a named owner and a reason, because nothing will ever remove it on time-based grounds. Make the write path require a lifetime so the exception has to be asked for, and review the list of exceptions on the same cadence as any other standing grant.
  • Why check store capabilities at startup rather than handling the failure at the call site?
    Because a missing capability is a configuration error, and configuration errors should stop a deployment rather than surface as a scattered handful of failed operations under load. A startup check also documents the requirement in one place, where a reviewer can see which store postures the service has actually been designed for.
  • Is designing from the intersection not just losing the better store's features?
    Only if the interface hides them for good. The intersection sets the contract callers may rely on; an implementation is still free to use a native operation where it exists, and to be measurably faster for it. What you give up is call sites that silently assume a capability, which is the thing that makes the second store a migration.

saying these in an interview costs you the question

  • Designs for the richer store and shims the other one.
  • Lets call sites branch on which store is configured.
  • Hides a read-plus-write fallback behind an operation that looks cheap.
  • Sends a computed instant without deciding whose clock governs.
  • Treats a deadline as a durability or erasure guarantee.
  • Discovers a missing capability on the first call rather than at startup.