Features

Optimistic Locking

The version role is TableTheory’s optimistic concurrency field. The optimistic-locking P0 scenario pins the portable behavior: a write with the current version succeeds and moves the persisted item to the next version; a stale-version write fails with a typed condition error.

Public runtime ergonomics differ slightly, but the shared contract is the same: the high-level versioned update path adds the version condition and increments the version on success. On a version mismatch (another writer raced ahead), the write fails — never a silent overwrite.

Why this matters

Every Theory Cloud consumer that ships versioned writes — AppTheory’s idempotency state, Autheory’s session refreshes, theory-mcp-server’s agent memory cursor — depends on guarded updates being expressed consistently. A Go writer racing a Python writer must use the same expected-version value and see one of the writes fail deterministically.

Go

type Counter struct {
    PK      string `theorydb:"pk"      json:"pk"`
    SK      string `theorydb:"sk"      json:"sk"`
    Value   int64  `json:"value"`
    Version int64  `theorydb:"version" json:"version"`
}

var c Counter
if err := db.Model(&Counter{PK: "TENANT#42", SK: "COUNTER#impressions"}).First(&c); err != nil {
    return err
}

c.Value++
if err := db.Model(&c).Update("value"); err != nil {
    // Version-mismatch is surfaced as a typed error; check via the
    // pkg/errors helpers (see api-reference.md).
    return err
}

TypeScript

const Counter = defineModel({
  name: 'Counter',
  table: { name: 'counters_contract' },
  keys: {
    partition: { attribute: 'PK', type: 'S' },
    sort:      { attribute: 'SK', type: 'S' },
  },
  attributes: [
    { attribute: 'PK',      type: 'S', roles: ['pk'] },
    { attribute: 'SK',      type: 'S', roles: ['sk'] },
    { attribute: 'value',   type: 'N' },
    { attribute: 'version', type: 'N', roles: ['version'] },
  ],
});

const key = { PK: 'TENANT#42', SK: 'COUNTER#impressions' };
const c   = await db.get('Counter', key);

// Pass the just-read version through; TheorydbClient.update asserts it
// matches the persisted item before mutating, and bumps it on success.
await db.update(
  'Counter',
  { ...key, value: (c.value as number) + 1, version: c.version },
  ['value'],
);

Python

@dataclass(frozen=True)
class Counter:
    pk:      str = theorydb_field(roles=["pk"])
    sk:      str = theorydb_field(roles=["sk"])
    value:   int = theorydb_field()
    version: int = theorydb_field(roles=["version"])

model = ModelDefinition.from_dataclass(Counter, table_name="counters_contract")
table = Table(model, client=client)

c = table.get("TENANT#42", "COUNTER#impressions")
table.update(
    "TENANT#42",
    "COUNTER#impressions",
    {"value": c.value + 1},
    expected_version=c.version,
)

Versioning a newly-created item

The first write of an item that does not yet exist starts at version = 0. Subsequent versioned writes increment the value, so the first successful update moves it to version = 1.

Versioning under transactions

Optimistic locking composes with DynamoDB transactions. A transactional write group that includes a versioned item asserts the version on the item’s condition; if any condition fails, the whole transaction fails atomically. See Transactions.

Anti-patterns

  • Don’t read with one runtime, write with another, and mutate the version field manually in between. TableTheory is the only thing that should mutate the version role.
  • Don’t catch the conflict error and retry indefinitely. Bound the retry loop. The contract guarantees deterministic loss, not deterministic eventual success.
  • Don’t omit the version role from your model and expect “best effort” concurrency. No version role means no locking — you opt in by declaring the role on a field.