Simba is a JVM library for leader election and distributed locks. Each process opens one session on a backend you
already operate — MySQL 8 or PostgreSQL 14+ (JDBC), Redis 7, or Zookeeper — and contends for named leases on it. The
node that wins a lease gets a Term: its fencing token and how long it is still valid. There is no Simba server to
run.
- At most one valid term per name, as far as the local clock can tell: the local deadline is computed from when each keep-alive was sent and always passes before the backend can hand the lease to someone else.
- Fencing tokens. Every term carries a token that strictly increases per name. A process paused by GC can resume after its term ended; resources that must reject such a stale owner check the token. Simba cannot do that for you, and does not promise "no split-brain".
- One session per node. One keep-alive renews every election, lock and partition of the process.
- Work bound to the term. Leader work runs on virtual threads, is interrupted when the term ends, and finishes before the lease is released.
Read Fencing and Failures before protecting anything that must never be written twice. Coming from 4.x? See Migrating to 5.0.
Java 21+; the starter needs Spring Boot 4 (4.1+). Add the Spring Boot starter and one backend:
implementation("me.ahoo.simba:simba-spring-boot-starter:${simbaVersion}")
// JDBC (MySQL 8 or PostgreSQL 14+): also create the tables once with
// me/ahoo/simba/jdbc/init-simba-mysql.sql or init-simba-postgresql.sql from simba-jdbc
implementation("me.ahoo.simba:simba-jdbc:${simbaVersion}")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
runtimeOnly("com.mysql:mysql-connector-j") // or org.postgresql:postgresql
// or Redis 7 with maxmemory-policy noeviction (an evicted lease would reset its fencing token)
implementation("me.ahoo.simba:simba-spring-redis:${simbaVersion}")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
// or Zookeeper: set simba.zookeeper.connect-string (a node keeps its terms through pauses of about 5/12 of
// simba.session.lease-duration, about 6 s by default: raise it to tolerate longer GC or network stalls)
implementation("me.ahoo.simba:simba-zookeeper:${simbaVersion}")
// optional, since 5.1: suspending lock calls, withLock and an event Flow (kotlinx-coroutines)
implementation("me.ahoo.simba:simba-coroutines:${simbaVersion}")The starter builds one Simba bean over the backend on the classpath. With several backend modules, set
simba.backend=jdbc|redis|zookeeper. Without Spring:
val simba = Simba.builder()
.backend(JdbcLeaseBackend.postgres(dataSource)) // or RedisLeaseBackend / ZookeeperLeaseBackend
.build() // AutoCloseable: close() frees every lease at oncesimba.election("order-dispatcher").start(object : LeadershipListener {
override fun onElected(term: Term) = dispatcher.start(term.token)
override fun onRevoked(term: Term, reason: RevokeReason) = dispatcher.stop()
})Every onElected(term) is followed by exactly one onRevoked(term, reason).
simba.election("ingest").runWhileLeader { term ->
while (term.isValid) sink.write(source.next(), fencingToken = term.token)
}The block runs once per term and is interrupted when the term ends; stop() waits for it before releasing the lease.
@Service
public class ReportJobs {
@SimbaScheduled(name = "report", fixedDelay = "1m")
public void generate(Term term) {
reportService.generate(term.getToken());
}
}Without Spring: simba.election("report").schedule(Schedule.fixedDelay(Duration.ofMinutes(1))) { term -> ... }.
@SimbaPartitioned(group = "billing", partitions = 16, fixedDelay = "1m")
public void bill(Term term, int partition) {
billing.run(partition, term.getToken());
}Each of the 16 partitions runs on one live node at a time. Without Spring: simba.group("billing", 16).schedule(...).
simba.lock("invoice-42").acquire(Duration.ofSeconds(5)).use { term ->
term.ensureValid(Duration.ofSeconds(2))
invoiceStore.write(invoice, fencingToken = term.token)
}The returned LockTerm is the credential: closing it releases the lease. Since 5.1, acquireAsync waits without
blocking a thread; cancelling its future abandons the attempt:
simba.lock("invoice-42").acquireAsync(Duration.ofSeconds(5))
.thenCompose { term -> invoiceStore.writeAsync(invoice, term.token).whenComplete { _, _ -> term.releaseAsync() } }With simba-coroutines (since 5.1), the same as a suspending call:
simba.lock("invoice-42").withLock(Duration.ofSeconds(5)) { term -> invoiceStore.write(invoice, term.token) }simba.snapshot() shows the session and every election of the node; the starter adds Micrometer metrics, the
simba Actuator endpoint and the simba health indicator.
simba-testing provides an in-memory backend and virtual time for your unit tests; simba-tck provides FencingSpec
to verify your token-checking resource and LeaseBackendSpec for backends.
| Why Simba | The problem from first principles, guarantees, choosing an API and a backend |
| Quick Start | Dependencies, backend setup, first code for each API |
| How Simba Works | Sessions, local deadline, terms, tokens, election lifecycle, handover, timing |
| Fencing and Failures | Fencing protocol and recipes, token sources, failure modes |
| Backends | JDBC, Redis, Zookeeper: schema, keys, paths, operating notes |
| Spring Boot | simba.* properties, beans, shutdown order |
| Elections and Leader Work | Elections, runWhileLeader, schedules, partition groups, annotations |
| Locks | lock(name) and LockTerm |
| Observability | Snapshot, events, metrics, Actuator endpoint, health |
| Testing | In-memory backend, virtual time, TCK |
| Migrating from 4.x | API, property and storage mapping |
| API Reference | Public types |
| Architecture, ADRs | Internals and design decisions |
The example application runs against any backend.
See CONTRIBUTING.md for the workflow, quality gates and release process, the contributor guide for tests and the backend TCK, and SECURITY.md to report vulnerabilities.