Bluetape4k Leader Part 1: Repository Overview and Mental Model

This is Part 1 of the bluetape4k-leader series. The series starts with the repository overview,
then continues into core APIs, multi-leader and strategic election, Spring Boot and Ktor integration,
and finally storage backends plus operational features.
Distributed leader election sounds larger than the day-to-day problem. The practical version is simple: you run three service replicas, and suddenly the nightly settlement job runs three times, the schema migration tries three times, and three webhook pollers compete for the same event. Horizontal scaling does not automatically mean “only one instance runs this work.” A service needs a way to decide who is allowed to proceed.
bluetape4k-leader is a standalone Kotlin/JVM library for that decision. It supports Redis,
Exposed JDBC/R2DBC, MongoDB, DynamoDB, etcd, Kubernetes Lease, Hazelcast, and ZooKeeper backends,
while exposing blocking, CompletableFuture, coroutine, and virtual-thread APIs.
The Problem: Many Replicas, One Job
Section titled “The Problem: Many Replicas, One Job”The most common example is a scheduled job. @Scheduled works in each JVM. In a three-pod
deployment, it works three times.
val result = leaderElector.runIfLeader("nightly-settlement") { settlementService.processYesterday()}
if (result == null) { log.info { "another instance is running; skip" }}The important part is null. When the node is not elected, the call skips without throwing. This keeps
the familiar ShedLock-style behavior: contention is not an application error. If every missed
lock becomes an error log, the signal from the next real incident gets buried in noise.
The base runIfLeader contract is intentionally small.
- Only the call that acquires the lock executes the action.
- A non-leader path returns
null. - Exceptions from the action propagate to the caller.
- Leadership is released or expires according to the backend lease policy.
When the action itself can return null, use LeaderRunResult to distinguish “elected and returned
null” from “not elected.”
when (val result = leaderElector.runIfLeaderResult("daily-job") { runJob() }) { is LeaderRunResult.Elected -> log.info { "executed=${result.value}" } LeaderRunResult.Skipped -> log.info { "another node is leader" } is LeaderRunResult.ActionFailed -> log.warn(result.cause) { "leader action failed" }}Source: LeaderElector.kt, LeaderRunResult.kt
Repository Map
Section titled “Repository Map”bluetape4k-leader moved out of bluetape4k-projects because its scope is broad.
The core API is compact, but production use brings backend parity, lease semantics, Spring/Ktor
integration, metrics, examples, and benchmarks.

| Area | Representative modules | When to read it |
|---|---|---|
| Core API | leader-core | Understanding LeaderElector, SuspendLeaderElector, group election, and result types |
| Redis | leader-redis-lettuce, leader-redis-redisson | Fastest path when Redis is already available |
| SQL/Coroutine | leader-exposed-jdbc, leader-exposed-r2dbc | Row-backed leases when RDB infrastructure is the natural home |
| Infra-native | leader-etcd, leader-k8s, leader-consul | Platform lease/session models for control-plane style workloads |
| Framework | leader-spring-boot, leader-ktor, leader-micrometer | Annotation, scheduler, actuator, and metrics integration |
| Examples/Benchmark | examples/*, benchmark | Scenario-driven adoption and backend cost comparison |
The current README marks Redis Lettuce/Redisson, Exposed JDBC/R2DBC, MongoDB, Hazelcast, and ZooKeeper as stable backends. DynamoDB, etcd, Consul, and Kubernetes Lease are preview backends.
Execution Models Matter
Section titled “Execution Models Matter”If all you need is annotation-based scheduled-job protection, several tools can help. The point of
bluetape4k-leader is that service code should not be forced into one execution model.
Different Kotlin/JVM services reach leader election from different places.
| Execution model | API | Fits |
|---|---|---|
| Blocking | LeaderElector | Existing MVC, batch, and plain JVM jobs |
| Async | AsyncLeaderElector | CompletableFuture adapters |
| Coroutine | SuspendLeaderElector | Suspend services, R2DBC, and Ktor handlers |
| Virtual thread | VirtualThreadLeaderElector | Java 21 virtual-thread boundaries |
The coroutine API takes a suspend action. If cancellation arrives, CancellationException must be
re-thrown rather than hidden. That sounds like a small implementation detail until a long-running
worker is shutting down. A cancelled leader that keeps holding work is not a graceful shutdown.
val result = suspendLeaderElector.runIfLeader("tenant-aggregator") { metricsService.aggregateTenant(tenantId)}Source: SuspendLeaderElector.kt, LeaderElectionOptions.kt
Compared With ShedLock
Section titled “Compared With ShedLock”ShedLock is a practical tool for preventing duplicate scheduled-job execution. bluetape4k-leader
keeps the same “skip when another node already holds the lock” behavior, but its scope is wider than
protecting a Spring scheduled method.
| Topic | ShedLock behavior | bluetape4k-leader behavior |
|---|---|---|
| Contention | Skip when lock is not acquired | runIfLeader returns null; result API returns Skipped |
| Spring annotation | Primary path | Supported, but separated from the core API |
| Coroutine | Needs adaptation | SuspendLeaderElector |
| Async | Adapter-oriented | AsyncLeaderElector |
| Virtual thread | Usually regular blocking code | Dedicated virtual-thread elector layer |
| Multi-leader | Not the main goal | LeaderGroupElector for N concurrent leaders |
| Strategy election | Not the main goal | FIFO, random, scored, and weighted strategies |
| Backend range | Multiple lock providers | Redis, SQL/R2DBC, MongoDB, DynamoDB, etcd, Kubernetes, Hazelcast, ZooKeeper |
So if the only problem is one Spring scheduled method, the mental model overlaps with ShedLock.
If the same service also has coroutine workers, tenant-specific pollers, N-slot work distribution,
Kubernetes Lease-backed operators, or Ktor scheduling, bluetape4k-leader gives those paths a
shared vocabulary.
Examples Show the Real Use Cases
Section titled “Examples Show the Real Use Cases”Each of the six examples targets one operational failure mode. They are a direct way to understand what the library actually protects against.
Batch Scheduler — A nightly settlement job running once per replica instead of once per cluster. A Lettuce Redis lock lets only one node execute.

Migration Gate — Multiple pods attempting boot-time schema migration simultaneously during a rolling deploy. An Exposed JDBC lock serializes the migration; others wait and confirm completion.

Webhook Poller — Several pollers claiming the same webhook event. A MongoDB-backed
SuspendLeaderElector ensures only one poller picks up each event.

Cache Warmer — Per-partition cache warming running on multiple nodes at once. Each partition gets its own leader lock so warming is distributed safely.

Tenant Aggregator — Per-tenant coroutine polling loops running on multiple nodes simultaneously.
forTenant() creates an independent lock per tenant; SuspendLeaderElector controls execution.

K8s Operator — Several operator replicas running the same reconcile loop. A
KubernetesLeaseLeaderElector uses the Kubernetes Lease API directly to elect one active reconciler.

Source: examples/batch-scheduler, examples/tenant-aggregator, examples/k8s-operator
A First Selection Rule
Section titled “A First Selection Rule”Part 5 will go deeper on backend choice, but the first-pass rule is enough for orientation.
- If Redis is already part of the service platform, start with Lettuce or Redisson.
- If the service only has an RDB, Exposed JDBC/R2DBC keeps the operational footprint smaller.
- If the workload is a Kubernetes operator or platform-native controller, Kubernetes Lease is natural.
- If etcd, Consul, or ZooKeeper already sits in the control plane, evaluate that session/lease model.
- If up to N nodes may run the same work, look at
LeaderGroupElectorinstead of single leader election. - If the winner should be chosen by rule, such as idle time or weight, look at strategic election.
References
Section titled “References”- Repository: bluetape4k-leader
- README: README.md
- Korean README: README.ko.md
- BOM README: bluetape4k-leader-bom/README.md
Closing
Section titled “Closing”Leader election is less about “distributed systems” and more about a practical question: should this replica run this piece of work right now?
Scheduled jobs, migrations, pollers, cache warmers, and operator reconcile loops all ask that
question. bluetape4k-leader answers it with a common execution model rather than a pile of
backend-specific APIs. Part 2 zooms into the core API: LeaderElector, SuspendLeaderElector,
LeaderElectionOptions, and LeaderRunResult. That is where the important distinction appears:
which node ran, and what every other node did instead.
Comments
Leave a note or reaction with your GitHub account.