Ekbatan is a Java persistence framework for event-driven systems. One database transaction commits your data and the domain events; the events are then reliably shipped from the events outbox table to Kafka or any event broker.
A replacement for Hibernate, Spring Data, or hand-rolled JDBC. Drops into Spring Boot, Quarkus, or Micronaut — or plain java.
Your service inserts a row into the database, then publishes an event to Kafka. Two independent operations across two systems. If the second fails — Kafka outage, network blip, service crash — the row is committed but the event is lost. Your database and your event stream silently disagree.
Crash between writes ⇒ DB and Kafka disagree.
The solution is a known pattern: the Transactional Outbox. Write the row AND the event into the same database transaction — both commit or both roll back. A separate process (a CDC tool like Debezium, a background job, or similar) drains the outbox table to Kafka, retrying until delivery succeeds.
Ekbatan makes adopting this pattern hassle-free. No boilerplate, no glue code, no outbox plumbing to maintain. The publish step stays decoupled by design — pair it with Debezium or any CDC tool of your choice.
CDC tails the outbox — events ship later, always in sync.
A domain object that emits events when it mutates. deposit(amount) returns a new Wallet with a WalletMoneyDepositedEvent attached inside the same builder call. State and event coupled at the source — never two writes. Don't need events for a table? Extend Entity instead — same persistence surface, no event emission.
1
@AutoBuilder
2
public final class Wallet extends Model<Wallet, …> {
3
public final BigDecimal balance;
4
// …
5
6
public Wallet deposit(BigDecimal amount) {
7
return copy()
8
.withEvent(new WalletMoneyDepositedEvent(id, amount))
9
.balance(balance.add(amount))
10
.build();
11
}
12
}
A unit of business work. perform() reads from a repository, mutates the model, stages the new version on plan(). No transaction handling, no direct writes. Once perform() returns, ActionExecutor opens one database transaction and persists everything atomically — domain rows AND the matching events in the outbox table. All of it commits, or all of it rolls back.
1
@EkbatanAction
2
public class WalletDepositAction extends Action<Params, Wallet> {
3
4
protected Wallet perform(Principal p, Params params) {
5
var wallet = walletRepository.getById(params.walletId());
6
return plan().update(wallet.deposit(params.amount()));
7
}
8
// …
9
}
The framework's single entry point. Inject it via DI and call execute(SomeAction.class, params) from anywhere — a Spring @RestController (shown), a Quarkus resource, a scheduled job, a CLI command. The executor handles discovery, perform(), the transaction, and the atomic commit.
1
@RestController
2
@RequestMapping("/wallets")
3
public class WalletController {
4
5
private final ActionExecutor executor;
6
// …
7
8
@PostMapping("/{id}/deposit")
9
public Wallet deposit(@PathVariable UUID id, @RequestBody Body body) throws Exception {
10
return executor.execute(() -> "rest-user", WalletDepositAction.class,
11
new Params(Id.of(Wallet.class, id), body.amount()));
12
}
13
}
A short subclass of ModelRepository. Inherits getById / add / update; custom queries written in the typed jOOQ DSL when you need them — no JPA, no annotations soup.
1
@EkbatanRepository
2
public class WalletRepository extends ModelRepository<Wallet, …> {
3
4
public List<Wallet> findAllByOwnerId(UUID ownerId) {
5
return readonlyDb()
6
.selectFrom(WALLETS)
7
.where(WALLETS.OWNER_ID.eq(ownerId))
8
.fetch(this::fromRecord);
9
}
10
// …
11
}
The model the framework rests on. Dual-write, outbox-by-construction, the two-phase action lifecycle, sharding theory. Read once and the rest reads like notation.
Step-by-step tutorials with a stack picker that follows you across every lesson. Spring Boot / Quarkus / Micronaut · Gradle / Maven · PG / MariaDB / MySQL — pick once, get matching snippets everywhere.
Every class, every config, every dialect specific. Signatures, semantics, source links. If you already know what you're looking for, start here.