Quickstart¶
This local workflow starts FoundationDB and fdyno. One boto3 program creates a stream-enabled table, writes and reads an item, and queries a partition. It then runs a conditional update, commits a two-item transaction, reads stream records, and deletes the table.
Local evaluation only
The example binds fdyno to loopback and uses the built-in local/local
development credential. It is not a production authentication boundary.
Prerequisites¶
From a local clone of fdyno, you need:
- Go 1.27.1, as declared in
go.mod; - FoundationDB client and server packages, with a client library that supports API version 730; and
- Python 3.
Install FoundationDB using the
FoundationDB package instructions.
The usual cluster-file paths are /etc/foundationdb/fdb.cluster on Linux and
/usr/local/etc/foundationdb/fdb.cluster on macOS.
1. Start the database and service¶
In terminal 1, export the cluster file for your platform and verify FoundationDB:
export FDB_CLUSTER_FILE=/usr/local/etc/foundationdb/fdb.cluster # macOS
test -f "$FDB_CLUSTER_FILE"
fdbcli --exec "status minimal"
On Linux, use export FDB_CLUSTER_FILE=/etc/foundationdb/fdb.cluster instead. Do
not continue until status minimal reports the local cluster as available.
Then build and run fdyno on loopback:
go build -o dynodb ./cmd/dynodb
DYNODB_LISTEN_ADDR=127.0.0.1:8000 \
DYNODB_STREAM_SHARDS=1 \
DYNODB_CREDENTIALS= \
./dynodb
The empty credential variable selects the built-in development keys; the fixed one-shard setting makes the stream check below deterministic.
Leave that process running. In terminal 2, check both process liveness and backend readiness:
curl --fail --silent --show-error http://127.0.0.1:8000/livez
curl --fail --silent --show-error http://127.0.0.1:8000/readyz
The first command prints a healthy: line; the second prints ready. Both exit zero.
/livez does not touch FoundationDB; /readyz requests a FoundationDB read version
and returns HTTP 503 if that round trip fails.
2. Install boto3¶
In terminal 2, from the repository root:
3. Run the complete workflow¶
Save the following as quickstart.py. The table name is unique, and finally
deletes it even if a later check fails.
import time
import uuid
import boto3
from botocore.exceptions import ClientError
ENDPOINT = "http://127.0.0.1:8000"
TABLE = f"fdyno-quickstart-{uuid.uuid4().hex[:8]}"
CLIENT = {
"endpoint_url": ENDPOINT,
"region_name": "us-east-1",
"aws_access_key_id": "local",
"aws_secret_access_key": "local",
}
ddb = boto3.client("dynamodb", **CLIENT)
streams = boto3.client("dynamodbstreams", **CLIENT)
created = False
def check(condition, message):
if not condition:
raise RuntimeError(message)
try:
ddb.create_table(
TableName=TABLE,
AttributeDefinitions=[
{"AttributeName": "pk", "AttributeType": "S"},
{"AttributeName": "sk", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "pk", "KeyType": "HASH"},
{"AttributeName": "sk", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
StreamSpecification={
"StreamEnabled": True,
"StreamViewType": "NEW_AND_OLD_IMAGES",
},
)
created = True
ddb.get_waiter("table_exists").wait(TableName=TABLE)
table = ddb.describe_table(TableName=TABLE)["Table"]
check(table["TableStatus"] == "ACTIVE", "table did not become ACTIVE")
stream_arn = table.get("LatestStreamArn")
check(stream_arn, "table did not return LatestStreamArn")
description = streams.describe_stream(StreamArn=stream_arn)["StreamDescription"]
check(description["StreamStatus"] == "ENABLED", "stream is not enabled")
check(len(description["Shards"]) == 1, "quickstart expects the default one shard")
shard_id = description["Shards"][0]["ShardId"]
iterator = streams.get_shard_iterator(
StreamArn=stream_arn,
ShardId=shard_id,
ShardIteratorType="LATEST",
)["ShardIterator"]
print("create: ACTIVE, stream enabled")
profile_key = {"pk": {"S": "USER#1"}, "sk": {"S": "PROFILE"}}
ddb.put_item(
TableName=TABLE,
Item={
**profile_key,
"name": {"S": "Ada"},
"login_count": {"N": "1"},
},
ConditionExpression="attribute_not_exists(pk)",
)
profile = ddb.get_item(
TableName=TABLE,
Key=profile_key,
ConsistentRead=True,
).get("Item")
check(profile is not None, "GetItem did not return the profile")
check(profile["name"]["S"] == "Ada", "unexpected profile name")
check(profile["login_count"]["N"] == "1", "unexpected initial count")
print("put/get: Ada, login_count=1")
rows = ddb.query(
TableName=TABLE,
KeyConditionExpression="pk = :pk",
ExpressionAttributeValues={":pk": {"S": "USER#1"}},
ConsistentRead=True,
)["Items"]
check(len(rows) == 1 and rows[0]["sk"]["S"] == "PROFILE", "Query mismatch")
print("query: 1 item in USER#1")
updated = ddb.update_item(
TableName=TABLE,
Key=profile_key,
UpdateExpression="SET login_count = :next",
ConditionExpression="login_count = :expected",
ExpressionAttributeValues={
":expected": {"N": "1"},
":next": {"N": "2"},
},
ReturnValues="ALL_NEW",
)["Attributes"]
check(updated["login_count"]["N"] == "2", "conditional update mismatch")
print("update: login_count=2")
try:
ddb.update_item(
TableName=TABLE,
Key=profile_key,
UpdateExpression="SET login_count = :next",
ConditionExpression="login_count = :stale",
ExpressionAttributeValues={
":stale": {"N": "1"},
":next": {"N": "99"},
},
ReturnValuesOnConditionCheckFailure="ALL_OLD",
)
raise RuntimeError("stale conditional update unexpectedly succeeded")
except ClientError as error:
check(
error.response["Error"]["Code"] == "ConditionalCheckFailedException",
f"unexpected condition error: {error}",
)
check(
error.response["Item"]["login_count"]["N"] == "2",
"condition failure did not return the current item",
)
print("condition: stale value rejected, current value=2")
event_key = {"pk": {"S": "USER#1"}, "sk": {"S": "EVENT#1"}}
ddb.transact_write_items(
ClientRequestToken=str(uuid.uuid4()),
TransactItems=[
{
"Update": {
"TableName": TABLE,
"Key": profile_key,
"UpdateExpression": "SET login_count = :next",
"ConditionExpression": "login_count = :expected",
"ExpressionAttributeValues": {
":expected": {"N": "2"},
":next": {"N": "3"},
},
}
},
{
"Put": {
"TableName": TABLE,
"Item": {**event_key, "kind": {"S": "LOGIN"}},
"ConditionExpression": "attribute_not_exists(pk)",
}
},
],
)
result = ddb.transact_get_items(
TransactItems=[
{"Get": {"TableName": TABLE, "Key": profile_key}},
{"Get": {"TableName": TABLE, "Key": event_key}},
]
)["Responses"]
check(result[0]["Item"]["login_count"]["N"] == "3", "transaction update missing")
check(result[1]["Item"]["kind"]["S"] == "LOGIN", "transaction put missing")
print("transaction: login_count=3 and event=LOGIN")
records = []
deadline = time.monotonic() + 3
while len(records) < 4 and time.monotonic() < deadline:
page = streams.get_records(ShardIterator=iterator, Limit=100)
records.extend(page.get("Records", []))
iterator = page.get("NextShardIterator", iterator)
if len(records) < 4:
time.sleep(0.1)
event_names = [record["eventName"] for record in records]
check(len(event_names) == 4, f"expected 4 stream records, got {event_names}")
check(event_names.count("INSERT") == 2, f"unexpected stream events: {event_names}")
check(event_names.count("MODIFY") == 2, f"unexpected stream events: {event_names}")
print("stream: 4 records (2 INSERT, 2 MODIFY)")
finally:
if created:
ddb.delete_table(TableName=TABLE)
ddb.get_waiter("table_not_exists").wait(TableName=TABLE)
print("cleanup: table deleted")
Run it:
Expected output:
create: ACTIVE, stream enabled
put/get: Ada, login_count=1
query: 1 item in USER#1
update: login_count=2
condition: stale value rejected, current value=2
transaction: login_count=3 and event=LOGIN
stream: 4 records (2 INSERT, 2 MODIFY)
cleanup: table deleted
The generated table name does not affect this output. Any failed request or invariant raises an exception; cleanup still runs.
fdyno request flow¶
- Both boto3 clients signed ordinary DynamoDB requests. fdyno verified SigV4, decoded DynamoDB JSON, and dispatched the named actions.
PutItemandUpdateItemeach used a FoundationDB transaction. The condition was evaluated against the item read in that transaction; the stale write returned before commit.TransactWriteItemscommitted the profile update, event insert, both stream records, and theClientRequestTokenfingerprint in one FoundationDB transaction.- Each state-changing write stored its stream record beside the item mutation. A FoundationDB versionstamp assigned its sequence position at commit.
- The fdyno process retained no table data needed after restart.
DeleteTableremoved this table's schema, items, and stream records from FoundationDB. The transaction token remains usable for the 10-minute deduplication window and is reclaimed only when token garbage collection is enabled.
These statements map to the checked-in item, transaction, stream, and persistence paths summarized in Architecture.
Next steps¶
- Compatibility: Implemented workflows: classify the actions, request options, limits, and AWS integrations your application uses.
- Transactions → Idempotency with
ClientRequestToken: handle retries and unknown commit outcomes correctly. - Change streams → Consumer checkpoints: build a durable, idempotent consumer; this quickstart only reads one local shard.
- Deployment & operations → Security boundary: replace development credentials, add TLS and authorization controls, and test FoundationDB durability before any non-local use.