Skip to main content

Distributed Locking

Overview

Distributed locking prevents the same critical business operation from running concurrently across multiple application instances.

TeaQL applications commonly use distributed locks for:

  • Scheduled jobs
  • Order submission
  • Payment confirmation
  • Inventory deduction
  • Idempotent callbacks
  • Large data import/export
  • Schema or index preparation

CustomUserContext is the recommended place to expose project-specific lock behavior because it knows the user, tenant, request, and infrastructure environment.


Local Lock vs Distributed Lock

Lock TypeScopeUse Case
Local lockOne JVM instanceProtect in-memory work inside one process
Distributed lockAll service instancesProtect business operations in clustered deployments

If your service can run more than one instance, use a distributed lock for business-critical operations.


Keep lock provider details outside business logic:

Business method
-> userContext.withDistributedLock(...)
-> lock service
-> Redis / database / ZooKeeper / etcd / custom provider

Example lock service:

public interface DistributedLockService {

<T> T executeWithLock(String key, Duration timeout, Supplier<T> task);

void executeWithLock(String key, Duration timeout, Runnable task);
}

Expose it through CustomUserContext:

public class CustomUserContext extends UserContext {

public DistributedLockService distributedLockService() {
return getBean(DistributedLockService.class);
}

public void withDistributedLock(String key, Runnable task) {
distributedLockService().executeWithLock(key, Duration.ofSeconds(30), task);
}
}

Lock Key Design

A lock key must identify the exact business resource being protected.

Good examples:

tenant:order:submit:ORD-2026-000001
tenant:payment:callback:PAY-893020
tenant:inventory:sku:SKU-10086
global:schema:ensure

Bad examples:

lock
order
job

Always include enough scope:

  • Tenant
  • Operation
  • Entity type
  • Business ID or internal ID
  • Optional region or shard

Example: Protect Order Submission

public WebResponse submitOrder(CustomUserContext userContext, String orderId) {
String lockKey = userContext.currentTenantCode() + ":order:submit:" + orderId;

userContext.withDistributedLock(lockKey, () -> {
Order order = Q.orders().filterById(orderId).comment("Query orders").purpose("Load data").executeForOne(userContext);
order.submit(userContext);
order.auditAs("Save order").save(userContext);
});

return WebResponse.success();
}

This prevents duplicate submission when the user double-clicks, retries, or when two service instances receive the same command.


Timeout and Failure Policy

Every distributed lock needs a clear timeout policy.

Common choices:

PolicyBehavior
Fail fastReturn error immediately if lock cannot be acquired
Wait with timeoutWait for a short period, then fail
SkipUsed for scheduled jobs where another instance is already running
RetryRetry with backoff for recoverable workflows

Example:

public void runDailySettlement(CustomUserContext userContext) {
userContext.distributedLockService().executeWithLock(
"global:settlement:daily",
Duration.ofMinutes(5),
() -> SettlementUtil.runDailySettlement(userContext)
);
}

Idempotency

Locks are not a replacement for idempotency.

For critical integrations, use both:

  • Distributed lock to prevent concurrent execution
  • Idempotency record to prevent duplicate effects after retries
  • Transaction boundary to keep state consistent

Example fields:

  • External request ID
  • Callback ID
  • Operation hash
  • Processed time
  • Result status

Best Practices

  • Use business-specific lock keys.
  • Include tenant scope in lock keys unless the operation is truly global.
  • Keep locked sections short.
  • Do not call slow external services while holding a lock unless necessary.
  • Always release locks with finally behavior inside the provider.
  • Use timeout and retry policies intentionally.
  • Combine locks with database constraints for critical uniqueness.

Testing Checklist

  • Two concurrent requests for the same key cannot both execute the critical section.
  • Different tenants do not block each other.
  • Lock timeout returns the expected error.
  • Failed operations release the lock.
  • Scheduled jobs run on only one instance.
  • Retry behavior does not create duplicate business effects.

Summary

Distributed locking is a project-level infrastructure concern. TeaQL keeps it clean by routing lock behavior through CustomUserContext, letting business code express the protected operation while the project controls the actual lock provider and failure policy.