Skip to main content
Version: 1.1.0

Caching API Reference

Quark cache support is opt-in per client and per query.

Query Cache

Cache(ttl time.Duration, tags ...string) *Query[T]

Enables result caching for a query.

users, err := quark.For[User](ctx, client).
Where("active", "=", true).
Limit(100).
Cache(5*time.Minute, "users", "users:active").
List()

The query will only use caching if the client has a cache store:

client, err := quark.New("postgres", dsn,
quark.WithCacheStore(store),
)

Cache keys include:

ComponentPurpose
Dialect nameSeparates SQL syntax by engine.
Tenant IDPrevents row-level tenant cache leaks.
SchemaPrevents schema-per-tenant cache leaks.
SQL stringSeparates query shapes.
ArgumentsSeparates parameter values.

Tags

If no tags are passed, Quark tags the cache entry with the model table name:

users, err := quark.For[User](ctx, client).
Cache(5*time.Minute).
List()

If custom tags are passed, Quark uses exactly those tags. Include the table tag when you want automatic invalidation on writes to catch the entry:

users, err := quark.For[User](ctx, client).
Cache(5*time.Minute, "users", "users:active").
List()

Successful write executions invalidate the model table tag.

Per-row invalidation (<table>:<pk>)

When a mutation knows the affected primary key — Update, UpdateFields, Tracked.Save, the by-PK delete paths, and Create once the new ID is populated — Quark also invalidates the <table>:<pk> tag in the same InvalidateTags call. Cache by-PK queries with that tag to avoid flushing the whole table on every row write:

user, err := quark.For[User](ctx, client).
Where("id", "=", 1).
Cache(5*time.Minute, "users", "users:1"). // both tags
First()

A later quark.For[User](ctx, client).UpdateFields(&u, "name") on the row with id = 1 invalidates users (every listing on the table, as before) AND users:1 (this single Find). Updates on a different row, e.g. id = 2, invalidate users (still consistent for listings) and users:2users:1 survives.

Mutations whose affected rows aren't known up front (DeleteBatch with a complex WHERE, raw Exec, Upsert/UpsertBatch) emit only the table tag — the historical fallback. UpdateBatch and Update / UpdateFields / Tracked.Save / Create / by-PK Delete all DO emit per-row tags. Composite-primary-key models fall back to the table tag; a follow-up may add a stable encoding of composite keys for per-row invalidation if demand surfaces.

:::note Tag format is opaque The <table>:<pk> shape is implementation. Treat it as an opaque identifier — pass it whole to Cache(...), don't parse it. A string PK that contains : is preserved verbatim (orders:abc:def); equality matching still works, but splitting on : does not. :::

Stampede protection

Every CacheStore passed to WithCacheStore is wrapped automatically with singleflight + TTL jitter + XFetch (ADR-0011). The wrapper implements CacheStore, so existing third-party stores keep working unchanged inside it. Two Options tune the wrapper; both are optional.

WithCacheJitter(pct float64) Option

Sets the ±jitter factor applied to every TTL. Default 0.1 (±10%). Range [0, 1]; values outside are clamped. Setting to 0 disables jitter — singleflight and XFetch stay active.

client, _ := quark.New("pgx", dsn,
quark.WithCacheStore(memory.New()),
quark.WithCacheJitter(0.2), // ±20%
)

WithCacheXFetchBeta(beta float64) Option

Tunes the XFetch probabilistic-early-refresh threshold (Vattani et al.). Default 1.0. Range β ≥ 0. Higher β triggers earlier refresh; β = 0 disables XFetch — singleflight and jitter stay active.

client, _ := quark.New("pgx", dsn,
quark.WithCacheStore(memory.New()),
quark.WithCacheXFetchBeta(0), // XFetch off
)

See Caching and Observability — Stampede protection for the design rationale and the cross-instance gap.

CacheStore

type CacheStore interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, val []byte, ttl time.Duration, tags ...string) error
Delete(ctx context.Context, key string) error
InvalidateTags(ctx context.Context, tags ...string) error
}

Cached values are JSON-encoded []T results.

Memory Store

import "github.com/jcsvwinston/quark/cache/memory"

store := memory.New()
defer store.Close()

client, err := quark.New("sqlite", "file:app.db?cache=shared",
quark.WithCacheStore(store),
)

The memory store is process-local, thread-safe, and supports tag invalidation through an in-memory reverse index.

Redis Store

import rediscache "github.com/jcsvwinston/quark/cache/redis"

store := rediscache.New(rediscache.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})

if err := store.Ping(ctx); err != nil {
return err
}

client, err := quark.New("postgres", dsn,
quark.WithCacheStore(store),
)

Redis keys use these prefixes:

PrefixPurpose
quark:cache:Cached query payloads.
quark:tag:Redis set mapping tags to cache keys.

Manual Invalidation

err := store.InvalidateTags(ctx, "users:active")

Use manual invalidation when a cache entry is tagged by a business concept or when writes happen outside Quark.