2026-02-21 20:50:14 -05:00
2026-02-21 20:50:14 -05:00
2026-02-15 11:17:12 -05:00
2026-02-15 13:56:08 -05:00
2026-02-15 12:42:55 -05:00
2026-02-17 02:03:40 -05:00
2026-02-17 14:54:29 -05:00
2026-02-21 20:50:14 -05:00
2026-02-21 19:17:36 -05:00
2026-02-15 08:55:22 -05:00
2026-02-21 19:17:36 -05:00
2026-02-21 19:14:00 -05:00
2026-02-17 15:02:57 -05:00

JormunDB logo

JormunDB

A high-performance, DynamoDB-compatible database server written in Odin, backed by RocksDB.
DynamoDB-Compatible Database · Powered by RocksDB + Odin


What is JormunDB?

JormunDB is a Self-Hosted DynamoDB replacement that speaks the DynamoDB wire protocol. Point your AWS SDK or CLI at it and use it as a drop-in replacement.

Why Odin? The original Zig implementation suffered from explicit allocator threading. Where every function ended up needing an allocator parameter and every allocation needed errdefer cleanup. Odin's implicit context allocator system eliminates this ceremony. Just one context.allocator = arena_allocator at the request handler entry and it feels more like working with ctx in Go instead of filling out tax forms.

Features

  • DynamoDB Wire Protocol: Works with AWS SDKs and CLI out of the box
  • Binary Storage: Efficient TLV encoding for items, varint-prefixed keys
  • Arena-per-Request: Zero explicit memory management in business logic
  • Table Operations: CreateTable, DeleteTable, DescribeTable, ListTables
  • Item Operations: PutItem, GetItem, DeleteItem
  • Query & Scan: With pagination support (Limit, ExclusiveStartKey)
  • Expression Parsing: KeyConditionExpression for Query operations
  • Persistent Storage: RocksDB-backed with full ACID guarantees
  • Concurrency: Table-level RW locks for safe concurrent access

Quick Start

Prerequisites

  • Odin compiler (latest)
  • RocksDB development libraries
  • Standard compression libraries (snappy, lz4, zstd, etc.)

macOS (Homebrew)

brew install rocksdb odin

Ubuntu/Debian

sudo apt install librocksdb-dev libsnappy-dev liblz4-dev libzstd-dev libbz2-dev
# Install Odin from https://odin-lang.org/docs/install/

Build & Run

# Build the server
make build

# Run with default settings (localhost:8002, ./data directory)
make run

# Run with custom port
make run PORT=9000

# Run with custom data directory
make run DATA_DIR=/tmp/jormundb

Test with AWS CLI

# Create a table
aws dynamodb create-table \
    --endpoint-url http://localhost:8002 \
    --table-name Users \
    --key-schema AttributeName=id,KeyType=HASH \
    --attribute-definitions AttributeName=id,AttributeType=S \
    --billing-mode PAY_PER_REQUEST

# Put an item
aws dynamodb put-item \
    --endpoint-url http://localhost:8002 \
    --table-name Users \
    --item '{"id":{"S":"user123"},"name":{"S":"Alice"},"age":{"N":"30"}}'

# Get an item
aws dynamodb get-item \
    --endpoint-url http://localhost:8002 \
    --table-name Users \
    --key '{"id":{"S":"user123"}}'

# Query items
aws dynamodb query \
    --endpoint-url http://localhost:8002 \
    --table-name Users \
    --key-condition-expression "id = :id" \
    --expression-attribute-values '{":id":{"S":"user123"}}'

# Scan table
aws dynamodb scan \
    --endpoint-url http://localhost:8002 \
    --table-name Users

Architecture

HTTP Request (POST /)
  ↓
X-Amz-Target header → Operation routing
  ↓
JSON body → DynamoDB types
  ↓
Storage engine → RocksDB operations
  ↓
Binary encoding → Disk
  ↓
JSON response → Client

Storage Format

Keys (varint-length-prefixed segments):

Meta:  [0x01][len][table_name]
Data:  [0x02][len][table_name][len][pk_value][len][sk_value]?
GSI:   [0x03][len][table_name][len][index_name][len][gsi_pk][len][gsi_sk]?
LSI:   [0x04][len][table_name][len][index_name][len][pk][len][lsi_sk]

Values (TLV binary encoding):

[attr_count:varint]
  [name_len:varint][name:bytes][type_tag:u8][value_encoded:bytes]...

Type tags:
  String=0x01, Number=0x02, Binary=0x03, Bool=0x04, Null=0x05
  SS=0x10, NS=0x11, BS=0x12
  List=0x20, Map=0x21

Memory Management

JormunDB uses Odin's context allocator system for elegant memory management:

// Request handler entry point
handle_request :: proc(conn: net.TCP_Socket) {
    arena: mem.Arena
    mem.arena_init(&arena, make([]byte, mem.Megabyte * 4))
    defer mem.arena_destroy(&arena)

    context.allocator = mem.arena_allocator(&arena)

    // Everything below uses the arena automatically
    // No manual frees, no errdefer cleanup needed
    request := parse_request()  // Uses context.allocator
    response := process(request) // Uses context.allocator
    send_response(response)      // Uses context.allocator

    // Arena is freed here automatically
}

Long-lived data (table metadata, locks) uses the default allocator. Request-scoped data uses the arena.

Development

# Build debug version
make build

# Build optimized release
make release

# Run tests
make test

# Format code
make fmt

# Clean build artifacts
make clean

# Run with custom settings
make run PORT=9000 DATA_DIR=/tmp/db VERBOSE=1

Performance

Benchmarked on single node localhost, 1000 iterations per test.

Basic Operations

Operation Throughput Avg Latency P95 Latency P99 Latency
PutItem 1,021 ops/sec 0.98ms 1.02ms 1.64ms
GetItem 1,207 ops/sec 0.83ms 0.90ms 1.14ms
Query 1,002 ops/sec 1.00ms 1.11ms 1.85ms
Scan (100 items) 18,804 ops/sec 0.05ms - -
DeleteItem 1,254 ops/sec 0.80ms - -

Batch Operations

Operation Throughput Batch Size
BatchWriteItem 9,297 ops/sec 25 items
BatchGetItem 9,113 ops/sec 25 items

Concurrent Operations

Workers Throughput Avg Latency P95 Latency P99 Latency
10 concurrent 1,286 ops/sec 7.70ms 15.16ms 19.72ms

Large Payloads

Payload Size Throughput Avg Latency
10KB 522 ops/sec 1.91ms
50KB 166 ops/sec 6.01ms
100KB 96 ops/sec 10.33ms

API Compatibility

Supported Operations

  • CreateTable
  • DeleteTable
  • DescribeTable
  • ListTables
  • PutItem
  • GetItem
  • DeleteItem
  • Query (with KeyConditionExpression)
  • Scan (with pagination)
  • ConditionExpression
  • FilterExpression
  • ProjectionExpression
  • BatchWriteItem
  • BatchGetItem
  • Global Secondary Indexes

Coming Soon

  • UpdateItem (works but needs UPDATED_NEW/UPDATED_OLD response filtering to work for full Dynamo Parity)
  • Local Secondary Indexes

Configuration

Environment Variables

JORMUN_PORT=8002           # Server port (I have something locally on port 8000 so now everyone has to use port 8002)
JORMUN_HOST=0.0.0.0        # Bind address
JORMUN_DATA_DIR=./data     # RocksDB data directory
JORMUN_VERBOSE=1           # Enable verbose logging

Command Line Arguments

./jormundb --port 9000 --host 127.0.0.1 --data-dir /var/db --verbose

Troubleshooting

"Cannot open RocksDB"

Ensure RocksDB libraries are installed and the data directory is writable:

# Check RocksDB installation
pkg-config --libs rocksdb

# Check permissions
mkdir -p ./data
chmod 755 ./data

"Connection refused"

Check if the port is already in use:

lsof -i :8002

"Invalid JSON" errors

Ensure you're using the correct DynamoDB JSON format:

{
  "TableName": "Users",
  "Item": {
    "id": {"S": "user123"},
    "age": {"N": "30"}
  }
}

Credits

  • Inspired by DynamoDB
  • Built with Odin
  • Powered by RocksDB
  • Originally implemented as ZynamoDB in Zig

Contributing

Contributions welcome! Please:

  1. Format code with make fmt
  2. Run tests with make test
  3. Update documentation as needed
  4. Follow Odin idioms (context allocators, explicit returns, etc.)

Why "Jormun"? Jörmungandr, the World Serpent from Norse mythology, which I found fitting for something built in a language called Odin. Also, it sounds cool.

Description
No description provided
Readme 1.6 MiB
Languages
Odin 96%
Makefile 1.6%
C++ 1.3%
Shell 0.6%
C 0.5%