Skip to content

Prevent Race Conditions with Winter Boot Distributed Locking

Winter Boot’s locking abstraction gives you declarative, AOP-based concurrency control over any public bean method. By annotating a method with #[Lockable] you guarantee that only one caller can execute that method at a time — either at the local-node level (default) or across an entire cluster when a distributed LockManager is provided. Parameter expressions such as name: "order-#{id}" allow the lock scope to be narrowed down to a specific resource instance, preventing unnecessary serialization of unrelated requests.

  • name string (required) Unique name for the lock. Supports #{param} interpolation to bind method parameters into the lock name (e.g. "order-#{id}").

  • lockManager string (default: Framework default) Bean name of a class implementing LockManager. Omit to use local node locking.

  • waitMilliSecs int (default: 0) How long (in milliseconds) to wait for the lock before failing. 0 means fail immediately if the lock is not available.

  • ttlSeconds int (default: 0) Maximum time (in seconds) the lock is held before it is automatically released. 0 means no TTL limit.

The framework provides local node locking out of the box — no extra configuration or bean is needed. Only one process on the same node can execute the annotated method at a time. The #{id} syntax binds the method parameter $id into the lock name, so two concurrent calls with different order IDs proceed in parallel while two calls for the same ID are serialized.

OrderService.php
use dev\winterframework\stereotype\concurrent\Lockable;
#[Lockable(name: 'order-#{id}')]
public function updateOrderStatus(int $id): void
{
// Only one process per $id can run this at a time on this node.
}

For deployments with multiple nodes you need a LockManager backed by a shared store so that locks are visible across the entire cluster. Provide a bean that implements LockManager, pass its bean name to the lockManager option, and the #[Lockable] attribute works identically to local locking from your code’s perspective.

Supported backends include Redis, database locks, Apache ZooKeeper, Consul, and anything else you can wrap in a LockManager implementation.

  1. Annotate the method with a named lockManager

    OrderService.php
    use dev\winterframework\stereotype\concurrent\Lockable;
    #[Lockable(name: 'order-#{id}', lockManager: 'redisLockManager')]
    public function updateOrderStatus(int $id): void
    {
    // No two processes across the cluster can run this for the same $id.
    }
  2. Register the LockManager bean

    LockConfig.php
    use dev\winterframework\stereotype\Configuration;
    use dev\winterframework\stereotype\Bean;
    use dev\winterframework\util\concurrent\LockManager;
    #[Configuration]
    class LockConfig
    {
    #[Bean('redisLockManager')]
    public function getRedisLockManager(): LockManager
    {
    // Construct and return your LockManager implementation.
    // E.g. wrap a Symfony RedisStore inside an adapter.
    return new MyRedisLockManager();
    }
    }

If the lock cannot be acquired within waitMilliSecs milliseconds, Winter Boot throws a LockException. Catch it in your controller or service layer and return an appropriate response to the caller.

OrderController.php
use dev\winterframework\util\concurrent\LockException;
try {
$this->orderService->updateOrderStatus($id);
} catch (LockException $e) {
// Lock could not be acquired — handle retry or conflict response
return $this->response->conflict('Order is currently being updated. Please retry.');
}
Common LockException scenarios
Scenario Recommended response
High-throughput writes to the same resource Return HTTP 409 Conflict and ask the client to retry with exponential back-off.
Background job overlap prevention Log and skip the current execution; the next scheduled run will proceed normally.
Deadlock prevention with ttlSeconds The lock auto-releases after the TTL; the next caller acquires it cleanly.