Platform

Cache providers

Redis, in-memory, custom — all behind one canonical cache contract with singleflight fill, AEAD wrapping, and four-tier invalidation.

Vadyl ships a foundational caching plane that sits behind every entity read after access enforcement. The cache is post-enforcement (anti-pattern #60), publication-aware, scope-aware, and invalidated only after committed writes — never serves actor-variant data across actors, never serves stale data after a commit.

Bindings

Cache choice is project state — bound through CacheProviderBinding:

bindings: {
  cache: {
    type:       "redis",
    configRef:  "config:Vadyl:Redis:PrimaryConnection",
    isDefault:  true,
  },
},

configRef is a resolvable reference (config:or env:); raw connection strings are never persisted. Bindings inherit through the project hierarchy — closer ancestors win.

Built-in providers

  • Redis — production-grade, distributed, supports singleflight via SETNX leases, AEAD-wrapped payloads.
  • InMemory — process-local bounded LRU. Marked IsLocalOnly=true; bootstrap never selects it implicitly. Dev / test / explicit opt-in only.

Custom providers can be authored via the capability-surface system (built-in native, declarative bundle, or Wasm component). All flow through the same router and registry.

Singleflight fill

When two concurrent requests miss the cache for the same key, Vadyl elects one filler via the provider's lease primitive. Losers poll on capped exponential backoff (25ms→250ms, 5 attempts) and consume the winner's value. If the winner fails to publish within the window, losers fall back to their own fill to avoid starvation.

AEAD payload protection

Entities marked with cacheAllowProtected: true have cached payloads wrapped in CachedPayloadEnvelope via the project's key ring. Wrong AAD, wrong key version, or truncated envelope all fail-closed to CacheMiss — the original read path runs and re-fills.

Four-tier invalidation

Physical cache keys compose four generation tiers — platform, global, project, entity. Bumping any tier invalidates everything below. This gives you O(1) namespace invalidation without scanning keys:

  • platformInvalidateAllAsync bumps the platform-wide generation; affects every project.
  • global — per-provider global generation (optimization tier).
  • project — bumped by every committed write into the project.
  • entity — bumped by writes to a specific entity.

Distributed coherence

Cache invalidations broadcast via the canonical ICacheInvalidationBus (the same bus 13 other caches subscribe to). Peer instances see invalidations within one bus roundtrip — TTL is fallback only, never the primary coherence mechanism. Dormant providers catch up via durable per-project sequence counters when they re-materialize.

Inspecting cache decisions

vadyl explain read --entity Order --filter "id=order_123"
# Output includes:
#   cache.decision: hit | miss | bypass | refused
#   cache.key:      <physical key with all four generation tiers>
#   cache.provider: <provider alias>
#   cache.reason:   stable reason code from ReadPlanReasonCodes