Architecture overview¶
fdyno is a DynamoDB-protocol service over FoundationDB. The Go process validates and translates requests; FoundationDB stores the durable database state and supplies the transactional concurrency control. There is no in-process item store and no local write-ahead log.
This split keeps DynamoDB request semantics, expressions, key layout, and response compatibility in fdyno. FoundationDB provides replication and recovery.
System map¶
flowchart TB
SDK["AWS SDK<br/>signed HTTP"]
subgraph SVC["fdyno process"]
direction TB
EDGE["HTTP boundary<br/>SigV4 · routing"]
OPS["DynamoDB semantics<br/>JSON · expressions · PartiQL"]
STORE["FDB mapping<br/>tuples · codec · indexes"]
WORKERS["background workers<br/>TTL · streams · recovery"]
EDGE --> OPS --> STORE
WORKERS --> STORE
end
FDB[("FoundationDB<br/>transactions · ordered keys · versionstamps")]
SDK -->|"DynamoDB JSON"| EDGE
STORE -->|"transactions"| FDB
The service has two kinds of work:
- Foreground API work translates one DynamoDB action into one or more FoundationDB transactions.
- Maintenance work uses bounded transactions for TTL reclamation, stream trimming, idempotency-token garbage collection, and resumable GSI backfill. TTL, stream-trim, and token-GC workers coordinate with FoundationDB leases; GSI recovery is instead safe to rerun idempotently and may do redundant work if several instances start together.
Request path¶
sequenceDiagram
participant C as Client
participant R as service.go
participant H as http.go / helpers.go
participant O as operation code
participant F as FoundationDB
C->>R: POST / + X-Amz-Target + SigV4
R->>R: deadline, method/path/header checks
R->>R: bound body and verify signature
R->>H: dispatch action
H->>H: decompress and decode typed JSON
H->>O: PutItem(input, request context)
O->>O: validate request and expressions
O->>F: transaction callback
F-->>O: write commit, retryable error, or terminal error
O-->>H: typed result or ServiceError
H-->>C: DynamoDB-shaped JSON + error headers/CRC
For a normal data-plane request, the path is:
ServeHTTPinstalls one operation deadline for the whole HTTP request. The default is 30 seconds andDYNODB_OPERATION_TIMEOUTcan change it.service.goaccepts DynamoDBPOST /requests, bounds request headers and the raw body, verifies SigV4, and selects the action fromX-Amz-Target.- The adapter decodes the request into an operation-specific type. Raw and decompressed request bodies are both bounded at 16 MiB.
- The operation performs DynamoDB validation, evaluates expressions, and opens a read-only or read-write FoundationDB transaction.
- A read-only operation returns after its required futures resolve. It does not commit an unmodified transaction. Its reads still use one transactional snapshot and the normal retry and deadline path.
- The persistence helpers map table metadata, item keys, values, indexes, and stream records to FoundationDB keys. A successful write response is produced only after commit completes.
- Typed domain errors become DynamoDB-shaped errors. Unexpected storage or codec failures become internal errors rather than partial successful responses.
Core item, query, batch, transaction, and PartiQL methods do not depend on HTTP. Tests
can call them directly. Some compatibility control-plane handlers remain in http.go.
Not every routed action has a separate *_ops.go method.
Request handling code¶
cmd/dynodb/main.go process startup, HTTP server, optional workers
internal/dynodb/service.go request deadline, routes, health, response capture
internal/dynodb/http.go typed HTTP adapters and compatibility handlers
internal/dynodb/*_ops.go item, query, batch, transaction, table, backup logic
internal/dynodb/expressions.go condition/update/filter expression evaluation
internal/dynodb/projection.go projection-path handling
internal/dynodb/partiql*.go lexer/parser and PartiQL execution
internal/dynodb/fdb_store.go tuple keys, chunks, indexes, item access
internal/dynodb/codec.go typed binary item values
internal/dynodb/transaction_runner.go typed read/write completion, retries, deadlines
The operation layer handles boolean conditions, comparison/functions, nested update
paths, numeric and set updates, filters, and projections independently of the HTTP
adapter and raw key format. PartiQL is parsed into a typed statement and executed
through the same item/index/stream helpers; transactional PartiQL uses one FoundationDB
transaction. Keeping these semantics above fdb_store.go prevents transport JSON or
raw key bytes from becoming the domain model.
Process state¶
The process-state rule is:
User database state needed after restart is stored in FoundationDB, not in a particular fdyno process.
| Stored in FoundationDB | Process-local and replaceable |
|---|---|
| Table schema, tags, policies, and TTL specification | Open connections and in-flight request buffers |
| Base items, item chunks, and synchronous index entries | Metrics counters |
| Stream records and large-record chunks | Positive existence and decoded table-metadata caches |
| Immutable backup generations and durable restore jobs | Tag-operation coordination maps |
| Transaction tokens and operation receipts | Credentials and feature configuration loaded from the environment |
| Worker leases and TTL sweep cursors | Alternator-compatible system-config rows |
The right-hand column matters. In particular, .scylla.alternator.system.config
updates are held in the Service.systemConfig map; they are not durable and are not
shared between instances. Metrics also reset on restart. These are compatibility or
operability surfaces, not table data.
The positive existence cache only speeds up selected control-plane checks.
A separate decoded table-metadata cache can supply speculative schema to
selected item paths and bounded base-table Query. The transaction checks
the metadata revision in FoundationDB before using a speculative result;
losing either cache does not lose durable table, item, index, stream, backup,
restore, receipt, tag, TTL, or transaction-token state. Every instance must
use the same credentials and feature configuration.
API transaction mapping¶
“One request equals one transaction” is true for the main item paths, but not for every workflow.
| API operation | FoundationDB mapping |
|---|---|
GetItem, one Query/Scan page, BatchGetItem, TransactGetItems |
One read transaction and one MVCC snapshot. An expired GetItem is treated as absent and then rechecked/deleted in a later cleanup transaction before the request completes. |
PutItem, UpdateItem, DeleteItem |
One read-write transaction containing the condition read, base-item mutation, index maintenance, and optional stream record. |
TransactWriteItems, PartiQL ExecuteTransaction |
One transaction across the selected items/tables. A client request token, when supported and supplied, is checked and written in that transaction. |
BatchWriteItem |
Tries one transaction. If the physical FoundationDB transaction is too large, the implementation validates the whole batch without committing, then uses bounded chunks and reports failures as UnprocessedItems; the DynamoDB API is non-atomic. |
| Add a GSI | One metadata transaction registers it as CREATING; bounded transactions backfill old items; a final transaction marks it ACTIVE. Live writes maintain the new index during backfill. |
| Backup, restore, TTL sweep, stream trim | Multi-transaction workflows with explicit visibility, cursor, or state rules rather than one unbounded transaction. |
See Transactions for retry and failure behavior and Consistency model for the guarantee attached to each boundary.
FoundationDB storage mapping¶
fdyno uses FoundationDB primitives instead of emulating them in memory:
- Directory layer: isolates each table and global service-owned keyspace.
- Tuple layer: creates unambiguous prefix ranges for partitions, indexes, and pagination cursors.
- Transactions: keep a condition read, base mutation, secondary-index entries, and stream record in one atomic boundary.
- Versionstamped keys: assign stream positions at commit, after the final commit order is known.
- Range reads: implement ordered partition queries and bounded pages without materializing a whole table in the service.
fdyno has one storage backend. Replacing FoundationDB would also require new ordering, conflict, and versionstamp behavior. Data model and keyspace defines the current key layout.
Failure behavior¶
- If validation or a condition fails, the transaction callback returns an error and no buffered writes commit.
- If FoundationDB reports a retryable transaction error, the transaction body can be executed again. Operation code must therefore keep per-attempt accumulators local and avoid irreversible side effects in callbacks.
- A process crash cannot expose half of a committed item/index/stream mutation: FoundationDB commits the transaction atomically or not at all.
- If FoundationDB is unreachable, data operations fail instead of falling back to a
local copy.
/readyzperforms a backend round trip and returns unavailable while liveness endpoints continue to report that the process itself is running. - A timeout while commit is in flight is explicitly treated as an unknown commit
outcome. Use a stable
ClientRequestTokenfor writes that must not be applied twice; see Reliable writes.
Not implemented¶
fdyno does not provide:
- a second replication layer, local durable cache, or offline-write mode;
- an eventually consistent read replica path; normal reads use FoundationDB
transactions even when the DynamoDB request does not set
ConsistentRead; - an unbounded transaction; large maintenance operations are paged or chunked;
- multi-Region/global-table replication at the fdyno layer;
- dynamic DynamoDB-style stream shard splitting;
- durable, cluster-wide Alternator system-config mutations.
Synchronous indexes and strict transactional reads remove propagation states, but put index work on the write path and make backend availability a prerequisite for serving data.
Code and tests¶
The statements above are grounded in these paths:
- Request boundary and routing:
internal/dynodb/service.go,internal/dynodb/auth.go,internal/dynodb/helpers.go,internal/dynodb/http.go - Operation deadline and retry loop:
internal/dynodb/transaction_runner.go - Service state boundary:
internal/dynodb/types.go,internal/dynodb/lease.go,internal/dynodb/ttl.go,internal/dynodb/idempotency.go - Storage mapping:
internal/dynodb/fdb_store.go,internal/dynodb/fdb_persistence.go,internal/dynodb/codec.go - Atomic item paths:
internal/dynodb/item_ops.go,internal/dynodb/txn_ops.go - Multi-transaction exceptions:
internal/dynodb/batch_ops.go,internal/dynodb/table_ops.go,internal/dynodb/backup_ops.go - Retry-attempt isolation test:
TestBatchGetRetryDoesNotLeakAttemptResultsininternal/dynodb/retry_safety_integration_test.go
The repository does not contain an end-to-end network-partition history checker; backend partition behavior comes from FoundationDB's transaction guarantees and is an evidence gap for fdyno-specific fault testing.