Skip to content

Bluetape4k Leader Part 1: Repository Overview and Mental Model

Robotic operators coordinating one elected leader across several service nodes on a workbench
Replicas can be many. Some work still needs to run once.

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 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

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.

Bluetape4k Leader module map with BOM, core APIs, backend modules, integrations, examples, and preview backends
The core API stays small while backends and framework integrations live in separate modules.
AreaRepresentative modulesWhen to read it
Core APIleader-coreUnderstanding LeaderElector, SuspendLeaderElector, group election, and result types
Redisleader-redis-lettuce, leader-redis-redissonFastest path when Redis is already available
SQL/Coroutineleader-exposed-jdbc, leader-exposed-r2dbcRow-backed leases when RDB infrastructure is the natural home
Infra-nativeleader-etcd, leader-k8s, leader-consulPlatform lease/session models for control-plane style workloads
Frameworkleader-spring-boot, leader-ktor, leader-micrometerAnnotation, scheduler, actuator, and metrics integration
Examples/Benchmarkexamples/*, benchmarkScenario-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.

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 modelAPIFits
BlockingLeaderElectorExisting MVC, batch, and plain JVM jobs
AsyncAsyncLeaderElectorCompletableFuture adapters
CoroutineSuspendLeaderElectorSuspend services, R2DBC, and Ktor handlers
Virtual threadVirtualThreadLeaderElectorJava 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

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.

TopicShedLock behaviorbluetape4k-leader behavior
ContentionSkip when lock is not acquiredrunIfLeader returns null; result API returns Skipped
Spring annotationPrimary pathSupported, but separated from the core API
CoroutineNeeds adaptationSuspendLeaderElector
AsyncAdapter-orientedAsyncLeaderElector
Virtual threadUsually regular blocking codeDedicated virtual-thread elector layer
Multi-leaderNot the main goalLeaderGroupElector for N concurrent leaders
Strategy electionNot the main goalFIFO, random, scored, and weighted strategies
Backend rangeMultiple lock providersRedis, 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.

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.

Batch Scheduler Architecture: three scheduler instances share one Redis leader lock

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.

Migration Gate Architecture: rolling deploy pods check migration markers before and after the Exposed JDBC leader lock

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

Webhook Poller Architecture: one elected poller claims MongoDB events atomically

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

Cache Warmer Architecture: every cache partition gets its own leader lock

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

Tenant Aggregator Architecture: each tenant has a long-running coroutine and an independent leader lock

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

K8s Operator Architecture: three Spring Boot operator pods share one Kubernetes Lease before reconciling

Source: examples/batch-scheduler, examples/tenant-aggregator, examples/k8s-operator

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 LeaderGroupElector instead of single leader election.
  • If the winner should be chosen by rule, such as idle time or weight, look at strategic election.

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.