Skip to content

Data model & keyspace

fdyno maps DynamoDB's data model onto FoundationDB's ordered key-value space using the directory and tuple layers. Each table gets its own directory; within it, every kind of record lives in a distinct subspace.

Keyspace layout

dynodb/<table>/
  ├── m                                → table metadata (JSON schema)
  ├── i/<hash>[/<range>]               → item (binary codec)
  ├── c/<hash>[/<range>]/<chunk_idx>   → large-item chunks (> 10 KB)
  ├── x/<index>/<hash>/<sort>/<basePK> → secondary index entry (GSI or LSI)
  └── v/<versionstamp>                 → change-stream (CDC) record
flowchart LR
    T["dynodb/&lt;table&gt;/"]
    T --> M["m · metadata"]
    T --> I["i · items"]
    T --> C["c · large-item chunks"]
    T --> X["x · secondary indexes"]
    T --> V["v · change stream"]

Keys: the tuple layer

DynamoDB keys can be strings (S), numbers (N), or binary (B), and queries must return them in the correct sorted order. fdyno encodes keys with FoundationDB tuples, which are byte-ordered so that lexicographic comparison of the encoded bytes matches DynamoDB's type-aware ordering, so keys need no manual padding or delimiter escaping. A composite (hash + range) key becomes a two-element tuple; range queries become FoundationDB range reads.

Values: a binary codec

Item attributes are serialized with a compact, type-tagged, deterministic binary codec (codec.go) that supports all ten DynamoDB attribute types (S, N, B, BOOL, NULL, M, L, SS, NS, BS). Items under ~10 KB are stored as a single FoundationDB value.

Chunked large items

FoundationDB values have a size limit, so items larger than ~10 KB are split. The primary value becomes a chunked:N manifest and the payload is written across c/.../<chunk_idx> keys. Reads fetch all chunks in parallel using FoundationDB futures and reassemble them.

flowchart LR
    W["PutItem (large)"] --> M["i/&lt;pk&gt; = chunked:N"]
    W --> C0["c/&lt;pk&gt;/0"]
    W --> C1["c/&lt;pk&gt;/1"]
    W --> Cn["c/&lt;pk&gt;/…N-1"]

Secondary indexes

Global (GSI) and local (LSI) secondary indexes share one subspace (x/) and one write path. An index entry's key is x/<index>/<indexHashValue>/<indexSortValue>/<basePK>, where the base table's primary key is appended as a tiebreaker so that multiple items with the same index value never collide.

The key property: index entries are written in the same FoundationDB transaction as the base item. Indexes are not maintained asynchronously, so there is no propagation delay and no window in which an index disagrees with the base table. In DynamoDB, global secondary indexes are eventually consistent.

flowchart TB
    P["PutItem / UpdateItem / DeleteItem"]
    subgraph txn["single FoundationDB transaction"]
      B["base item (i/)"]
      G["GSI entries (x/)"]
      L["LSI entries (x/)"]
      S["change record (v/)"]
    end
    P --> txn

See Transactions & consistency for what this guarantees and Change streams for the v/ subspace.