

# Best practices for handling concurrent updates in DynamoDB
<a name="BestPractices_ImplementingVersionControl"></a>

In distributed systems, multiple processes or users may attempt to modify the same data at the same time. Without concurrency control, these concurrent writes can lead to lost updates, inconsistent data, or race conditions. DynamoDB provides several mechanisms to help you manage concurrent access and maintain data integrity.

**Note**  
Individual write operations such as `UpdateItem` are atomic and always operate on the most recent version of the item, regardless of concurrency. Locking strategies are needed when your application must read an item and then write it back based on the read value (a read-modify-write cycle), because another process could modify the item between the read and the write.

There are two primary strategies for handling concurrent updates:
+ **Optimistic locking** – Assumes conflicts are rare. It allows concurrent access and detects conflicts at write time using conditional writes. If a conflict is detected, the write fails and the application can retry.
+ **Pessimistic locking** – Assumes conflicts are likely. It prevents concurrent access by acquiring exclusive access to a resource before modifying it. Other processes must wait until the lock is released.

The following table summarizes the approaches available in DynamoDB:


| Approach | Mechanism | Best for | 
| --- | --- | --- | 
| Optimistic locking | Version attribute \$1 conditional writes | Low contention, inexpensive retries | 
| Pessimistic locking (transactions) | TransactWriteItems | Multi-item atomicity, moderate contention | 
| Pessimistic locking (lock client) | Dedicated lock table with lease and heartbeat | Long-running workflows, distributed coordination | 

# Optimistic locking with version number
<a name="BestPractices_OptimisticLocking"></a>

Optimistic locking is a strategy that detects conflicts at write time rather than preventing them. Each item includes a version attribute that increments with every update. When updating an item, you include a [condition expression](Expressions.ConditionExpressions.md) that checks whether the version number matches the value your application last read. If another process modified the item in the meantime, the condition fails and DynamoDB returns a `ConditionalCheckFailedException`.

## When to use optimistic locking
<a name="BestPractices_OptimisticLocking_WhenToUse"></a>

Optimistic locking is a good fit when:
+ Multiple users or processes may update the same item, but conflicts are infrequent.
+ Retrying a failed write is inexpensive for your application.
+ You want to avoid the overhead and complexity of managing distributed locks.

Common examples include e-commerce inventory updates, collaborative editing platforms, and financial transaction records.

## Tradeoffs
<a name="BestPractices_OptimisticLocking_Tradeoffs"></a>

**Retry overhead in high contention**  
In high-concurrency environments, the likelihood of conflicts increases, potentially causing higher retries and write costs.

**Implementation complexity**  
Adding version control to items and handling conditional checks adds complexity to the application logic. The AWS SDK for Java v2 Enhanced Client provides built-in support through the [https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE) annotation, which automatically manages version numbers for you.

## Pattern design
<a name="BestPractices_OptimisticLocking_PatternDesign"></a>

Include a version attribute in each item. Here is a simple schema design:
+ Partition key – A unique identifier for each item (for example, `ItemId`).
+ Attributes:
  + `ItemId` – The unique identifier for the item.
  + `Version` – An integer that represents the version number of the item.
  + `QuantityLeft` – The remaining inventory of the item.

When an item is first created, the `Version` attribute is set to 1. With each update, the version number increments by 1.


| ItemID (partition key) | Version | QuantityLeft | 
| --- | --- | --- | 
| Bananas | 1 | 10 | 
| Apples | 1 | 5 | 
| Oranges | 1 | 7 | 

## Implementation
<a name="BestPractices_OptimisticLocking_Implementation"></a>

To implement optimistic locking, follow these steps:

1. Read the current version of the item.

   ```
   def get_item(item_id):
       response = table.get_item(Key={'ItemID': item_id})
       return response['Item']
   
   item = get_item('Bananas')
   current_version = item['Version']
   ```

1. Update the item using a condition expression that checks the version number.

   ```
   def update_item(item_id, qty_bought, current_version):
       try:
           response = table.update_item(
               Key={'ItemID': item_id},
               UpdateExpression="SET QuantityLeft = QuantityLeft - :qty, Version = :new_v",
               ConditionExpression="Version = :expected_v",
               ExpressionAttributeValues={
                   ':qty': qty_bought,
                   ':new_v': current_version + 1,
                   ':expected_v': current_version
               },
               ReturnValues="UPDATED_NEW"
           )
           return response
       except ClientError as e:
           if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
               print("Version conflict: another process updated this item.")
           raise
   ```

1. Handle conflicts by retrying with a fresh read.

   Each retry requires an additional read, so limit the total number of retries.

   ```
   def update_with_retry(item_id, qty_bought, max_retries=3):
       for attempt in range(max_retries):
           item = get_item(item_id)
           try:
               return update_item(item_id, qty_bought, item['Version'])
           except ClientError as e:
               if e.response['Error']['Code'] != 'ConditionalCheckFailedException':
                   raise
               print(f"Retry {attempt + 1}/{max_retries}")
       raise Exception("Update failed after maximum retries.")
   ```

For Java applications, the AWS SDK for Java v2 Enhanced Client provides built-in optimistic locking support through the [https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE) annotation, which automatically manages version numbers for you.

For more information about condition expressions, see [DynamoDB condition expression CLI example](Expressions.ConditionExpressions.md).

# Pessimistic locking with DynamoDB transactions
<a name="BestPractices_PessimisticLocking"></a>

DynamoDB [transactions](transactions.md) provide an all-or-nothing approach to grouped operations. When you use `TransactWriteItems`, DynamoDB monitors all items in the transaction. If any item is modified by another operation during the transaction, the entire transaction is canceled and DynamoDB returns a `TransactionCanceledException`. This behavior provides a form of pessimistic concurrency control because conflicting concurrent modifications are prevented rather than detected after the fact.

## When to use transactions for locking
<a name="BestPractices_PessimisticLocking_WhenToUse"></a>

Transactions are a good fit when:
+ You need to update multiple items atomically, either within the same table or across tables.
+ Your business logic requires all-or-nothing semantics – either all changes succeed or none are applied.

Common examples include transferring funds between accounts, placing orders that update both inventory and order tables, and exchanging items between players in a game.

## Tradeoffs
<a name="BestPractices_PessimisticLocking_Tradeoffs"></a>

**Higher write cost**  
For items up to 1 KB, transactions consume 2 WCUs per item (one to prepare, one to commit), compared to 1 WCU for a standard write.

**Item limit**  
A single transaction can include up to 100 actions across one or more tables.

**Conflict sensitivity**  
If any item in the transaction is modified by another operation, the entire transaction fails. In high-contention scenarios, this can lead to frequent cancellations.

## Implementation
<a name="BestPractices_PessimisticLocking_Implementation"></a>

The following example uses `TransactWriteItems` to transfer inventory between two items atomically. If another process modifies either item during the transaction, the entire operation is rolled back.

```
import boto3

client = boto3.client('dynamodb')

def transfer_inventory(source_id, target_id, quantity):
    try:
        client.transact_write_items(
            TransactItems=[
                {
                    'Update': {
                        'TableName': 'Inventory',
                        'Key': {'ItemID': {'S': source_id}},
                        'UpdateExpression': 'SET QuantityLeft = QuantityLeft - :qty',
                        'ConditionExpression': 'QuantityLeft >= :qty',
                        'ExpressionAttributeValues': {
                            ':qty': {'N': str(quantity)}
                        }
                    }
                },
                {
                    'Update': {
                        'TableName': 'Inventory',
                        'Key': {'ItemID': {'S': target_id}},
                        'UpdateExpression': 'SET QuantityLeft = QuantityLeft + :qty',
                        'ExpressionAttributeValues': {
                            ':qty': {'N': str(quantity)}
                        }
                    }
                }
            ]
        )
        return True
    except client.exceptions.TransactionCanceledException as e:
        print(f"Transaction canceled: {e}")
        return False
```

In this example, the condition expression checks that sufficient inventory exists, but no version attribute is needed. DynamoDB automatically cancels the transaction if any item in the transaction is modified by another operation between the prepare and commit phases. This is what provides the pessimistic concurrency control – conflicting concurrent modifications are prevented by the transaction itself.

**Note**  
You can combine transactions with optimistic locking by adding version checks as additional condition expressions. This provides an extra layer of protection but is not required for the transaction to detect conflicts.

For more information, see [Managing complex workflows with DynamoDB transactions](transactions.md).

# Distributed locking with the DynamoDB Lock Client
<a name="BestPractices_DistributedLocking"></a>

For applications that require traditional lock-acquire-release semantics, the DynamoDB Lock Client is an open-source library that implements distributed locking using a DynamoDB table as the lock store. This approach is useful when you need to coordinate access to an external resource (such as an S3 object or a shared configuration) across multiple application instances.

The lock client is available as an open-source [Java library](https://github.com/awslabs/amazon-dynamodb-lock-client).

## How it works
<a name="BestPractices_DistributedLocking_HowItWorks"></a>

The lock client uses a dedicated DynamoDB table to track locks. Each lock is represented as an item with the following key attributes:
+ A partition key that identifies the resource being locked.
+ A lease duration that specifies how long the lock is valid. If the lock holder crashes or becomes unresponsive, the lock automatically expires after the lease duration.
+ A heartbeat that the lock holder sends periodically to extend the lease. This prevents the lock from expiring while the holder is still actively processing.

The lock client uses conditional writes to ensure that only one process can acquire a lock at a time. If a lock is already held, the caller can choose to wait and retry or fail immediately.

## When to use the lock client
<a name="BestPractices_DistributedLocking_WhenToUse"></a>

The lock client is a good fit when:
+ You need to coordinate access to a shared resource across multiple application instances or microservices.
+ The critical section is long-running (seconds to minutes) and retrying the entire operation on conflict would be expensive.
+ You need automatic lock expiry to handle process failures gracefully.

Common examples include orchestrating distributed workflows, coordinating cron jobs across multiple instances, and managing access to shared external resources.

## Tradeoffs
<a name="BestPractices_DistributedLocking_Tradeoffs"></a>

**Additional infrastructure**  
Requires a dedicated DynamoDB table for lock management, with additional read and write capacity for lock operations and heartbeats.

**Clock dependency**  
Lock expiry relies on timestamps. Significant clock skew between clients can cause unexpected behavior, particularly for short lease durations.

**Deadlock risk**  
If your application acquires locks on multiple resources, you must acquire them in a consistent order to avoid deadlocks. The lease duration provides a safety net by automatically releasing locks from unresponsive holders.

## Implementation
<a name="BestPractices_DistributedLocking_Implementation"></a>

The following example shows how to use the DynamoDB Lock Client to acquire and release a lock:

```
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

final DynamoDbClient dynamoDB = DynamoDbClient.builder()
    .region(Region.US_WEST_2)
    .build();

final AmazonDynamoDBLockClient lockClient = new AmazonDynamoDBLockClient(
    AmazonDynamoDBLockClientOptions.builder(dynamoDB, "Locks")
        .withTimeUnit(TimeUnit.SECONDS)
        .withLeaseDuration(10L)
        .withHeartbeatPeriod(3L)
        .withCreateHeartbeatBackgroundThread(true)
        .build());

// Try to acquire a lock on a resource
final Optional<LockItem> lock =
    lockClient.tryAcquireLock(AcquireLockOptions.builder("my-shared-resource").build());

if (lock.isPresent()) {
    try {
        // Perform operations that require exclusive access
        processSharedResource();
    } finally {
        // Always release the lock when done
        lockClient.releaseLock(lock.get());
    }
} else {
    System.out.println("Failed to acquire lock.");
}

lockClient.close();
```

**Important**  
Always release locks in a `finally` block to ensure locks are released even if your processing logic throws an exception. Unreleased locks block other processes until the lease expires.

You can also implement a simple locking mechanism without the lock client library by using conditional writes directly. The following example uses `UpdateItem` with a condition expression to acquire a lock, and `DeleteItem` to release it:

```
from datetime import datetime, timedelta
from boto3.dynamodb.conditions import Attr

def acquire_lock(table, resource_name, owner_id, ttl_seconds):
    """Attempt to acquire a lock. Returns True if successful."""
    expiry = (datetime.now() + timedelta(seconds=ttl_seconds)).isoformat()
    now = datetime.now().isoformat()
    try:
        table.update_item(
            Key={'LockID': resource_name},
            UpdateExpression='SET #owner = :owner, #expiry = :expiry',
            ConditionExpression=Attr('LockID').not_exists() | Attr('ExpiresAt').lt(now),
            ExpressionAttributeNames={'#owner': 'OwnerID', '#expiry': 'ExpiresAt'},
            ExpressionAttributeValues={':owner': owner_id, ':expiry': expiry}
        )
        return True
    except table.meta.client.exceptions.ConditionalCheckFailedException:
        return False

def release_lock(table, resource_name, owner_id):
    """Release a lock. Only succeeds if the caller is the lock owner."""
    try:
        table.delete_item(
            Key={'LockID': resource_name},
            ConditionExpression=Attr('OwnerID').eq(owner_id)
        )
        return True
    except table.meta.client.exceptions.ConditionalCheckFailedException:
        return False
```

This approach uses a condition expression to ensure that a lock can only be acquired if it doesn't exist or has expired, and can only be released by the process that acquired it. Consider enabling [Time to Live (TTL)](TTL.md) on the lock table to automatically clean up expired lock items.

## Choosing a concurrency control strategy
<a name="BestPractices_ChoosingLockingStrategy"></a>

Use the following guidelines to choose the right approach for your workload:

**Use optimistic locking** when:  
+ Conflicts are infrequent.
+ Retrying a failed write is inexpensive.
+ You are updating a single item at a time.

**Use transactions** when:  
+ You need to update multiple items atomically.
+ You require all-or-nothing semantics across items or tables.
+ You need to combine condition checks with writes in a single operation.

**Use the lock client** when:  
+ You need to coordinate access to external resources across distributed processes.
+ The critical section is long-running and retrying on conflict is expensive.
+ You need automatic lock expiry to handle process failures.

**Note**  
If you use [DynamoDB global tables](GlobalTables.md), be aware that global tables use a "last writer wins" reconciliation strategy for concurrent updates. Optimistic locking with version numbers does not work as expected across Regions because a write in one Region may overwrite a concurrent write in another Region without a version check. Design your application to handle conflicts at the application level when using global tables.