Introduction
This is the final installment in my atomic counters series where I explore different distributed databases and how they implement atomic counters.
This time, were looking at Amazon DocumentDB a managed NoSQL document database, MongoDB-compatible and optimized for AWS.
Atomic counters are a common requirement in distributed applications, whether for tracking views, managing inventory, or implementing rate limiting.
In this article, well discuss how DocumentDB handles atomic updates and explore a working example from my GitHub repository.
Serializability and Linearizability in DocumentDB
Before we can dive deep into code, we need to recall few concepts (please refer to the first article of this series for a detailed explanation )
Serializability : Operations appear in a consistent sequential order, ensuring correctness.
Linearizability : Writes are immediately visible for subsequent reads, ensuring real-time consistency.
DocumentDB achieves linearizable writes through its single-primary, multi-replica architecture :
Write operations are directed to the primary instance , and changes are asynchronously replicated to secondaries.
Read operations from the primary always return the latest committed value , ensuring linearizability.
Replica reads may return stale data due to replication lag, meaning they are eventually consistent.
This guarantees that atomic updates within a single document, like counters using the $inc operator, remain correct and isolated.
While not required in this specific scenario, it is worth to mention DocumentDB supports:
Replication and Leader Election in DocumentDB
DocumentDB automatically replicates data across multiple availability zones to ensure durability and availability.
Key mechanisms include:
Single-primary replication : A single primary instance handles writes, while replicas asynchronously replicate data and serve read requests.
Leader election : If the primary instance fails, DocumentDB automatically promotes a replica to primary, minimizing downtime and maintaining availability.
This replication strategy allows DocumentDB to scale reads across replicas while ensuring that writes remain strongly consistent on the primary.
However, applications must account for eventual consistency when reading from replicas due to asynchronous replication.
The Atomic Counter Pattern
The atomic counter pattern enables precise increment operations, even in distributed environments.
With DocumentDB, you use the $inc operator , which atomically increments a numeric field within a document.
This ensures that concurrent increments are safely serialized without race conditions.
DocumentDB supports conditional increments natively : you can use $inc with $cond in an update operation to increment the counter only when certain conditions are met all in a single atomic operation.
This makes DocumentDB a good choice when you need both unconditional and conditional increments , ensuring correctness without requiring complex client-side logic.
Hands-on! Walkthrough of the Deployable Example
Lets examine how the deployable example in this GitHub repository
This example demonstrates how to implement an atomic counter using AWS Lambda , API Gateway , and DocumentDB.
API Gateway : Provides HTTP endpoints for interacting with the counter.
Lambda Functions : Implements the business logic for incrementing the counter.
DocumentDB : Stores the counters.
In my example project you can decide wheter to use a maximum value for the counter or not: this determine if use or not conditional writes.
Lets focus on lambda business logic, from the docDbCounterLambda code:
const documentDBClient = await buildDocumentDbClient();await documentDBClient.connect();const countersCollection = documentDBClient.db("atomic_counter").collection('counters');const updateFilter = getUpdateFilter(useConditionalWrites, id, maxCounterValue);const updateResult = await countersCollection.updateOne( updateFilter, { $inc: { atomic_counter: 1 } }, { upsert: true, });
Here I use the $inc operator with the upsert flag set to true: this makes the method work both the first time, when the counter does not exist and is therefore initialized to zero, and for further increment operations.
What changes between conditional and unconditional write operations is the updateFilter returned by the getUpdateFilter method.
Lets have a look at it:
const getUpdateFilter = (useConditionalWrites: boolean, id: number, maxCounterValue: string) => { const unconditionalWriteParams = { counter_id: id } const conditionalWriteParams = { counter_id: id, $and: [{ atomic_counter: { $lt: Number(maxCounterValue) } }], } return useConditionalWrites ? conditionalWriteParams : unconditionalWriteParams;}
For unconditional writes, the only filter is the counter_id attribute.
For conditional writes, the construct $lt (lower than) is added as an additional condition to check whether the value is below the maximum value.
Since the update is performed for a single document and the increment operation is performed on the server side, atomicity is guaranteed and the counter value cannot exceed the maximum value
Trade-Offs and Conclusion
Like other databases in this series, DocumentDB comes with trade-offs when used for atomic counters:
Strenghts:
MongoDB Compatibility : Developers familiar with MongoDB can reuse existing knowledge.
Managed Scaling : AWS handles replication, backups, and failover.
Atomic Updates on a Single Document : $inc ensures updates are atomic.
Limitations:
Eventual Consistency for Replicas : Secondary reads may return stale data.
Higher Latency for Stronger Consistency : To ensure fresh data , queries must be sent to the primary instance.
Key Takeaways:
Atomic counters in DocumentDB can be implemented using the $inc operator, ensuring atomic updates at the document level.
Conditional increments are fully supported using $inc combined with $cond , allowing for server-side enforcement of constraints.
DocumentDB follows a single-primary, multi-replica model , meaning writes are strongly consistent , but replica reads may be eventually consistent.
Automatic leader election ensures high availability by promoting a replica to primary in case of failure.
You can find the full runnable example in my GitHub repository: atomic-counter.
This marks the end of the atomic counter series! 🚀
Top comments (0)