---
title: DynamoDB Driver
description: AWS DynamoDB driver for Grove KV with TTL, Batch, CAS, and Transaction support.
---

The DynamoDB driver connects Grove KV to [AWS DynamoDB](https://aws.amazon.com/dynamodb/) using the [AWS SDK for Go v2](https://github.com/aws/aws-sdk-go-v2). It stores values as binary attributes and supports TTL via DynamoDB's native time-to-live feature.

## Installation

```bash
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:

```go
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

| Capability | Supported |
|------------|-----------|
| TTL | Yes (native DynamoDB TTL) |
| CAS (SetNX/SetXX) | Yes |
| Scan | No (use Query/ScanTable for advanced access) |
| Batch | Yes (BatchGetItem/BatchWriteItem) |
| PubSub | No |
| Transactions | Yes (TransactGet/TransactWrite) |
| Streams | No |

## Table Schema Requirements

Your DynamoDB table must have the following attribute schema:

| Attribute | DynamoDB Type | Description |
|-----------|---------------|-------------|
| `pk` | `S` (String) | Partition key -- stores the KV key |
| `val` | `B` (Binary) | Stores the encoded value bytes |
| `ttl` | `N` (Number) | Unix timestamp for DynamoDB TTL expiration |

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

```bash
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:

```bash
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:

```go
// 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

```go
// 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:

```go
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:

```go
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)

```go
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

```go
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:

```go
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`:

```go
ddb := dynamodriver.New(client,
    dynamodriver.WithPKAttribute("pk"),
    dynamodriver.WithValueAttribute("val"),
    dynamodriver.WithTTLAttribute("ttl"),
)
```

| Option | Default | Description |
|--------|---------|-------------|
| `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:

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

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

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