Conditional writes and optimistic locking

Amazon DynamoDB · Practical · 12 min

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 - :n with condition stock >= :n.
  • Optimistic locking: store a version number. Read the item, change it, then write with version = :read_version in the condition and version = version + 1 in 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 retry

Remember

  • 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.

Check yourself

  1. What happens when a conditional update's condition is false?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html