DynamoDB optimistic locking doesn’t prevent concurrent updates; it detects them after the fact and lets you decide what to do.

Let’s see it in action. Imagine we have a simple Product table with a productId (string, partition key), name (string), and stockCount (number).

{
  "productId": "PROD123",
  "name": "Wireless Mouse",
  "stockCount": 100
}

We want to update the stockCount. Without optimistic locking, if two processes try to update stockCount simultaneously, the last one to write wins, potentially overwriting legitimate changes.

With optimistic locking, we add a version attribute to our item, typically an integer.

{
  "productId": "PROD123",
  "name": "Wireless Mouse",
  "stockCount": 100,
  "version": 1
}

Now, when we update the item, we include a condition that the version must match the version we read.

Consider two users, Alice and Bob, both trying to update the stock for "PROD123".

  1. Alice reads the item:

    • Reads productId: "PROD123", stockCount: 100, version: 1.
    • Alice decides to increase stock by 5. Her intended update is stockCount: 105.
  2. Bob reads the item:

    • Reads productId: "PROD123", stockCount: 100, version: 1.
    • Bob decides to decrease stock by 2. His intended update is stockCount: 98.
  3. Bob updates first:

    • Bob issues an UpdateItem operation with:

      • Key: { "productId": "PROD123" }
      • UpdateExpression: SET stockCount = :s
      • ExpressionAttributeValues: { ":s": 98 }
      • ConditionExpression: version = :v
      • ExpressionAttributeValues: { ":v": 1 } (This is the version Bob read)
    • DynamoDB checks the condition: The current version in the table is 1, which matches :v. The update succeeds.

    • DynamoDB increments the version automatically because it’s part of the conditional update. The item is now:

      {
        "productId": "PROD123",
        "name": "Wireless Mouse",
        "stockCount": 98,
        "version": 2
      }
      
  4. Alice attempts to update:

    • Alice issues an UpdateItem operation with:

      • Key: { "productId": "PROD123" }
      • UpdateExpression: SET stockCount = :s
      • ExpressionAttributeValues: { ":s": 105 }
      • ConditionExpression: version = :v
      • ExpressionAttributeValues: { ":v": 1 } (This is the version Alice read)
    • DynamoDB checks the condition: The current version in the table is 2. Alice’s :v is 1. The condition version = :v (i.e., 2 = 1) fails.

    • The UpdateItem operation returns a ConditionalCheckFailedException. Alice’s update is not applied.

At this point, Alice’s application receives the ConditionalCheckFailedException. It knows its read version is stale and must re-evaluate. Alice’s application should then:

  1. Re-read the item: It reads the latest version: stockCount: 98, version: 2.
  2. Re-apply her logic: Her original intent was to increase stock by 5. So, she calculates the new stockCount based on the current stock: 98 + 5 = 103.
  3. Attempt the update again:
    • Key: { "productId": "PROD123" }
    • UpdateExpression: SET stockCount = :s
    • ExpressionAttributeValues: { ":s": 103 }
    • ConditionExpression: version = :v
    • ExpressionAttributeValues: { ":v": 2 } (The version she just read)

This time, the condition version = :v (i.e., 2 = 2) passes. The update succeeds, and the item becomes:

{
  "productId": "PROD123",
  "name": "Wireless Mouse",
  "stockCount": 103,
  "version": 3
}

This retry mechanism ensures that updates are applied based on the most recent state of the data, preventing lost updates.

The core mechanism is the ConditionExpression on the version attribute. When you use UpdateItem with a condition like version = :version_read, DynamoDB performs the update and increments the version attribute atomically if and only if the condition is met. This is crucial: the check and the potential increment of the version happen as a single, indivisible operation. If the condition fails, neither the update nor the version increment occurs, and you get a ConditionalCheckFailedException.

The version attribute itself can be managed in a few ways. A common pattern is to use VersionAttributeName and VersionAttributeValue in your SDK calls. For example, in AWS SDKs, you might configure your DynamoDBMapper or use specific parameters in lower-level API calls. When you use UpdateItem with a condition on version, you don’t typically need to explicitly increment it in your UpdateExpression. DynamoDB handles the increment of the version attribute automatically as part of the conditional update if the condition passes. You just need to specify the ConditionExpression to check against the version you read.

The most surprising aspect of this pattern is how DynamoDB’s atomic conditional updates, combined with a simple version number, provide a robust concurrency control mechanism without complex locking protocols. The system doesn’t stop concurrent operations; it simply makes them aware of each other’s presence and forces a reconciliation step. This reactive approach is often more scalable than proactive locking, which can lead to deadlocks and performance bottlenecks.

The key is that the UpdateItem operation, when paired with a ConditionExpression that includes checking the version attribute, performs the read-check-update-increment-version sequence atomically. Your application then receives a success or a failure. If it’s a failure, the application must retry the entire read-modify-write cycle. The read must fetch the newest version, the modification logic must be re-applied to this new version, and the write must use the new version number in its condition.

The next challenge is handling the "retry" logic itself, especially when multiple retries might be needed due to continuous contention.

Want structured learning?

Take the full Dynamodb course →