cache

package module
v2.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 15 Imported by: 2

README

cache

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

cache is a typed Go cache library with explicit hit, miss, stale, decode, and backend-failure semantics. It provides bounded cache-aside loading, versioned and hashed keys, strict codecs, a bounded memory backend, and native Redis and Valkey adapters. Valkey-backed caches can also publish a refresh only while a compatible distributed lease is still owned.

The semantic API does not expose Redis or Valkey client types. Backends keep their native clients and atomic behavior while applications share one portable contract.

The published v1 module is a stable member of Golib's Persistence and durability family. The current source is the v2.0.0 release candidate with protected-error and bounded-shutdown contracts. For ecosystem-wide selection and ownership guidance, see the versioned Golib ecosystem index and its Persistence and durability family.

Install

go get github.com/faustbrian/go-cache

Go 1.27.0 or newer is required.

Quickstart

package main

import (
	"context"
	"fmt"
	"time"

	cache "github.com/faustbrian/go-cache"
	cachememory "github.com/faustbrian/go-cache/adapters/memory"
)

type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

func main() {
	ctx := context.Background()
	backend, err := cachememory.New(cachememory.Config{
		MaxEntries: 10_000,
		MaxBytes:   64 << 20,
		Clock:      cache.SystemClock{},
	})
	if err != nil {
		panic(err)
	}

	keys, err := cache.NewKeySpace("accounts", "user", 1,
		cache.StringKeyEncoder{}, 128)
	if err != nil {
		panic(err)
	}
	users, err := cache.New(cache.Config[string, User]{
		Backend:  backend,
		Keys:     keys,
		Codec:    cache.JSONCodec[User]{Version: 1},
		TTL:      cache.TTLPolicy{TTL: 5 * time.Minute},
		Clock:    cache.SystemClock{},
		MaxValue: 1 << 20,
		Load: cache.LoadPolicy{
			MaxConcurrent:    64,
			MaxWaitersPerKey: 256,
			NegativeTTL:      30 * time.Second,
		},
	})
	if err != nil {
		panic(err)
	}
	defer users.Close()

	result, err := users.GetOrLoad(ctx, "user-42",
		func(ctx context.Context, id string) (cache.LoadResult[User], error) {
			// Replace this with a context-aware source lookup.
			return cache.LoadResult[User]{Value: User{ID: id, Name: "Ada"}, Found: true}, nil
		})
	if err != nil {
		panic(err)
	}
	fmt.Println(result.State, result.Value.Name)
}

V2 adoption

Confirm that the /v2 v2.0.0 tag resolves publicly before changing production dependencies. Consumers can migrate with the guide in docs/migration.md and, after publication, install it with:

go get github.com/faustbrian/go-cache/v2@v2.0.0

Always inspect Result.State; a stored zero value can be a Hit. A miss is not an error. Backend, decoding, schema, policy, limit, and loader failures remain errors and can be classified with errors.Is.

For distributed refreshes, acquire a Valkey lease through github.com/faustbrian/go-lease/valkey, derive its opaque guard with Store.Guard, and pass that guard to Cache.SetIfOwned or Cache.SetNegativeIfOwned. The cache record and active owner/token comparison occur in one Valkey script. Protected negative publication uses the configured NegativeTTL. The lease store and cache backend must use the same standalone Valkey deployment.

Choose a policy

  • Use plain cache-aside for data where a source lookup is affordable.
  • Add a short NegativeTTL only when source-level absence is authoritative.
  • Use StaleWhileRevalidate when low latency is more important than returning the refresh error.
  • Use StaleIfError when callers must see refresh failures but may also use the stale value. It returns both the stale Result and the error.
  • Do not enable both stale policies; construction rejects ambiguous precedence.

See policy decisions and failure modes before enabling stale behavior.

Backends and observability

Use the root package for portable cache policy and result semantics. Select a package under adapters/ only for its named integration, cachetest only for backend conformance tests, and an observability adapter only when the corresponding integration is required. Do not use this module as durable authoritative storage, a distributed-lock implementation, or an application-wide retry owner.

Service lifecycle

adapters/service.New adapts an explicit concrete cache, Redis, or Valkey resource to service.Component. Startup validation and readiness are opt-in callbacks that receive the service context and the concrete resource. Callers add the readiness check to service.Plan only when cache availability is required to accept new work.

Omitting Shutdown keeps the resource shared and guarantees the adapter never closes it. Providing Shutdown explicitly transfers close ownership. The adapter performs no retries, closes a transferred resource once after draining later-declared components, and preserves startup and partial-cleanup failures.

The former backend/*, cacheservice, and observability/* import paths are deprecated compatibility facades. New code should use the corresponding adapters/* package; existing callers can migrate imports without changing configuration or runtime semantics.

Documentation

Use the documentation index for behavior, backends, operations, adoption, compatibility, and security guidance.

Development and verification guidance is in CONTRIBUTING.md.

License

MIT. See LICENSE.

Documentation

Overview

Package cache provides typed, backend-independent cache semantics.

Index

Examples

Constants

View Source
const MaxLoadFlights = 1 << 16

MaxLoadFlights bounds configured loader concurrency and retained flight work.

Variables

View Source
var (
	// ErrMiss identifies an explicit cache miss where an error value is needed.
	ErrMiss = errors.New("cache miss")
	// ErrBackend identifies a storage or transport failure.
	ErrBackend = errors.New("cache backend error")
	// ErrDecode identifies malformed serialized data.
	ErrDecode = errors.New("cache decode error")
	// ErrSchemaMismatch identifies an incompatible payload version.
	ErrSchemaMismatch = errors.New("cache schema mismatch")
	// ErrInvalidKey identifies invalid key configuration or encoding.
	ErrInvalidKey = errors.New("invalid cache key")
	// ErrKeyTooLarge identifies a backend key beyond its configured bound.
	ErrKeyTooLarge = errors.New("cache key too large")
	// ErrValueTooLarge identifies a payload beyond its configured bound.
	ErrValueTooLarge = errors.New("cache value too large")
	// ErrInvalidTTL identifies an invalid or already expired deadline.
	ErrInvalidTTL = errors.New("invalid cache TTL")
	// ErrCapacity identifies a record that cannot fit a bounded backend.
	ErrCapacity = errors.New("cache capacity exceeded")
	// ErrClosed identifies use after cache or backend shutdown.
	ErrClosed = errors.New("cache backend closed")
	// ErrShutdownIncomplete identifies shutdown ending with active loader work.
	ErrShutdownIncomplete = errors.New("cache shutdown incomplete")
	// ErrLoader identifies a source loader failure.
	ErrLoader = errors.New("cache loader error")
	// ErrLoaderPanic identifies a recovered loader panic.
	ErrLoaderPanic = errors.New("cache loader panic")
	// ErrRecursiveLoad identifies a loader re-entering the same cache.
	ErrRecursiveLoad = errors.New("recursive cache load")
	// ErrWaiterLimit identifies excess callers for one active key flight.
	ErrWaiterLimit = errors.New("cache waiter limit exceeded")
	// ErrFlightLimit identifies excess distinct-key load or refresh work.
	ErrFlightLimit = errors.New("cache flight limit exceeded")
	// ErrInvalidPolicy identifies invalid or contradictory policy options.
	ErrInvalidPolicy = errors.New("invalid cache policy")
	// ErrBatchTooLarge identifies a bulk request beyond its configured bound.
	ErrBatchTooLarge = errors.New("cache batch too large")
	// ErrInvalidRecord identifies malformed portable backend state.
	ErrInvalidRecord = errors.New("invalid cache record")
	// ErrInvalidConfig identifies invalid constructor dependencies or limits.
	ErrInvalidConfig = errors.New("invalid cache configuration")
	// ErrOwnershipLost identifies a protected write rejected by its backend.
	ErrOwnershipLost = errors.New("cache ownership lost")
	// ErrOwnershipUnsupported identifies a backend without atomic ownership validation.
	ErrOwnershipUnsupported = errors.New("cache ownership validation unsupported")
)

Functions

This section is empty.

Types

type Backend

type Backend interface {
	Get(context.Context, string) (Record, bool, error)
	Set(context.Context, string, Record, Condition) (bool, error)
	Delete(context.Context, string) (bool, error)
}

Backend is the atomic storage contract implemented by cache adapters.

type BulkResult

type BulkResult[K, V any] struct {
	Key K
	Result[V]
	Err error
}

BulkResult reports one GetMany result without flattening per-key errors.

type Cache

type Cache[K, V any] struct {
	// contains filtered or unexported fields
}

Cache provides typed cache operations over a backend.

func New

func New[K, V any](config Config[K, V]) (*Cache[K, V], error)

New validates config and constructs a typed cache.

func (*Cache[K, V]) Add

func (c *Cache[K, V]) Add(ctx context.Context, logical K, value V) (bool, error)

Add writes a value only when the key has no live record.

func (*Cache[K, V]) Close

func (c *Cache[K, V]) Close() error

Close cancels active loads, rejects new loads, and waits up to five seconds for loader cleanup.

func (*Cache[K, V]) Delete

func (c *Cache[K, V]) Delete(ctx context.Context, logical K) (err error)

Delete removes a logical key. Deleting an absent key succeeds.

func (*Cache[K, V]) DeleteMany

func (c *Cache[K, V]) DeleteMany(ctx context.Context, keys []K) ([]MutationResult[K], error)

DeleteMany deletes keys in input order and records per-key failures.

func (*Cache[K, V]) Get

func (c *Cache[K, V]) Get(ctx context.Context, logical K) (result Result[V], err error)

Get returns an explicit hit, miss, or stale result.

func (*Cache[K, V]) GetMany

func (c *Cache[K, V]) GetMany(ctx context.Context, keys []K) ([]BulkResult[K, V], error)

GetMany reads keys in input order and records per-key failures in the result.

func (*Cache[K, V]) GetOrLoad

func (c *Cache[K, V]) GetOrLoad(ctx context.Context, logical K, loader Loader[K, V]) (Result[V], error)

GetOrLoad returns a cached value or coalesces a bounded source load.

Example
package main

import (
	"context"
	"fmt"
	"time"

	cache "github.com/faustbrian/go-cache/v2"
	cachememory "github.com/faustbrian/go-cache/v2/adapters/memory"
)

func main() {
	backend, _ := cachememory.New(cachememory.Config{
		MaxEntries: 100,
		MaxBytes:   1 << 20,
		Clock:      cache.SystemClock{},
	})
	keys, _ := cache.NewKeySpace("example", "greeting", 1, cache.StringKeyEncoder{}, 128)
	store, _ := cache.New(cache.Config[string, string]{
		Backend:  backend,
		Keys:     keys,
		Codec:    cache.JSONCodec[string]{Version: 1},
		TTL:      cache.TTLPolicy{TTL: time.Minute},
		Clock:    cache.SystemClock{},
		MaxValue: 1024,
	})
	defer func() { _ = store.Close() }()

	result, err := store.GetOrLoad(context.Background(), "hello",
		func(context.Context, string) (cache.LoadResult[string], error) {
			return cache.LoadResult[string]{Value: "world", Found: true}, nil
		})
	fmt.Println(result.State == cache.Hit, result.Value, err)
}
Output:
true world <nil>

func (*Cache[K, V]) Replace

func (c *Cache[K, V]) Replace(ctx context.Context, logical K, value V) (bool, error)

Replace writes a value only when the key has a live record.

func (*Cache[K, V]) Set

func (c *Cache[K, V]) Set(ctx context.Context, logical K, value V) error

Set writes a value without an existence precondition.

func (*Cache[K, V]) SetIfOwned

func (c *Cache[K, V]) SetIfOwned(
	ctx context.Context,
	logical K,
	value V,
	guard OwnershipGuard,
) error

SetIfOwned atomically writes a value only while guard identifies the active backend owner. Ownership loss is reported as ErrOwnershipLost.

func (*Cache[K, V]) SetMany

func (c *Cache[K, V]) SetMany(ctx context.Context, entries []Entry[K, V]) ([]MutationResult[K], error)

SetMany writes entries in input order and records per-key failures.

func (*Cache[K, V]) SetNegativeIfOwned

func (c *Cache[K, V]) SetNegativeIfOwned(
	ctx context.Context,
	logical K,
	guard OwnershipGuard,
) error

SetNegativeIfOwned atomically writes an explicit negative record only while guard identifies the active backend owner. The configured NegativeTTL must be positive. Ownership loss is reported as ErrOwnershipLost.

func (*Cache[K, V]) Shutdown

func (c *Cache[K, V]) Shutdown(ctx context.Context) error

Shutdown cancels active loads, rejects new loads, and waits for loader cleanup within ctx. A nil error means all active loads have completed. ErrShutdownIncomplete permits late completion of already-admitted backend work when trusted callbacks or backends ignore cancellation.

type Clock

type Clock interface {
	Now() time.Time
}

Clock supplies time for deterministic expiration behavior.

type Codec

type Codec[V any] interface {
	Encode(V) ([]byte, error)
	Decode([]byte) (V, error)
}

Codec serializes and deserializes typed cache values.

type Condition

type Condition uint8

Condition controls the atomic precondition applied by Backend.Set.

const (
	// Unconditional always writes the record.
	Unconditional Condition = iota
	// IfAbsent writes only when the key does not hold a live record.
	IfAbsent
	// IfPresent writes only when the key holds a live record.
	IfPresent
)

type Config

type Config[K, V any] struct {
	Backend  Backend
	Keys     KeySpace[K]
	Codec    Codec[V]
	TTL      TTLPolicy
	Clock    Clock
	MaxValue int
	MaxBatch int
	Load     LoadPolicy
	Jitter   JitterSource
	Observer Observer
}

Config contains all dependencies, limits, and policies for a Cache.

type Entry

type Entry[K, V any] struct {
	Key   K
	Value V
}

Entry pairs a logical key with a value for SetMany.

type Error

type Error struct {
	Kind      ErrorKind
	Operation Operation
	// contains filtered or unexported fields
}

Error combines a stable semantic kind with a protected cause identity. Construct errors with NewError when a source cause must remain recognizable through errors.Is without exposing its text or concrete type.

func NewError

func NewError(kind ErrorKind, operation Operation, cause error) *Error

NewError creates a classified error whose cause remains recognizable with errors.Is but is hidden from formatting and errors.As.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() []error

type ErrorKind

type ErrorKind uint8

ErrorKind classifies an operation failure independently of its cause.

const (
	// BackendError identifies storage or transport failures.
	BackendError ErrorKind = iota + 1
	// DecodeError identifies malformed encoded values.
	DecodeError
	// SchemaMismatchError identifies incompatible payload versions.
	SchemaMismatchError
	// InvalidKeyError identifies invalid key configuration or encoding.
	InvalidKeyError
	// LimitError identifies a configured resource-limit violation.
	LimitError
	// PolicyError identifies an invalid or contradictory policy.
	PolicyError
	// LoaderError identifies a source loader failure.
	LoaderError
)

type Event

type Event struct {
	Operation Operation
	Outcome   Outcome
	Duration  time.Duration
	Size      int
}

Event is a redacted semantic observation with no key or value fields.

type JSONCodec

type JSONCodec[V any] struct {
	Version        byte
	MaxEncodedSize int
}

JSONCodec stores strict JSON behind a one-byte schema version.

func (JSONCodec[V]) Decode

func (c JSONCodec[V]) Decode(encoded []byte) (V, error)

Decode validates size and schema version before strict JSON decoding.

func (JSONCodec[V]) Encode

func (c JSONCodec[V]) Encode(value V) ([]byte, error)

Encode serializes a value with its configured schema version.

type JitterSource

type JitterSource interface {
	Duration(time.Duration) time.Duration
}

JitterSource chooses how much time to subtract from a loaded value's TTL.

type KeyEncoder

type KeyEncoder[K any] interface {
	EncodeKey(K) ([]byte, error)
}

KeyEncoder deterministically converts a typed logical key to bytes.

type KeySpace

type KeySpace[K any] struct {
	// contains filtered or unexported fields
}

KeySpace hashes logical keys beneath a namespace, name, and version prefix.

func NewKeySpace

func NewKeySpace[K any](
	namespace string,
	name string,
	version uint32,
	encoder KeyEncoder[K],
	maxKeySize int,
) (KeySpace[K], error)

NewKeySpace validates and constructs an isolated versioned key space.

func (KeySpace[K]) Key

func (s KeySpace[K]) Key(logical K) (string, error)

Key returns a deterministic backend key without exposing logical key bytes.

type LoadPolicy

type LoadPolicy struct {
	MaxConcurrent int
	// MaxFlights bounds distinct-key flights, including queued loads and
	// completed flights retained by an explicit mutation. Zero defaults to 1024.
	MaxFlights           int
	MaxWaitersPerKey     int
	NegativeTTL          time.Duration
	StaleWhileRevalidate bool
	StaleIfError         bool
	RefreshJitter        time.Duration
}

LoadPolicy bounds loading and enables optional negative and stale behavior.

type LoadResult

type LoadResult[V any] struct {
	Value V
	Found bool
}

LoadResult is the value and existence result returned by a Loader.

type Loader

type Loader[K, V any] func(context.Context, K) (LoadResult[V], error)

Loader fetches a logical key from its source of truth.

type MutationResult

type MutationResult[K any] struct {
	Key K
	Err error
}

MutationResult reports one bulk mutation error in input order.

type Observer

type Observer interface {
	Observe(context.Context, Event) error
}

Observer receives best-effort semantic events from cache operations.

type Operation

type Operation string

Operation names the semantic cache action associated with an event or error.

const (
	// OperationGet identifies a read.
	OperationGet Operation = "get"
	// OperationSet identifies a write.
	OperationSet Operation = "set"
	// OperationDelete identifies invalidation.
	OperationDelete Operation = "delete"
	// OperationLoad identifies a source load.
	OperationLoad Operation = "load"
	// OperationEvict identifies capacity eviction.
	OperationEvict Operation = "evict"
	// OperationExpire identifies deadline expiration.
	OperationExpire Operation = "expire"
)

type Outcome

type Outcome string

Outcome is a low-cardinality semantic operation result.

const (
	// OutcomeSuccess identifies a successful mutation or load.
	OutcomeSuccess Outcome = "success"
	// OutcomeHit identifies a fresh read.
	OutcomeHit Outcome = "hit"
	// OutcomeMiss identifies an absent read.
	OutcomeMiss Outcome = "miss"
	// OutcomeStale identifies a stale read.
	OutcomeStale Outcome = "stale"
	// OutcomeNegative identifies a negative-cache result.
	OutcomeNegative Outcome = "negative"
	// OutcomeRejected identifies a failed conditional mutation.
	OutcomeRejected Outcome = "rejected"
	// OutcomeError identifies an operation failure.
	OutcomeError Outcome = "error"
	// OutcomeEvicted identifies capacity eviction.
	OutcomeEvicted Outcome = "evicted"
	// OutcomeExpired identifies deadline expiration.
	OutcomeExpired Outcome = "expired"
)

type OwnershipBackend

type OwnershipBackend interface {
	Backend
	SetIfOwned(context.Context, string, Record, OwnershipGuard) error
}

OwnershipBackend atomically validates ownership and writes one record.

type OwnershipGuard

type OwnershipGuard interface {
	StorageKey() string
	Owner() string
	Token() string
}

OwnershipGuard identifies backend-authenticated ownership for one protected write. Implementations must return opaque storage coordinates, not logical keys or credentials.

type RandomJitter

type RandomJitter struct{}

RandomJitter samples a non-cryptographic duration in [0, max).

func (RandomJitter) Duration

func (RandomJitter) Duration(upperBound time.Duration) time.Duration

Duration returns a non-negative duration below max, or zero for max <= 0.

type Record

type Record struct {
	Payload   []byte
	ExpiresAt time.Time
	StaleAt   time.Time
	Negative  bool
}

Record is the portable value and expiration envelope stored by a Backend.

func (Record) Clone

func (r Record) Clone() Record

Clone returns a portable record whose payload does not alias the receiver.

func (Record) Validate

func (r Record) Validate() error

Validate checks deadline portability and negative-record invariants.

type Result

type Result[V any] struct {
	State    State
	Value    V
	Negative bool
}

Result is the explicit outcome of a typed cache read.

type State

type State uint8

State describes whether a cache lookup is fresh, absent, or stale.

const (
	// Hit indicates a fresh cached value.
	Hit State = iota + 1
	// Miss indicates no usable cached value.
	Miss
	// Stale indicates a decoded value beyond its fresh TTL but within StaleFor.
	Stale
)

type StringKeyEncoder

type StringKeyEncoder struct{}

StringKeyEncoder encodes strings without conversion.

func (StringKeyEncoder) EncodeKey

func (StringKeyEncoder) EncodeKey(key string) ([]byte, error)

EncodeKey returns the UTF-8 bytes of key.

type SystemClock

type SystemClock struct{}

SystemClock uses the system wall clock.

func (SystemClock) Now

func (SystemClock) Now() time.Time

Now returns the current system time.

type TTLPolicy

type TTLPolicy struct {
	TTL      time.Duration
	StaleFor time.Duration
	Sliding  bool
}

TTLPolicy defines the fresh and stale lifetime of positive records.

func (TTLPolicy) Validate

func (p TTLPolicy) Validate() error

Validate reports whether the TTL and stale window are well formed.

Directories

Path Synopsis
adapters
memory
Package cachememory provides a bounded, concurrency-safe LRU cache adapter.
Package cachememory provides a bounded, concurrency-safe LRU cache adapter.
otel
Package cacheotel exports cache events as low-cardinality OpenTelemetry metrics.
Package cacheotel exports cache events as low-cardinality OpenTelemetry metrics.
redis
Package cacheredis adapts go-redis/v9 clients to the cache backend contract.
Package cacheredis adapts go-redis/v9 clients to the cache backend contract.
service
Package cacheservice adapts explicit cache resources to the service lifecycle without hiding their concrete types.
Package cacheservice adapts explicit cache resources to the service lifecycle without hiding their concrete types.
slog
Package cacheslog records redacted cache events with the standard log package.
Package cacheslog records redacted cache events with the standard log package.
valkey
Package cachevalkey adapts valkey-go clients to the cache backend contract.
Package cachevalkey adapts valkey-go clients to the cache backend contract.
backend
memory
Package memory is the legacy bounded, concurrency-safe LRU cache backend.
Package memory is the legacy bounded, concurrency-safe LRU cache backend.
redis
Package redis is the legacy go-redis/v9 cache adapter.
Package redis is the legacy go-redis/v9 cache adapter.
valkey
Package valkey is the legacy valkey-go cache adapter.
Package valkey is the legacy valkey-go cache adapter.
Package cacheservice is the legacy cache service-lifecycle adapter.
Package cacheservice is the legacy cache service-lifecycle adapter.
Package cachetest provides a shared backend conformance suite.
Package cachetest provides a shared backend conformance suite.
internal
wire
Package wire encodes portable backend records for remote storage.
Package wire encodes portable backend records for remote storage.
observability
otel
Package otel is the legacy low-cardinality OpenTelemetry cache adapter.
Package otel is the legacy low-cardinality OpenTelemetry cache adapter.
slog
Package slog is the legacy redacted standard-log cache adapter.
Package slog is the legacy redacted standard-log cache adapter.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL