Grove

Keyspaces

Typed, namespaced key-value partitions with automatic prefix management, per-keyspace codecs, default TTLs, and type-safe access.

A Keyspace[T] is a typed, namespaced partition of a KV store. It binds a Go type, key prefix, codec, and TTL policy together so that every operation within the keyspace is type-safe, consistently prefixed, and automatically serialized.

Why Keyspaces?

Without keyspaces, you manage key prefixes and type assertions manually:

// Manual approach -- error-prone and repetitive.
store.Set(ctx, "users:u_123", &user, kv.WithTTL(24*time.Hour))
var user User
store.Get(ctx, "users:u_123", &user)

With keyspaces, the prefix, TTL, and codec are declared once:

// Keyspace approach -- type-safe and DRY.
users := keyspace.New[User](store, "users",
    keyspace.WithTTL(24*time.Hour),
)
users.Set(ctx, "u_123", user)
user, err := users.Get(ctx, "u_123")

The keyspace automatically prepends "users:" to every key, applies a 24-hour TTL to every Set, and returns a typed User from every Get -- no type assertions needed.

Creating a Keyspace

import (
    "github.com/xraph/grove/kv/keyspace"
    "github.com/xraph/grove/kv/codec"
)

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

users := keyspace.New[User](store, "users")

The type parameter T determines the value type for all operations. The prefix string is prepended to every key with a separator (default ":").

Options

OptionDescription
keyspace.WithTTL(d)Default TTL for all Set operations in the keyspace
keyspace.WithCodec(c)Override the store-level codec for this keyspace
keyspace.WithSeparator(sep)Change the key segment separator (default ":")
sessions := keyspace.New[Session](store, "session",
    keyspace.WithTTL(30*time.Minute),
    keyspace.WithCodec(codec.MsgPack()),
    keyspace.WithSeparator("/"),
)
// Keys will be "session/abc123" instead of "session:abc123".

If no codec is specified, the keyspace inherits the store's default codec.

Methods

Get

Retrieves and decodes the value for the given key. Returns a typed T value directly.

func (ks *Keyspace[T]) Get(ctx context.Context, key string) (T, error)
user, err := users.Get(ctx, "u_123")
if errors.Is(err, kv.ErrNotFound) {
    // key does not exist
}
fmt.Println(user.Name)

GetEntry

Retrieves the value with full metadata including TTL, version, and key.

func (ks *Keyspace[T]) GetEntry(ctx context.Context, key string) (*kv.Entry[T], error)
entry, err := users.GetEntry(ctx, "u_123")
fmt.Println(entry.Value.Name)  // the User
fmt.Println(entry.Key)         // "users:u_123"
fmt.Println(entry.TTL)         // remaining TTL
fmt.Println(entry.Version)     // CAS version (if supported)

The Entry[T] struct:

type Entry[T any] struct {
    Key      string
    Value    T
    TTL      time.Duration
    Version  uint64
    Metadata map[string]string
}

Set

Encodes and stores the value. Applies the keyspace's default TTL unless overridden by SetOption values.

func (ks *Keyspace[T]) Set(ctx context.Context, key string, value T, opts ...kv.SetOption) error
err := users.Set(ctx, "u_123", User{
    ID:    "u_123",
    Name:  "Alice",
    Email: "alice@example.com",
})

Override the default TTL for a single call:

err := users.Set(ctx, "u_temp", tempUser, kv.WithTTL(5*time.Minute))

Delete

Removes one or more keys within the keyspace.

func (ks *Keyspace[T]) Delete(ctx context.Context, keys ...string) error
err := users.Delete(ctx, "u_123", "u_456")

Exists

Returns the count of keys that exist within the keyspace.

func (ks *Keyspace[T]) Exists(ctx context.Context, keys ...string) (int64, error)
count, err := users.Exists(ctx, "u_123", "u_456")

MGet

Retrieves multiple keys within the keyspace. Returns a typed map[string]T keyed by the original (unprefixed) key names.

func (ks *Keyspace[T]) MGet(ctx context.Context, keys []string) (map[string]T, error)
result, err := users.MGet(ctx, []string{"u_123", "u_456"})
for key, user := range result {
    fmt.Printf("%s => %s\n", key, user.Name)
}

Scan

Iterates over keys matching a pattern suffix within the keyspace. The keys passed to the callback are stripped of the keyspace prefix.

func (ks *Keyspace[T]) Scan(ctx context.Context, pattern string, fn func(key string) error) error
err := users.Scan(ctx, "*", func(key string) error {
    fmt.Println("user key:", key) // prints "u_123", not "users:u_123"
    return nil
})

Prefix and Store

Access the keyspace's prefix and underlying store:

fmt.Println(users.Prefix()) // "users"
store := users.Store()      // the underlying *kv.Store

Key Composition

The keyspace package includes helpers for building and parsing composite keys:

ComposeKey

Joins segments with a separator:

key := keyspace.ComposeKey(":", "org", "team", "member")
// "org:team:member"

ParseKey

Splits a composite key into segments:

segments := keyspace.ParseKey("org:team:member", ":")
// ["org", "team", "member"]

Join

Shorthand for ComposeKey with ":" as the separator:

key := keyspace.Join("org", "team", "member")
// "org:team:member"

Complete Example

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/xraph/grove/kv"
    "github.com/xraph/grove/kv/codec"
    "github.com/xraph/grove/kv/keyspace"
    "github.com/xraph/grove/kv/drivers/redisdriver"
)

type User struct {
    ID    string `json:"id" msgpack:"id"`
    Name  string `json:"name" msgpack:"name"`
    Email string `json:"email" msgpack:"email"`
    Role  string `json:"role" msgpack:"role"`
}

func main() {
    ctx := context.Background()

    rdb := redisdriver.New()
    if err := rdb.Open(ctx, "redis://localhost:6379/0"); err != nil {
        log.Fatal(err)
    }

    store, err := kv.Open(rdb)
    if err != nil {
        log.Fatal(err)
    }
    defer store.Close()

    // Create a typed keyspace with a 24-hour default TTL and MsgPack codec.
    users := keyspace.New[User](store, "users",
        keyspace.WithTTL(24*time.Hour),
        keyspace.WithCodec(codec.MsgPack()),
    )

    // Set.
    alice := User{ID: "u_1", Name: "Alice", Email: "alice@example.com", Role: "admin"}
    if err := users.Set(ctx, alice.ID, alice); err != nil {
        log.Fatal(err)
    }

    // Get -- returns a typed User directly.
    user, err := users.Get(ctx, "u_1")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Name: %s, Role: %s\n", user.Name, user.Role)

    // GetEntry -- includes metadata.
    entry, err := users.GetEntry(ctx, "u_1")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Key: %s, TTL: %s\n", entry.Key, entry.TTL)

    // Scan all user keys.
    err = users.Scan(ctx, "*", func(key string) error {
        fmt.Println("found user:", key)
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }
}

On this page