cache
import "github.com/kitsunium/sdk/pkg/v1/data/cache"
Package cache is the public facade for the SDK's caching, and it offers two surfaces because there are two different jobs.
The primitive: Cache[K,V]
Cache is a generic LRU + TTL map — construct it, Set, Fetch, done. No port, no error surface, no vocabulary. Reach for it when you want a bounded map with expiry inside one component.
c := cache.New[string, int](cache.Config[string, int]{MaxEntries: 1024, DefaultTTL: time.Minute})
c.Set("answer", 42)
v, ok := c.Fetch("answer") // 42, true
The domain: Store[V]
Store is the cache as an INTERFACE, with the three things a primitive deliberately does not have: invalidation by tag, protection against a stampede of concurrent misses, and the ability to put one store in front of another. Reach for it when a cache is part of your architecture rather than an implementation detail — when something else has to be able to invalidate what you cached, or when the thing behind the cache must not be hit N times the instant a hot key expires.
store, err := cache.NewMemory[Profile](cache.MemoryConfig{
MaxEntries: 10_000, // refused if zero — a capacity is your decision
DefaultTTL: time.Minute,
})
loader, _ := store.(cache.Loader[Profile])
profile, err := loader.Load(ctx, key, func(ctx context.Context) (cache.Entry[Profile], error) {
p, err := db.Profile(ctx, id)
return cache.Entry[Profile]{Value: p, TTL: time.Minute, Tags: []string{"user:" + id}}, err
})
tagger, _ := store.(cache.Tagger)
removed, err := tagger.InvalidateTag(ctx, "user:"+id) // every entry derived from that user
The two do not compete: the domain store is BUILT on the primitive, and Cache stays exactly what it was.
Fetch, not Get — on both surfaces
A hit promotes the entry in the recency order and moves the counters, so a read MUTATES. Naming it Get would promise a purity neither surface offers, and in the domain store it takes a write lock for precisely that reason.
Stampede protection stops at the process boundary
Loader.Load guarantees ONE fill per key across every goroutine in THIS process. It does not coordinate across replicas. Ten instances of a service each running this still send ten concurrent requests to the origin when a hot key expires — so a fleet's worst-case origin load falls by the concurrency factor within an instance, not to one. Cross-process collapsing needs a shared lease, which is a distributed system and not something a cache can promise. Size the origin against the number of replicas, not against the number of requests.
Nothing across tiers is atomic
NewChain is two independent stores, and there is no transaction between them. Every multi-tier operation has a window in which they disagree; the package chooses which side of that window is safer and says so, rather than implying a consistency it cannot deliver. See internal/service/data/cache's CLAUDE.md for each choice and its reason.
Index
Constants
const NoExpiry time.Duration = -1
NoExpiry is the Entry.TTL that stores an entry with no deadline.
It is deliberately distinct from the zero TTL, which means "use the store's default". A store with a one-minute default holding one entry that must never expire is a real combination, and reading both meanings out of 0 would make the second one unsayable.
Initialised with github.com/kitsunium/sdk/internal/core/data/cache.NoExpiry.
Variables
var BackendFailed *errs.Error
BackendFailed is returned when a store could not serve an operation. A MISS is not this: a miss is (zero, false, nil).
Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheBackendFailed.
Code 0.2.18.2 · reason CACHE_BACKEND_FAILED · public "The cache backend could not serve the operation"
var ChainMisconfigured *errs.Error
ChainMisconfigured is returned by NewChain for a tier set it cannot honour.
Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheChainMisconfigured.
Code 0.3.48.1 · reason CACHE_CHAIN_MISCONFIGURED · public "The cache chain cannot be built from the given tiers"
var EntryRejected *errs.Error
EntryRejected is returned by Set for an entry no store can represent — an empty key, an empty tag, or a negative TTL other than NoExpiry.
Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheEntryRejected.
Code 0.2.18.4 · reason CACHE_ENTRY_REJECTED · public "The cache entry is not storable as given"
var FillFailed *errs.Error
FillFailed is returned by Load when your fill fails and its error carries no SDK code of its own. Nothing is stored: caching a failure turns one bad minute at the origin into a full TTL of confident wrong answers.
Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheFillFailed.
Code 0.2.18.3 · reason CACHE_FILL_FAILED · public "The cache fill function failed and nothing was stored"
var Misconfigured *errs.Error
Misconfigured is returned by a store constructor whose configuration it cannot honour — a non-positive capacity, a negative default TTL.
Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheMisconfigured.
Code 0.2.18.1 · reason CACHE_MISCONFIGURED · public "The cache cannot be built from the given configuration"
var TierFailed *errs.Error
TierFailed is returned when a chained operation failed inside one tier. Its fields name the position, because a chain's caller holds one Store and would otherwise have no way to tell which backend broke.
Initialised with github.com/kitsunium/sdk/internal/core/data/cache.CacheTierFailed.
Code 0.3.48.2 · reason CACHE_TIER_FAILED · public "A tier of the cache chain could not serve the operation"
func New[K comparable, V any](cfg Config[K, V]) *Cache[K, V]
New builds a Cache from cfg. A nil cfg.Clock defaults to the system clock.
func NewChain[V any](cfg ChainConfig, tiers ...Store[V]) (store Store[V], err error)
NewChain puts each store in front of the next: tiers[0] is consulted first, the last is the authority. A hit in a far tier is promoted into every nearer one, carrying its REMAINING TTL and its tags.
Every tier must implement EntryFetcher and Tagger, checked here and refused rather than discovered per call: a value promoted without its tags is one Tagger can no longer reach, so an invalidation would report success while a nearer tier kept serving the entry.
func NewMemory[V any](cfg MemoryConfig) (store Store[V], err error)
NewMemory builds an in-process store: an LRU with TTL, a tag index, and stampede protection on Load.
The returned Store also implements EntryFetcher, Tagger and Loader.
A non-positive MaxEntries is REFUSED, not defaulted. A capacity is the whole statement of how much memory you are willing to spend, so any number chosen for you would be arbitrary — and the value a zero would naturally mean, unbounded, is the dangerous one.
Cache is the public alias for the generic LRU+TTL primitive.
type Cache[K comparable, V any] = kcache.Cache[K, V]
Cache is an alias of github.com/kitsunium/sdk/internal/kernel/collections/cache.Cache, declared as:
type Cache[K comparable, V any] struct{}
func (Cache) Delete
func (*Cache) Delete(key K)
Delete removes key if present. An explicit Delete does NOT fire OnEvict (that hook is for capacity/expiry removals only).
func (Cache) Fetch
func (*Cache) Fetch(key K) (value V, ok bool)
Fetch returns the value for key and whether it was present and unexpired. A hit promotes the entry to most-recently-used (hence Fetch, not a pure Get).
func (Cache) Len
func (*Cache) Len() int
Len returns the current live entry count (including not-yet-reaped expired entries — expiry is lazy).
func (Cache) Purge
func (*Cache) Purge()
Purge drops every entry without firing OnEvict.
func (Cache) Set
func (*Cache) Set(key K, val V)
Set stores key=val with the cache's default TTL.
func (Cache) SetTTL
func (*Cache) SetTTL(key K, val V, ttl time.Duration)
SetTTL stores key=val with an explicit ttl (0 = no expiry), overriding the default for this entry only.
func (Cache) Stats
func (*Cache) Stats() StatsValue
Stats returns a point-in-time copy of the hit/miss/eviction counters.
ChainConfig parameterises NewChain.
type ChainConfig = svccache.ChainConfig
ChainConfig is an alias of github.com/kitsunium/sdk/internal/service/data/cache.ChainConfig, declared as:
type ChainConfig struct {
// OnPromoteError, if non-nil, is called when a value served from a FAR
// tier could not be written into a nearer one.
//
// A failed promotion is not a failed read: the caller already has the
// value, and failing the call would turn a degraded cache into a degraded
// service. But it is also not nothing — a near tier that rejects every
// promotion is a tier that will never be used again, and with this hook nil
// that is completely invisible. Wire it.
OnPromoteError func(key string, err error)
}
Config is the public alias for the primitive's constructor configuration.
type Config[K comparable, V any] = kcache.Config[K, V]
Config is an alias of github.com/kitsunium/sdk/internal/kernel/collections/cache.Config, declared as:
type Config[K comparable, V any] struct {
// MaxEntries caps the live entry count (LRU eviction above it). 0 = unbounded.
MaxEntries int
// DefaultTTL applies to Set entries (SetTTL overrides per-entry). 0 = no expiry.
DefaultTTL time.Duration
// Clock is the time source (injectable for tests). nil = clock.System.
Clock clock.Clock
// OnEvict, if non-nil, is called with the key+value of every entry removed by
// capacity eviction or expiry (not by an explicit Delete/Set-overwrite).
OnEvict func(key K, val V)
}
Entry is what a caller stores: the value, its lifetime, and the tags by which it can later be invalidated in bulk.
type Entry[V any] = corecache.EntryValue[V]
Entry is an alias of github.com/kitsunium/sdk/internal/core/data/cache.EntryValue, declared as:
type Entry[V any] struct {
// Value is the payload.
Value V
// TTL is how long the entry stays fetchable.
//
// > 0 this exact lifetime
// 0 the store's configured default
// [NoExpiry] no deadline at all
//
// Any other negative value is a mistake, not a third meaning, and is
// refused with [CacheEntryRejected] rather than reinterpreted.
TTL time.Duration
// Tags label the entry for bulk invalidation. Order is not significant and
// duplicates are collapsed. An empty tag is refused: it names a set nobody
// can ask for, so accepting it would index the entry under a key that can
// only ever be reached by accident.
//
// Tags cost memory in the store's reverse index — see
// internal/service/data/cache/CLAUDE.md, which states the per-pair cost and
// measures it.
Tags []string
}
EntryFetcher is the sibling that returns the whole entry — value, REMAINING TTL, tags — rather than only the value. Both stores built here implement it.
type EntryFetcher[V any] = corecache.EntryFetcher[V]
EntryFetcher is an alias of github.com/kitsunium/sdk/internal/core/data/cache.EntryFetcher, declared as:
type EntryFetcher[V any] interface {
FetchEntry(ctx context.Context, key string) (EntryValue[V], bool, error)
}
func (EntryFetcher) FetchEntry(ctx context.Context, key string) (EntryValue[V], bool, error)
FetchEntry is Store.Fetch returning the stored entry — value, remaining TTL and tags. It mutates the store for the same reasons Fetch does.
The TTL it reports is the REMAINING time, not the one originally supplied: promoting an entry with its original TTL would extend its life every time it moved between tiers.
Fill computes the entry for a key the cache does not hold: a query, a request, a derivation. It is your code.
type Fill[V any] = corecache.Fill[V]
Fill is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Fill, declared as:
type Fill[V any] func(ctx context.Context) (EntryValue[V], error)
Loader is the sibling that fills a miss while collapsing concurrent misses on one key into a single fill — within one process, and not across replicas. See the package documentation before sizing an origin against it.
type Loader[V any] = corecache.Loader[V]
Loader is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Loader, declared as:
type Loader[V any] interface {
Load(ctx context.Context, key string, fill Fill[V]) (V, error)
}
func (Loader) Load
func (Loader) Load(ctx context.Context, key string, fill Fill[V]) (V, error)
Load returns the value for key, calling fill exactly once across all concurrent callers of this key in this process if the cache does not hold it. A fill that fails stores nothing and returns [CacheFillFailed] — unless the failure already carries an SDK code, in which case it propagates untouched (ADR 0005 §origin wins).
fill does NOT receive ctx. It receives the shared call's context: the first caller's values with every deadline removed, cancelled only once every waiting caller has gone. A caller whose own ctx ends while it waits gets its own ctx.Err() and leaves the fill running for the others.
MemoryConfig parameterises NewMemory. Its zero value is NOT a working cache — MaxEntries must be stated, and is refused rather than defaulted.
type MemoryConfig = svccache.MemoryConfig
MemoryConfig is an alias of github.com/kitsunium/sdk/internal/service/data/cache.MemoryConfig, declared as:
type MemoryConfig struct {
// MaxEntries caps the live entry count; the least-recently-used entry is
// evicted above it. It MUST be positive.
MaxEntries int
// DefaultTTL applies to every entry whose own TTL is zero. Zero means the
// default is "no deadline", so entries leave only by eviction, by Delete,
// or by tag invalidation. Negative is refused.
DefaultTTL time.Duration
// Clock is the time source, injectable so TTL is testable. nil means
// clock.System. Only the reading half of the port is used — a cache reads
// time, it never waits for it (ADR 0039).
Clock clock.Clock
}
Stats is the public alias for the primitive's counters snapshot.
type Stats = kcache.StatsValue
Stats is an alias of github.com/kitsunium/sdk/internal/kernel/collections/cache.StatsValue, declared as:
type Stats struct {
Hits uint64
Misses uint64
Evictions uint64
}
Store is the cache port: Fetch, Set, Delete.
The method set is FROZEN at three. New capabilities arrive as SIBLING interfaces you reach by type assertion — EntryFetcher, Tagger and Loader — because this alias publishes the interface, Go interfaces are structural, and a fourth method would break every downstream implementer at compile time with no deprecation window.
type Store[V any] = corecache.Store[V]
Store is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Store, declared as:
type Store[V any] interface {
Delete(ctx context.Context, key string) error
Fetch(ctx context.Context, key string) (V, bool, error)
Set(ctx context.Context, key string, entry EntryValue[V]) error
}
func (Store) Delete
func (Store) Delete(ctx context.Context, key string) error
Delete removes key. It is idempotent: deleting a key that names nothing reports nil, because a caller who wanted it gone has got what they asked for.
func (Store) Fetch
func (Store) Fetch(ctx context.Context, key string) (V, bool, error)
Fetch returns the value stored under key and whether it was present and unexpired. It MUTATES the store — recency, counters, lazy expiry — which is why it is not called Get. A miss is (zero, false, nil), never an error: a cache that does not hold something has done nothing wrong.
func (Store) Set
func (Store) Set(ctx context.Context, key string, entry EntryValue[V]) error
Set stores entry under key, replacing whatever was there. An empty key, an empty tag, or a negative TTL that is not NoExpiry is refused with [CacheEntryRejected] rather than stored.
Tagger is the sibling for invalidating every entry carrying a tag.
The cost is proportional to the entries carrying the tag, not to the size of the store — measured in internal/service/data/cache/BENCH.md at two store sizes.
type Tagger = corecache.Tagger
Tagger is an alias of github.com/kitsunium/sdk/internal/core/data/cache.Tagger, declared as:
type Tagger interface {
// InvalidateTag removes every entry carrying tag and reports how many it
// removed. A tag that names nothing removes zero and reports nil.
//
// The cost is proportional to the number of entries carrying the tag, NOT
// to the size of the store — see internal/service/data/cache/BENCH.md, which
// measures it at two store sizes for exactly this claim.
InvalidateTag(ctx context.Context, tag string) (removed int, err error)
}
func (Tagger) InvalidateTag(ctx context.Context, tag string) (removed int, err error)
InvalidateTag removes every entry carrying tag and reports how many it removed. A tag that names nothing removes zero and reports nil.
The cost is proportional to the number of entries carrying the tag, NOT to the size of the store — see internal/service/data/cache/BENCH.md, which measures it at two store sizes for exactly this claim.
Generated from docs/api by tools/genindex.