DynamoDb.DistributedLock is a lightweight .NET library for distributed locking using Amazon DynamoDB. It is designed for serverless and cloud-native applications that require coordination across services or instances.
- ✅ Safe and atomic lock acquisition using conditional writes
- ✅ TTL-based expiration to prevent stale locks
- ✅ AWS-native, no external infrastructure required
- ✅ Simple
IDynamoDbDistributedLockinterface - ✅ IAsyncDisposable support for automatic lock cleanup
- ✅ Retry logic with exponential backoff for handling lock contention and throttling
- ✅ Tested and production-ready for .NET 8 and 9
| Package | Build | NuGet | Downloads |
|---|---|---|---|
| DynamoDb.DistributedLock | |||
| DynamoDb.DistributedLock.Observability |
dotnet add package DynamoDb.DistributedLockservices.AddDynamoDbDistributedLock(options =>
{
options.TableName = "my-lock-table";
options.LockTimeoutSeconds = 30;
options.PartitionKeyAttribute = "pk";
options.SortKeyAttribute = "sk";
});Or bind from configuration:
services.AddDynamoDbDistributedLock(configuration);If you need to customize the IAmazonDynamoDB client, you can pass in AWSOptions, or the configuration section name:
services.AddDynamoDbDistributedLock(configuration, awsOptionsSectionName: "DynamoDb");
// or configure AWSOptions manually
var awsOptions = configuration.GetAWSOptions("DynamoDb");
awsOptions.DefaultClientConfig.ServiceURL = "http://localhost:4566"; // use localstack for testing
services.AddDynamoDbDistributedLock(options =>
{
options.TableName = "my-lock-table";
options.LockTimeoutSeconds = 30;
options.PartitionKeyAttribute = "pk";
options.SortKeyAttribute = "sk";
}, awsOptions);{
"DynamoDbLock": {
"TableName": "my-lock-table",
"LockTimeoutSeconds": 30,
"PartitionKeyAttribute": "pk",
"SortKeyAttribute": "sk"
}
}public class MyService(IDynamoDbDistributedLock distributedLock)
{
public async Task<bool> TryDoWorkAsync()
{
await using var lockHandle = await distributedLock.AcquireLockHandleAsync("resource-1", "owner-abc");
if (lockHandle == null) return false; // Lock not acquired
// 🔧 Critical section - lock automatically released when disposed
// Your protected code here...
return true;
}
}public class MyService(IDynamoDbDistributedLock distributedLock)
{
public async Task<bool> TryDoWorkAsync()
{
var acquired = await distributedLock.AcquireLockAsync("resource-1", "owner-abc");
if (!acquired) return false;
try
{
// 🔧 Critical section
}
finally
{
await distributedLock.ReleaseLockAsync("resource-1", "owner-abc");
}
return true;
}
}The AcquireLockHandleAsync method returns an IDistributedLockHandle that implements IAsyncDisposable for automatic cleanup. This provides several benefits:
await using var lockHandle = await distributedLock.AcquireLockHandleAsync("resource-1", "owner-abc");
// Lock is automatically released when the handle goes out of scopeawait using var lockHandle = await distributedLock.AcquireLockHandleAsync("resource-1", "owner-abc");
if (lockHandle == null) return;
throw new Exception("Oops!"); // Lock is still properly releasedawait using var lockHandle = await distributedLock.AcquireLockHandleAsync("resource-1", "owner-abc");
if (lockHandle == null) return;
Console.WriteLine($"Lock acquired for {lockHandle.ResourceId} by {lockHandle.OwnerId}");
Console.WriteLine($"Lock expires at: {lockHandle.ExpiresAt}");
Console.WriteLine($"Lock is still valid: {lockHandle.IsAcquired}");The library includes built-in retry logic with exponential backoff to handle lock contention and DynamoDB throttling. Retry is disabled by default to maintain backward compatibility.
services.AddDynamoDbDistributedLock(options =>
{
options.TableName = "my-lock-table";
options.Retry.Enabled = true; // Enable retry logic
options.Retry.MaxAttempts = 5; // Max retry attempts (default: 3)
options.Retry.BaseDelay = TimeSpan.FromMilliseconds(100); // Base delay (default: 100ms)
options.Retry.MaxDelay = TimeSpan.FromSeconds(5); // Max delay (default: 5s)
options.Retry.BackoffMultiplier = 2.0; // Exponential multiplier (default: 2.0)
options.Retry.UseJitter = true; // Add jitter to prevent thundering herd (default: true)
options.Retry.JitterFactor = 0.25; // Jitter factor as percentage (default: 0.25 = 25%)
});{
"DynamoDbLock": {
"TableName": "my-lock-table",
"Retry": {
"Enabled": true,
"MaxAttempts": 5,
"BaseDelay": "00:00:00.100",
"MaxDelay": "00:00:05",
"BackoffMultiplier": 2.0,
"UseJitter": true,
"JitterFactor": 0.25
}
}
}The retry logic automatically handles these scenarios:
- Lock contention - When another process holds the lock (
ConditionalCheckFailedException) - DynamoDB throttling - When requests exceed provisioned capacity (
ProvisionedThroughputExceededException) - Internal errors - Transient DynamoDB service errors (
InternalServerErrorException) - Rate limiting - When request rate is exceeded (
RequestLimitExceededException)
Attempt 1: Immediate
Attempt 2: 100ms + jitter
Attempt 3: 200ms + jitter
Attempt 4: 400ms + jitter
Attempt 5: 800ms + jitter (capped at MaxDelay)
Note: Jitter adds randomness (configurable percentage of delay, default 25%) to prevent multiple clients from retrying simultaneously.
This library supports both dedicated tables and shared, single-table designs. You do not need to create a separate table just for locking — this works seamlessly alongside your existing entities.
By default, the library uses the following attributes:
- Partition key:
pk(String) - Sort key:
sk(String) - TTL attribute:
expiresAt(Number, UNIX timestamp in seconds)
However, the partition and sort key attribute names are fully configurable via DynamoDbLockOptions. This makes it easy to integrate into your existing table structure.
✅ Enable TTL on the expiresAt field in your table settings to allow automatic cleanup of expired locks.
This library uses System.Diagnostics.Metrics to collect telemetry data. These metrics can be exported to your preferred observability system (e.g., OpenTelemetry, Prometheus, console output) using standard .NET telemetry exporters.
A full list of metric names can be found in the MetricNames class.
For simplified OpenTelemetry integration, install the observability package:
dotnet add package DynamoDb.DistributedLock.ObservabilityThen configure metrics collection with a single method call:
using DynamoDb.DistributedLock.Observability;
services.AddOpenTelemetry()
.WithMetrics(metrics =>
metrics
// Automatically registers the DynamoDB distributed lock meter
.AddDynamoDbDistributedLock()
// Configure your preferred exporter, e.g., OpenTelemetry Protocol (OTLP)
.AddOtlpExporter(options => options.Endpoint = otlpEndpoint)
);If you prefer manual configuration without the observability package:
services.AddOpenTelemetry()
.WithMetrics(metrics =>
metrics
// Register the meter name manually
.AddMeter(DynamoDb.DistributedLock.Metrics.MetricNames.MeterName)
// Views can be used to filter out specific metrics
.AddView(DynamoDb.DistributedLock.Metrics.MetricNames.LockReleaseTimer, MetricStreamConfiguration.Drop)
// Configure your preferred exporter
.AddOtlpExporter(options => options.Endpoint = otlpEndpoint)
);The library emits the following metrics for observability:
- Lock acquisition counters: Track successful and failed lock acquisitions
- Lock release counters: Track successful and failed lock releases
- Retry counters: Track retry attempts and exhausted retries
- Timing histograms: Measure lock acquisition and release durations
Other options for metrics collection during development:
- dotnet-counters for real-time metrics viewing
- .NET Aspire dashboard for local observability
Unit tests are written with:
- ✅ xUnit v3
- ✅ AutoFixture + NSubstitute
- ✅ FluentAssertions (AwesomeAssertions)
The library provides DynamoDbDistributedLockAutoData to support streamlined tests with frozen mocks and null-value edge cases.
- ⏱ Lock renewal support
- 🔁 Auto-release logic for expired locks
- 🎯 Health check integration
MIT
This project is licensed under the MIT License. See the LICENSE file for details.
Contributions, feedback, and GitHub issues welcome!
Thanks goes to these wonderful people (emoji key):
Nick Cipollina 💻 |
Tyler Reid 💻 |
This project follows the all-contributors specification. Contributions of any kind welcome!