Skip to content

Repository files navigation

🔒 DynamoDb.DistributedLock

All Contributors

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 IDynamoDbDistributedLock interface
  • 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

📦 Packages

Package Build NuGet Downloads
DynamoDb.DistributedLock Build NuGet NuGet Downloads
DynamoDb.DistributedLock.Observability Build NuGet NuGet Downloads

🚀 Getting Started

1. Install the NuGet package

dotnet add package DynamoDb.DistributedLock

2. Register the lock in your DI container

services.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);

appsettings.json

{
  "DynamoDbLock": {
    "TableName": "my-lock-table",
    "LockTimeoutSeconds": 30,
    "PartitionKeyAttribute": "pk",
    "SortKeyAttribute": "sk"
  }
}

3. Use the lock

Recommended: IAsyncDisposable Pattern (v1.1.0+)

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;
    }
}

Traditional Pattern

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;
    }
}

🔧 Lock Handle API (v1.1.0+)

The AcquireLockHandleAsync method returns an IDistributedLockHandle that implements IAsyncDisposable for automatic cleanup. This provides several benefits:

✅ Automatic Lock Release

await using var lockHandle = await distributedLock.AcquireLockHandleAsync("resource-1", "owner-abc");
// Lock is automatically released when the handle goes out of scope

✅ Exception Safety

await using var lockHandle = await distributedLock.AcquireLockHandleAsync("resource-1", "owner-abc");
if (lockHandle == null) return;

throw new Exception("Oops!"); // Lock is still properly released

✅ Lock Metadata Access

await 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}");

🔄 Retry Configuration (v1.1.0+)

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.

✅ Enable Retry Logic

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%)
});

✅ Configuration via appsettings.json

{
  "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
    }
  }
}

✅ When Retry is Triggered

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)

✅ Exponential Backoff Example

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.


🏗️ Table Schema

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.


📈 Observability

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.

📦 Observability Package (Recommended)

For simplified OpenTelemetry integration, install the observability package:

dotnet add package DynamoDb.DistributedLock.Observability

Then 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)
    );

🔧 Manual Configuration

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)
    );

🔍 Available Metrics

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

🧪 Local Development

Other options for metrics collection during development:


🧪 Unit Testing

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.


🔮 Future Enhancements

  • ⏱ Lock renewal support
  • 🔁 Auto-release logic for expired locks
  • 🎯 Health check integration

📜 License

MIT

This project is licensed under the MIT License. See the LICENSE file for details.


🤝 Contributing

Contributions, feedback, and GitHub issues welcome!

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Nick Cipollina
Nick Cipollina

💻
Tyler Reid
Tyler Reid

💻

This project follows the all-contributors specification. Contributions of any kind welcome!

About

A lightweight distributed locking library for .NET using Amazon DynamoDB as the backend.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages