FaceTheory ISR Transaction Recipes (Metadata + Pointer Swap)
This document describes correctness-first transaction patterns for ISR regeneration when using:
- a metadata item (
sk = "META") and - a lease item (
sk = "LOCK")
as defined in docs/facetheory/isr-cache-schema.md.
The goal is to ensure:
- only the current lock holder can publish new metadata
- stale writers (expired lock, stolen lock) cannot overwrite newer state
- releasing the lock is tied to publishing metadata (where appropriate)
Recipe A: In-place metadata publish (single META row)
Use this when you only need one metadata record per cache key.
Steps
- Acquire the lease (
LOCK) forpk. - Regenerate the body and write it to S3.
- Publish metadata and release the lock with a single DynamoDB transaction:
Putthe metadata row (META) with the new pointer (s3_key), timestamps, etag, ttlDeletethe lease row (LOCK) with a condition expression (lease_tokenmatches ANDlease_expires_at > now)
Note: DynamoDB transactions cannot include multiple operations on the same item. Prefer a conditional Delete/Update
on the lease row over a separate ConditionCheck + Delete on the same lease key.
Why the transaction matters
Without the transaction, a writer can:
- finish regeneration after its lease expires, and
- overwrite the metadata even though another contender acquired the lease.
The transaction makes publishing contingent on still owning the lease.
Recipe B: Pointer swap (versioned metadata rows)
Use this when you want history, safer rollbacks, or deduping/observability across generations.
Recommended item roles (same pk):
- pointer row:
sk = "META"withcurrent_sk = "VER#<id>" - version rows:
sk = "VER#<id>"containings3_key,generated_at,etag, etc.
Transaction sketch:
Putthe new version row (VER#...) guarded withattribute_not_exists(pk)to avoid duplicate IDs.Updatethe pointer row (META) to setcurrent_skto the new version (optionally guard with optimisticversion).Deletethe lease row (LOCK) with a condition expression (token + not expired).
Stale-writer protection options
You can guard against stale writers in one (or multiple) of these ways:
- Lease token check (recommended): the lease row stores
lease_tokenand transactions require it. - Lease expiry check: require
lease_expires_at > nowin the transaction. - Optimistic version check: add a
versionfield to the pointer row and require it matches before updating. - Monotonic timestamp check: conditionally update only if
generated_at < :new_generated_at.
ETag update patterns
Recommended: publish etag alongside the pointer update.
- In Recipe A, store
etagdirectly onMETAand overwrite it atomically in the transaction. - In Recipe B, store
etagon the version row and optionally denormalize the currentetagonto the pointer row for faster reads.
Stale-writer guard: if you rely on monotonicity, require generated_at < :new_generated_at before setting a new etag.
Multi-language examples
These examples focus on the transaction shape; model definitions are in docs/facetheory/isr-cache-schema.md.
Go (TableTheory transaction builder)
ctx := context.Background()
nowUnix := time.Now().Unix()
leaseItem := &models.FaceTheoryCacheLease{
PK: "TENANT#t1#CACHE#abc",
SK: "LOCK",
}
metaItem := &models.FaceTheoryCacheMetadata{
PK: leaseItem.PK,
SK: "META",
S3Key: "s3://bucket/key.html",
GeneratedAt: nowUnix,
RevalidateSeconds: 60,
ETag: "\"abc123\"",
TTL: nowUnix + 86400,
}
err := db.TransactWrite(ctx, func(tx core.TransactionBuilder) error {
tx.Put(metaItem)
tx.Delete(
leaseItem,
tabletheory.Condition("lease_token", "=", leaseToken),
tabletheory.Condition("lease_expires_at", ">", nowUnix),
)
return nil
})
TypeScript (TheorydbClient.transactWrite)
await client.transactWrite([
{
kind: 'put',
model: 'FaceTheoryCacheMetadata',
item: { pk, sk: 'META', s3_key, generated_at: nowUnix, revalidate_seconds, etag, ttl },
},
{
kind: 'delete',
model: 'FaceTheoryCacheLease',
key: { pk, sk: 'LOCK' },
conditionExpression: '#tok = :tok AND #exp > :now',
expressionAttributeNames: { '#tok': 'lease_token', '#exp': 'lease_expires_at' },
expressionAttributeValues: {
':tok': { S: leaseToken },
':now': { N: String(nowUnix) },
},
},
]);
TypeScript (TableTheory helper: FaceTheoryIsrMetaStore)
TableTheory exports a small helper that implements the FaceTheory ISR metadata + lease operations using:
LeaseManagerfor acquiring/releasingLOCKrowsTheorydbClient.transactWrite()for atomic “publish META + release LOCK” (Recipe A)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { createFaceTheoryIsrMetaStore } from '@theory-cloud/tabletheory-ts';
const ddb = new DynamoDBClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
const isr = createFaceTheoryIsrMetaStore({
ddb,
tableName: process.env.FACETHEORY_CACHE_TABLE_NAME!,
});
const lease = await isr.tryAcquireLease({
cacheKey,
leaseOwner: 'my-app-instance', // optional (for app logs)
leaseDurationMs: 30_000,
});
if (!lease) return; // another contender is regenerating
await isr.commitGeneration({
cacheKey,
leaseOwner: 'my-app-instance', // optional (for app logs)
leaseToken: lease.leaseToken,
htmlPointer: s3Key,
generatedAtMs: Date.now(),
revalidateSeconds: 60,
etag,
});
Python (Table.transact_write)
table.transact_write(
[
TransactPut(
item=FaceTheoryCacheMetadata(
pk=pk,
sk="META",
s3_key=s3_key,
generated_at=now_unix,
revalidate_seconds=revalidate_seconds,
etag=etag,
ttl=ttl,
),
),
TransactDelete(
pk=pk,
sk="LOCK",
condition_expression="#tok = :tok AND #exp > :now",
expression_attribute_names={"#tok": "lease_token", "#exp": "lease_expires_at"},
expression_attribute_values={":tok": lease_token, ":now": now_unix},
),
]
)