Skip to content

Store Primer

Open this page after Stable Roots when the change still belongs to the stable surface, but the real question has narrowed to persistence primitives and storage contracts.

store owns base persistence abstractions. It is the right home when the work should stay below topology-heavy data features, tenant-aware implementations, and HTTP-facing file or cache behavior.

var repo store.Repository[User] = newUserRepo(db)
user, err := repo.Get(ctx, userID)
if err != nil {
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeNotFound).
Message("user not found").
Build())
return
}
_ = contract.WriteResponse(w, r, http.StatusOK, user, nil)
  • you are defining stable storage contracts or basic persistence helpers
  • the work stays below provider-specific topology decisions
  • the change belongs to file, KV, cache, idempotency, or DB helper primitives
  • the work is tenant-aware storage policy or tenant-aware adapters
  • the task introduces distributed engine behavior, signed URLs, or metadata-manager ownership
  • the real problem is upload/download HTTP transport or gateway-owned response caching

First files to read in the current repository

Section titled “First files to read in the current repository”
  1. store/module.yaml
  2. the target package under store/*
  3. specs/repo.yaml
Keep it in store when the work is aboutMove out when the work becomes
store/file interfaces, shared file types, and errorstenant-aware file backends and metadata persistence in x/data/file, or HTTP upload/download endpoints in x/fileapi
store/kv file-backed KV CRUD, TTL-aware helpers, key scans, and basic statsWAL, snapshots, serializer selection, compression, and shard tuning in x/data/kvengine
store/idempotency records, statuses, errors, and the minimal Store interfacedurable SQL or KV providers, SQL dialect policy, and duplicate-key handling in x/data/idempotency
store/cache base cache primitivesdistributed cache engines in x/data/cache, HTTP response caching in x/gateway/cache, and tenant-aware cache adapters in x/tenant/store/cache
store/db helper execution with caller-owned context.ContextDB analytics and slow-query inspection in x/observability/dbinsights
import kvstore "github.com/spcent/plumego/store/kv"
kv, err := kvstore.NewKVStore(kvstore.Options{
DataDir: "/var/lib/myapp/kv",
})
if err != nil { ... }
defer kv.Close()
kv.Set("session:abc", []byte(`{"user":"alice"}`), 30*time.Minute)
data, err := kv.Get("session:abc")
kv.Delete("session:abc")
exists := kv.Exists("session:abc")

KVStore is safe for concurrent use. Use it for short-lived application state such as JWT keys, sessions, and rate-limit counters. For distributed or high-throughput scenarios, see x/data/cache.

import "github.com/spcent/plumego/store/cache"
c, err := cache.NewMemoryCacheWithConfig(cache.DefaultConfig())
if err != nil { ... }
c.Set(ctx, "user:42", jsonBytes, 5*time.Minute)
data, err := c.Get(ctx, "user:42")

The Cache interface lets you swap implementations:

type Cache interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)
Close() error
}
import "github.com/spcent/plumego/store/db"
sqlDB, err := db.Open(db.DefaultConfig("postgres", dsn))
if err != nil { ... }
rows, err := db.QueryContext(ctx, sqlDB, "SELECT id, name FROM users WHERE active = $1", true)
result, err := db.ExecContext(ctx, sqlDB, "UPDATE users SET name = $1 WHERE id = $2", name, id)
err = db.WithTransaction(ctx, sqlDB, nil, func(tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, "INSERT INTO orders ...", ...)
return err
})

store/idempotency — exactly-once operation guard

Section titled “store/idempotency — exactly-once operation guard”
import "github.com/spcent/plumego/store/idempotency"
// Implement the Store interface (or use x/data/idempotency for a durable provider)
var idem idempotency.Store
rec, err := idem.Get(ctx, idempotencyKey)
if err == nil && rec.Status == idempotency.StatusCompleted {
// replay the stored response
return rec.Response, nil
}
// Process the operation
result, err := doExpensiveOperation(ctx, req)
if err != nil { return nil, err }
idem.Set(ctx, idempotencyKey, idempotency.Record{
Status: idempotency.StatusCompleted,
Response: result,
})

store looks broad because persistence touches many capabilities. The current repository narrows it deliberately: keep the stable persistence contracts here, and move provider-heavy, topology-heavy, tenant-aware, and HTTP-facing behavior outward.