Conditions are DynamoDB's tool for correctness under concurrency, without locks or transactions.
Any put, update or delete can carry a ConditionExpression. DynamoDB evaluates it against the current item atomically with the write; if it is false, nothing is written and the call fails with ConditionalCheckFailedException.
Common patterns:
- Create only if new:
put_item(..., ConditionExpression="attribute_not_exists(customer_id)"). Checking the partition key attribute is the idiom for "no item with this key exists". - Guard a state transition:
update_item(... SET #s = :shipped, ConditionExpression="#s = :paid")so an order can only ship once it is paid. - Never go negative:
SET stock = stock - :nwith conditionstock >= :n. - Optimistic locking: store a
versionnumber. Read the item, change it, then write withversion = :read_versionin the condition andversion = version + 1in the update. If someone else wrote in between, your write fails and you re-read and retry.
Catch the error via client.exceptions.ConditionalCheckFailedException (the resource exposes it as table.meta.client.exceptions...). Add ReturnValuesOnConditionCheckFailure="ALL_OLD" to receive the current item in the error and avoid another read.
A failed condition still consumes write capacity.
Example
import boto3
table = boto3.resource("dynamodb").Table("Orders")
errors = table.meta.client.exceptions
key = {"customer_id": "c5", "order_id": "o1"}
table.put_item(Item={**key, "stock": 3, "version": 1},
ConditionExpression="attribute_not_exists(customer_id)")
try: # same key again: refused
table.put_item(Item={**key, "stock": 0, "version": 1},
ConditionExpression="attribute_not_exists(customer_id)")
except errors.ConditionalCheckFailedException:
print("already exists") # already exists
def take(n, seen_version):
table.update_item(Key=key,
UpdateExpression="SET stock = stock - :n, version = version + :one",
ConditionExpression="stock >= :n AND version = :v",
ExpressionAttributeValues={":n": n, ":one": 1, ":v": seen_version})
take(2, 1) # ok: stock 1, version 2
try:
take(1, 1) # stale version
except errors.ConditionalCheckFailedException:
print("conflict, re-read and retry") # conflict, re-read and retryRemember
- Conditions are checked atomically with the write
attribute_not_exists(pk)means create-only- Version attributes implement optimistic locking
- Handle ConditionalCheckFailedException as an expected outcome
Watch out for
- Reading an item, checking a value in application code and then writing without a condition: another writer can slip in between.