Grove
KV Drivers

DynamoDB Driver

AWS DynamoDB driver for Grove KV with TTL, Batch, CAS, and Transaction support.

The DynamoDB driver connects Grove KV to AWS DynamoDB using the AWS SDK for Go v2. It stores values as binary attributes and supports TTL via DynamoDB's native time-to-live feature.

Installation

go get github.com/xraph/grove/kv
go get github.com/xraph/grove/kv/drivers/dynamodriver
go get github.com/aws/aws-sdk-go-v2/config

Connection

The DynamoDB driver requires a pre-configured *dynamodb.Client. The DSN passed to Open is used as the table name:

import (
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb"
    "github.com/xraph/grove/kv"
    "github.com/xraph/grove/kv/drivers/dynamodriver"
)

// Load AWS config
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
if err != nil {
    log.Fatal(err)
}

// Create the DynamoDB client
client := dynamodb.NewFromConfig(cfg)

// Create driver with options and open the store
// The DSN is the table name
store, err := kv.Open(
    dynamodriver.New(client, dynamodriver.WithPKAttribute("pk")),
    "my-kv-table",
)
if err != nil {
    log.Fatal(err)
}
defer store.Close()

Capabilities

CapabilitySupported
TTLYes (native DynamoDB TTL)
CAS (SetNX/SetXX)Yes
ScanNo (use Query/ScanTable for advanced access)
BatchYes (BatchGetItem/BatchWriteItem)
PubSubNo
TransactionsYes (TransactGet/TransactWrite)
StreamsNo

Table Schema Requirements

Your DynamoDB table must have the following attribute schema:

AttributeDynamoDB TypeDescription
pkS (String)Partition key -- stores the KV key
valB (Binary)Stores the encoded value bytes
ttlN (Number)Unix timestamp for DynamoDB TTL expiration

The attribute names are configurable via options. Create your table with the partition key attribute:

aws dynamodb create-table \
    --table-name my-kv-table \
    --attribute-definitions AttributeName=pk,AttributeType=S \
    --key-schema AttributeName=pk,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST

Enable TTL on the table:

aws dynamodb update-time-to-live \
    --table-name my-kv-table \
    --time-to-live-specification "Enabled=true,AttributeName=ttl"

TTL

DynamoDB TTL is handled natively. When you set a key with a TTL, the driver stores a Unix timestamp in the ttl attribute. DynamoDB automatically deletes expired items (typically within 48 hours of expiration). The driver also checks the TTL value on reads and returns kv.ErrNotFound for expired items immediately:

// Set with a 1-hour TTL
err := store.Set(ctx, "session:abc", data, 1*time.Hour)

// Check remaining TTL
remaining, err := store.TTL(ctx, "session:abc")

// Update TTL on an existing key
err = store.Expire(ctx, "session:abc", 2*time.Hour)

Batch Operations

The driver supports MGet and MSet via DynamoDB's BatchGetItem and BatchWriteItem APIs. The driver automatically handles DynamoDB's per-request limits by chunking:

  • BatchGetItem: max 100 keys per request
  • BatchWriteItem: max 25 items per request
// Batch get
results := make(map[string]any)
err := store.MGet(ctx, []string{"key1", "key2", "key3"}, results)

// Batch set
err = store.MSet(ctx, map[string]any{
    "key1": value1,
    "key2": value2,
    "key3": value3,
}, kv.WithTTL(1*time.Hour))

Transactions

The driver provides DynamoDB's transactional APIs for atomic multi-item operations:

TransactGet

Read multiple items atomically:

ddb := dynamodriver.Unwrap(store)

items, err := ddb.TransactGet(ctx, []string{"account:1", "account:2"})
for _, item := range items {
    // Process each item's attributes
}

TransactWrite

Write, delete, update, or condition-check multiple items atomically:

ddb := dynamodriver.Unwrap(store)

err := ddb.TransactWrite(ctx, []dynamodriver.TransactWriteItem{
    {
        Put: &dynamodriver.TransactPut{
            Item: map[string]types.AttributeValue{
                "pk":  &types.AttributeValueMemberS{Value: "order:123"},
                "val": &types.AttributeValueMemberB{Value: orderBytes},
            },
        },
    },
    {
        Delete: &dynamodriver.TransactDelete{
            Key: map[string]types.AttributeValue{
                "pk": &types.AttributeValueMemberS{Value: "cart:user1"},
            },
        },
    },
    {
        ConditionCheck: &dynamodriver.TransactConditionCheck{
            Key: map[string]types.AttributeValue{
                "pk": &types.AttributeValueMemberS{Value: "inventory:sku-99"},
            },
            ConditionExpression: "quantity > :zero",
            ExpressionValues: map[string]types.AttributeValue{
                ":zero": &types.AttributeValueMemberN{Value: "0"},
            },
        },
    },
})

Query and ScanTable

For access patterns that go beyond simple key-value lookups (e.g., querying GSIs or LSIs), use Query and ScanTable:

Query (GSI/LSI)

ddb := dynamodriver.Unwrap(store)

result, err := ddb.Query(ctx, dynamodriver.QueryInput{
    IndexName:              "gsi-status-created",
    KeyConditionExpression: "#status = :status AND #created > :since",
    ExpressionAttributeNames: map[string]string{
        "#status":  "status",
        "#created": "created_at",
    },
    ExpressionValues: map[string]types.AttributeValue{
        ":status": &types.AttributeValueMemberS{Value: "active"},
        ":since":  &types.AttributeValueMemberN{Value: "1700000000"},
    },
    Limit: aws.Int32(50),
})

for _, item := range result.Items {
    // Process each item
}

ScanTable

ddb := dynamodriver.Unwrap(store)

result, err := ddb.ScanTable(ctx, dynamodriver.ScanInput{
    FilterExpression: "#type = :type",
    ExpressionAttributeNames: map[string]string{
        "#type": "entity_type",
    },
    ExpressionValues: map[string]types.AttributeValue{
        ":type": &types.AttributeValueMemberS{Value: "user"},
    },
})

UpdateBuilder

The UpdateBuilder provides a fluent API for building DynamoDB UpdateItem expressions:

ddb := dynamodriver.Unwrap(store)

err := ddb.Update("user:123").
    Set("email", &types.AttributeValueMemberS{Value: "new@example.com"}).
    Set("updated_at", &types.AttributeValueMemberN{Value: "1700000000"}).
    Remove("temp_field").
    Condition("attribute_exists(#a0)").
    Exec(ctx)

Options

Configure the driver when calling dynamodriver.New:

ddb := dynamodriver.New(client,
    dynamodriver.WithPKAttribute("pk"),
    dynamodriver.WithValueAttribute("val"),
    dynamodriver.WithTTLAttribute("ttl"),
)
OptionDefaultDescription
dynamodriver.WithPKAttribute(attr)"pk"Name of the partition key attribute
dynamodriver.WithValueAttribute(attr)"val"Name of the attribute storing the value bytes
dynamodriver.WithTTLAttribute(attr)"ttl"Name of the attribute storing the TTL Unix timestamp

Unwrap for Native Client

Access the underlying DynamoDB client for operations not covered by the Grove KV interface:

// Get the DynamoDB driver
ddb := dynamodriver.Unwrap(store)

// Get the table name
tableName := ddb.Table()

// Get the AWS DynamoDB client
client := dynamodriver.UnwrapClient(store)

On this page