LockService is the SPI behind DeploymentMode.SINGLE_LEADER. When you mark a @ChangeStream as single-leader, FlowWarden’s leader-election coordinator uses this SPI to ensure exactly one application instance processes events for that stream at any time, with automatic failover when the current leader stops renewing its lock.
For the user-facing description of single-leader semantics, see the Deployment Modes guide. This page documents the SPI contract and the available implementations.
When the SPI is exercised
deploymentMode | What runs |
|---|---|
ALL_INSTANCES (default) | The framework uses the no-op LockService, and tryAcquire always returns true. No coordination, no Lock backend traffic. |
SINGLE_LEADER | The framework’s LeaderElectionCoordinator periodically calls tryAcquire / renew against the configured LockService bean, and release on graceful shutdown. |
LockService bean serves every SINGLE_LEADER stream in the application — each stream gets its own logical lock keyed by streamName.
Interface
Lifecycle contract
tryAcquiremust succeed if the lock does not exist, has expired, or is already owned byinstanceId. It must be atomic against concurrent callers — two instances racing to acquire the samestreamNamemust end with exactly one winner.renewmust extend the lock’s expiration only ifinstanceIdis still the current owner. It must fail (returnfalse) if the lock has expired or is held by a different instance.releaseremoves the lock if and only if it is owned byinstanceId. It is a no-op otherwise — releasing a lock you don’t own must not affect another instance’s grip on it.
Error contract for hot-path methods
Transient errors in this context are: network timeout, primary stepdown / failover, command timeout, lost connection, replica-set re-election, etc. — anything that is a recoverable infrastructure hiccup rather than a programming bug.LockState record
Returned by getLockState and consumed by the FlowWarden Reporter / Console Leadership panel, plus any external consumer that needs to inspect lock ownership:
Optional.empty() for missing or expired locks — never a LockState whose expiresAt is in the past.
Provided implementations
The framework ships with one MongoDB-backed implementation auto-configured by bothImperativeFlowWardenAutoConfiguration and ReactiveFlowWardenAutoConfiguration:
| Implementation | When active | Storage |
|---|---|---|
MongoLockService | Default in both imperative and reactive modes | The _fw_locks collection. tryAcquire uses findOneAndUpdate with a filter on either instanceId == self or expiresAt < now, falling back to insertOne on first acquisition. The collection name is internal — not configurable. |
flowwarden-redis satellite ships RedisLockService (blocking, auto-configured) and ReactiveRedisLockService (reactive, opt-in). Both use Redis Hashes with Lua scripts for the atomic compare-and-set semantics. See Redis Configuration for the storage layout.
No-op default
LockService.noOp() returns a shared singleton that always grants the lock and reports no active leader:
ALL_INSTANCES streams and what you should reach for in tests that want to bypass coordination entirely without standing up a Mongo or Redis container.
Providing a custom implementation
The auto-configured beans are guarded by@ConditionalOnMissingBean, so any @Bean LockService you declare wins. This is how you’d plug in a backend that ships nowhere in the FlowWarden codebase — Consul, etcd, ZooKeeper, JDBC, a homegrown lock service, etc.
Single-leader coordination is independent from checkpoint storage. You can mix backends — for example, a Consul
LockService paired with the default MongoCheckpointStore — by declaring only the bean you want to override. The framework wires whatever beans are present.Testing the contract
If you write a customLockService, run it against the contract tests published in flowwarden-stream-core-testkit:
LockServiceContractTest and implement the abstract lockService() factory method. The contract tests cover acquire / renew / release semantics, expiration, concurrent acquisition, and the instanceId ownership invariants — the same suite the built-in MongoDB and Redis implementations are validated against.
See Also
Deployment Modes
User-facing semantics of
ALL_INSTANCES vs SINGLE_LEADER.Redis backend
Drop-in Redis implementations of
LockService and CheckpointStore.CheckpointStore SPI
The sister SPI that backs
@Checkpoint. Often paired with SINGLE_LEADER for at-least-once recovery.DlqStore SPI
Failed-event persistence for the
@DeadLetterQueue annotation.StreamMetricsProvider SPI
Fire-and-forget callbacks for lifecycle / leadership / checkpoint events.
Actuator
Inspect lock ownership at runtime via
/actuator/flowwarden.