Skip to content

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:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install boto3

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:

python quickstart.py

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.
  • PutItem and UpdateItem each used a FoundationDB transaction. The condition was evaluated against the item read in that transaction; the stale write returned before commit.
  • TransactWriteItems committed the profile update, event insert, both stream records, and the ClientRequestToken fingerprint 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. DeleteTable removed 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