cascache Provider-agnostic CAS like (Compare-And-Set or generation-guarded conditional set) cache with pluggable codecs and a pluggable generation store. Safe single-key reads (no stale values), optional bulk caching with read-side validation, and an opt‑in distributed mode for multi-replica deployments. Contents Overview CAS safety: Writers snapshot a per-key generation before the DB read. Cache writes commit only if the generation is unchanged. Writers snapshot a per-key before the DB read. Cache writes commit only if the generation is unchanged. Singles: Never return stale values; corrupt/type-mismatched entries self-heal. Never return stale values; corrupt/type-mismatched entries self-heal. Bulk: Cache a set-shaped result. On read, validate every member’s generation. Reject the bulk if any member is stale. Cache a set-shaped result. On read, validate every member’s generation. Reject the bulk if any member is stale. Composable: Plug any value provider (Ristretto/BigCache/Redis) and any codec (JSON/Msgpack/CBOR/Proto). Plug any value provider (Ristretto/BigCache/Redis) and any codec (JSON/Msgpack/CBOR/Proto). Distributed Keep local generations (default) or plug a shared GenStore (e.g., Redis) for cross-replica correctness and warm restarts. Read path (single) Get(k) │ provider.Get("single::"+k) ───► [wire.DecodeSingle] │ gen == currentGen("single::"+k) ? │ yes no ▼ ┌───────────────┐ codec.Decode(payload) │ Del(entry) │ │ │ return miss │ return v └───────────────┘ Write path (single, CAS) obs := SnapshotGen(k) // BEFORE DB read v := DB.Read(k) SetWithGen(k, v, obs) // write iff currentGen(k) == obs Bulk read validation GetBulk(keys) -> provider.Get("bulk::hash(sorted(keys))") Decode -> [(key,gen,payload)]*n for each item: gen == currentGen("single::"+key) ? if all valid -> decode all, return else -> drop bulk, fall back to singles Quick start import ( "context" "time" "github.com/unkn0wn-root/cascache" rp "github.com/unkn0wn-root/cascache/provider/ristretto" ) type User struct { ID , Name string } func buildCache () cascache. CAS [ User ] { // Value provider (in-process) rist , _ := rp . New (rp. Config { NumCounters : 1_000_000 , MaxCost : 64 << 20 , // 64 MiB BufferItems : 64 , Metrics : false , }) cc , _ := cascache. New [ User ](cascache. Options [ User ]{ Namespace : "user" , Provider : rist , Codec : cascache. JSONCodec [ User ]{}, DefaultTTL : 5 * time . Minute , BulkTTL : 5 * time . Minute , // GenStore: nil -> Local (default). See "Distributed generations". }) return cc } func readUser ( ctx context. Context , c cascache. CAS [ User ], id string ) ( User , bool ) { if u , ok , _ := c . Get ( ctx , id ); ok { return u , true } obs := c . SnapshotGen ( id ) // CAS snapshot BEFORE DB read u := loadFromDB ( id ) _ = c . SetWithGen ( ctx , id , u , obs , 0 ) return u , true } Alternative type name: You can use cascache.Cache[V] instead of cascache.CAS[V] . See Cache Type alias. Design Components Provider - byte store with TTLs: Ristretto/BigCache/Redis. - byte store with TTLs: Ristretto/BigCache/Redis. Codec[V] - serialize/deserialize your V to/from []byte . - serialize/deserialize your to/from . GenStore - per-key generation counter (Local by default; Redis available). Keys Single entry: single:: Bulk entry: bulk::> CAS model Per-key generation is monotonic . . Reads validate only; no write amplification. Mutations call Invalidate(key) → bump generation and delete the single entry. Wire format Small binary envelope before the codec payload. Big-endian integers. Magic "CASC" . Single +---------+---------+---------+---------------+---------------+-------------------+ | magic | version | kind | gen (u64) | vlen (u32) | payload (vlen) | | "CASC" | 0x01 | 0x01 | 8 bytes | 4 bytes | vlen bytes | +---------+---------+---------+---------------+---------------+-------------------+ Bulk +---------+---------+---------+------------------------+ | magic | version | kind | n (u32) | +---------+---------+---------+------------------------+ repeated n times: +----------------+-----------------+----------------+-------------------+------------------+ | keyLen (u16) | key (keyLen) | gen (u64) | vlen (u32) | payload (vlen) | +----------------+-----------------+----------------+-------------------+------------------+ Decoders are zero-copy for payloads and keys (one string alloc per bulk item). Providers type Provider interface { Get ( ctx context. Context , key string ) ([] byte , bool , error ) Set ( ctx context. Context , key string , value [] byte , cost int64 , ttl time. Duration ) ( bool , error ) Del ( ctx context. Context , key string ) error Close ( ctx context. Context ) error } Ristretto : in-process; per-entry TTL; cost-based eviction. : in-process; per-entry TTL; cost-based eviction. BigCache : in-process; global life window; per-entry TTL ignored. : in-process; global life window; per-entry TTL ignored. Redis: distributed (optional); per-entry TTL. Use any provider for values. Generations can be local or distributed independently. Codecs type Codec [ V any ] interface { Encode ( V ) ([] byte , error ) Decode ([] byte ) ( V , error ) } type JSONCodec [ V any ] struct {} You can drop in Msgpack/CBOR/Proto or decorators (compression/encryption). CAS is codec-agnostic. Distributed generations Local generations are correct for singles but bulk validation can be stale across replicas. Use a shared GenStore to eliminate this window and survive restarts. Important: LocalGenStore is single-process only. In multi-replica setups, both singles and bulks can be stale on nodes that haven’t observed the bump. Use a shared GenStore (e.g., Redis) for cross-replica correctness or run a single instance. import "github.com/redis/go-redis/v9" rdb := redis . NewClient ( & redis. Options { Addr : "127.0.0.1:6379" }) gs := cascache . NewRedisGenStore ( rdb , "user" ) // namespace should match Options.Namespace // or, with TTL: // gs := gen.NewRedisGenStoreWithTTL(rdb, "user", 90*24*time.Hour) // with TTL to prevent growth cache , _ := cascache. New [ User ](cascache. Options [ User ]{ Namespace : "user" , Provider : ristrettoProvider , // or Redis, BigCache Codec : cascache. JSONCodec [ User ]{}, GenStore : gs , // shared generations }) Behavior Singles: never stale (same as local). Bulks: validated against shared generations across replicas. Restarts: generations persist; valid entries remain valid. If you do not use a distributed GenStore in a multi-replica deployment, set Options.DisableBulk = true (or use a very short BulkTTL). Singles remain safe: they never return stale data. API type CAS [ V any ] interface { Enabled () bool Close (context. Context ) error // Single Get ( ctx context. Context , key string ) ( V , bool , error ) SetWithGen ( ctx context. Context , key string , value V , observedGen uint64 , ttl time. Duration ) error Invalidate ( ctx context. Context , key string ) error // Bulk GetBulk ( ctx context. Context , keys [] string ) ( map [ string ] V , [] string , error ) SetBulkWithGens ( ctx context. Context , items map [ string ] V , observedGens map [ string ] uint64 , ttl time. Duration ) error // Generations SnapshotGen ( key string ) uint64 SnapshotGens ( keys [] string ) map [ string ] uint64 } Cache Type Alias For readability, we provide a type alias: type Cache [ V any ] = CAS [ V ] You may use either name. They are identical types. Example: var a cascache. CAS [ User ] var b cascache. Cache [ User ] a = b // ok b = a // ok In examples we often use CAS to emphasize the CAS semantics, but Cache is equally valid and may read more naturally in your codebase. Performance notes Time: O(1) singles; O(n) bulk for n members. O(1) singles; O(n) bulk for n members. Allocations: zero-copy wire decode; one string alloc per bulk item. zero-copy wire decode; one alloc per bulk item. Ristretto cost hint: evict bulks first under pressure. ComputeSetCost: func ( key string , raw [] byte , isBulk bool , n int ) int64 { if isBulk { return int64 ( n ) } return 1 }