Skip to content

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
  1. An HTTP POST arrives; service.go extracts the action from the X-Amz-Target header, verifies the SigV4 signature, and enforces request/header size limits.
  2. The request is dispatched to a thin adapter in http.go that decodes the JSON body into a typed input struct.
  3. The adapter calls an operation method (*_ops.go) that knows nothing about HTTP.
  4. The operation validates its input and opens a FoundationDB transaction.
  5. 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.

  1. 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.
  2. Parser: recursive descent over the token stream, producing a typed AST.
  3. 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.