Skip to content

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

License

Notifications You must be signed in to change notification settings

LayeredCraft/dynamodb-distributed-lock

🔒 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

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Contributors 5

Languages