Architecture overview¶
fdyno is a thin, stateless HTTP service that speaks the DynamoDB wire protocol and stores everything in FoundationDB. There is no in-memory database and no local persistence: FoundationDB is the single source of truth.
The big picture¶
flowchart TB
subgraph client[Client]
SDK["AWS DynamoDB SDK"]
end
subgraph fdyno[fdyno · stateless Go layer]
direction TB
R["service.go<br/>router · SigV4 · CORS · limits"]
A["http.go<br/>decode → call → encode"]
O["*_ops.go<br/>operation logic"]
E["expressions.go / partiql.go<br/>expression & query engine"]
S["fdb_store.go<br/>keyspace · codec · indexes"]
R --> A --> O --> E
O --> S
end
FDB[("FoundationDB<br/>ACID · tuple keys · versionstamps")]
SDK -- "POST x-amz-json-1.0" --> R
S -- "one transaction per request" --> FDB
Every durable thing (items, table schema, secondary indexes, change-stream
records, backups, tags, TTL configuration, and transaction idempotency tokens)
is read and written inside per-request FoundationDB transactions. The Service
struct holds no durable metadata; its only in-memory maps are process-local
coordination (tag-mutation leases, a positive-existence cache, static
compatibility config) and are safe to lose on restart.
The result: instances are effectively stateless. TTL configuration, tags, and idempotency tokens live in FoundationDB, so a restart preserves them, and multiple instances behind a load balancer share one consistent view.
Request flow¶
sequenceDiagram
participant C as Client SDK
participant R as service.go (router)
participant H as http.go (adapter)
participant Op as *_ops.go
participant DB as FoundationDB
C->>R: POST / (X-Amz-Target: DynamoDB_20120810.PutItem)
R->>R: verify SigV4, size limits, CORS
R->>H: dispatch by action
H->>H: decode JSON body → typed input
H->>Op: PutItem(in)
Op->>Op: validate input
Op->>DB: transaction: write item + indexes + CDC record
DB-->>Op: commit
Op-->>H: result or *ServiceError
H-->>C: JSON response / typed error
- An HTTP
POSTarrives;service.goextracts the action from theX-Amz-Targetheader, verifies the SigV4 signature, and enforces request/header size limits. - The request is dispatched to a thin adapter in
http.gothat decodes the JSON body into a typed input struct. - The adapter calls an operation method (
*_ops.go) that knows nothing about HTTP. - The operation validates its input and opens a FoundationDB transaction.
- Inside that one transaction, the base item, every affected index, and (if the table has a stream) a change record are written atomically.
Onion architecture¶
Business logic is transport-agnostic. Operations return typed results and errors; the HTTP layer is a thin adapter that decodes, calls, and encodes.
// Operation (in *_ops.go): no HTTP knowledge.
func (s *Service) PutItem(in putItemInput) (*putItemResult, error)
// HTTP adapter (in http.go): thin decode / encode.
func (s *Service) httpPutItem(w http.ResponseWriter, r *http.Request) {
var in putItemInput
if !decodeJSON(w, r, &in) { return }
res, err := s.PutItem(in)
if err != nil { writeServiceError(s, w, err); return }
writeJSON(w, http.StatusOK, res)
}
Every DynamoDB action (control-plane, data-plane, and Streams) follows this
pattern. Errors are returned as a typed *ServiceError (code + message, plus an
optional item for ReturnValuesOnConditionCheckFailure), which the HTTP layer
maps to the correct status code and error JSON. Because operations are callable
without constructing an HTTP request, they are directly unit-testable.
Code layout¶
cmd/
dynodb/main.go Server entry point
compatbench/main.go Differential benchmark vs DynamoDB Local
internal/dynodb/
HTTP layer service.go (router), http.go (decode → call → encode)
Operation logic item_ops, table_ops, query_ops, batch_ops, txn_ops,
partiql_ops, controlplane_ops, backup_ops, streams
Domain logic expressions (condition/update/filter/projection engine),
partiql (lexer + recursive-descent parser), reserved words
Shared utilities validation, projection, clone, capacity, number, helpers
Persistence fdb_store (keyspace, chunking, indexes),
fdb_persistence (schema), codec (binary item serialization),
idempotency (transaction tokens)
Types types.go, codes.go
The expression engine¶
expressions.go implements DynamoDB's expression grammar with a single
evaluator shared across condition, filter, update, and projection contexts:
- ConditionExpression: boolean logic (
AND/OR/NOT), comparisons,BETWEEN,IN, and functions (attribute_exists,attribute_not_exists,begins_with,contains,size,attribute_type). - UpdateExpression:
SET(nested paths,list_append,if_not_exists, arithmetic),REMOVE,ADD(numbers and sets),DELETE(set elements). - FilterExpression: the same engine, applied after a query or scan.
- ProjectionExpression: nested path and list-index resolution.
The PartiQL parser¶
partiql.go implements DynamoDB's PartiQL dialect in three stages: a tokenizer, a
recursive-descent parser, and an executor.
- Lexer: a single-pass O(n) scan that produces typed tokens. Keywords are resolved to dedicated token kinds during lexing, so parsing works directly on the typed tokens.
- Parser: recursive descent over the token stream, producing a typed AST.
- Executor: walks the AST, builds the corresponding FoundationDB query, and applies filters.
Continue to Data model & keyspace to see how items and indexes are laid out in FoundationDB.