Author: Miguel Carvalho
Contents
- How we compared
- What a transactional outbox is and how the six libraries differ
- Spring Boot and Java support
- Outbox for Kafka and RabbitMQ: brokers and databases
- Retries, backoff and DLQ
- Polling and per-event-type tuning
- Inbox pattern: consumer-side message deduplication
- Observability and operations
- Maturity and maintenance
- Example: outbox in Spring Boot with Kafka
- Pros and cons
- Final scores
- Who should choose what
In short
These six projects are not interchangeable. Tandem and oncebox provide ready-made delivery from Spring Boot to a broker, while spring-boot-outbox-starter leaves delivery to you. ekbatan works as an outbox only if you rewrite your entire data access layer from Spring Data and JPA to its own, so it is practically unsuitable for existing Spring projects; transactional-event targets Jakarta EE and Quarkus, and boxy is still an experiment with no releases. For Kafka on PostgreSQL with multiple instances and an Admin API, Tandem is the better fit; for Kafka or RabbitMQ on PostgreSQL, MySQL or Oracle with a DLQ and an inbox, oncebox.
- Tandem for Kafka and PostgreSQL, multiple instances, Admin API
- oncebox for Kafka or RabbitMQ, PostgreSQL, MySQL or Oracle, separate polling, retry and backoff settings per event type, a DLQ and consumer-side deduplication in a single starter (i think this is the most versatile and rich starter)
- spring-boot-outbox-starter for PostgreSQL and delivery beyond brokers (REST, webhooks), several isolated channels
- ekbatan for a new project on Java 25, and only if you are willing to drop Spring Data in favour of JOOQ and take on heavy infrastructure with Debezium
- transactional-event for Jakarta EE (WildFly, Liberty) or Quarkus rather than Spring
- boxy for exploring the idea of "outbox as an event stream"; not production-ready yet
Libraries compared
| Library | Artifact | Version | License | Links |
|---|---|---|---|---|
| Tandem | com.codingful:tandem-spring-relay | 1.0.0-rc.3 | Apache-2.0 | site · repo |
| spring-boot-outbox-starter | com.kholodilin:spring-boot-outbox-starter | 0.2.3 | Apache-2.0 | site · repo |
| oncebox | io.github.dmitriy-iliyov:oncebox-starter | 2.0.0 | Apache-2.0 | site · repo |
| ekbatan | io.github.ekbatan-io:ekbatan-spring-boot-starter | 1.1.3 | Apache-2.0 | site · repo |
| transactional-event | io.github.jonasrutishauser:transactional-event-core | 3.4.2 | LGPL-3.0 | site · repo |
| boxy | mvsm:boxy-core (not published) | 1.0-SNAPSHOT | Apache-2.0 | site · repo |
Criteria
- What it is. These six projects mean different things by "outbox": a starter that delivers to a broker, a persistence layer, event handlers, or an event stream inside the database. That determines what you will have to write yourself.
- Platform and Java. The library has to fit your stack without switching frameworks or upgrading the JDK just for its sake.
- Where it delivers. A library is useless if it cannot deliver to where your services publish.
- Databases. The outbox table lives in the same database as your business data, so the DBMS is already chosen for you, and the library has to support it.
- Failures and DLQ. A broker or consumer can be unavailable for hours, and what matters is what happens to an event once retries are exhausted.
- Polling and fine-tuning. Events within one service are not equal: a payment must go out in a fraction of a second, while an analytics event can wait a minute, and identical settings for all of them either load the database or delay what matters.
- Consumer-side deduplication. An outbox gives at-least-once delivery, so duplicates are unavoidable, and something has to filter them out.
- Observability and operations. You need to notice a growing outbox table before your users do, and be able to deal with stuck events without SQL.
- Maturity and maintenance. All six projects are young or niche, so API stability, releases on Maven Central and the number of maintainers matter more than stars.
Outbox libraries compared by criteria
| Criterion | Tandem | spring-boot-outbox-starter | oncebox | ekbatan | transactional-event | boxy |
|---|---|---|---|---|---|---|
| What it is | Outbox library: table write + relay to a broker; Spring starters for both parts | Spring Boot starter: outbox table + workers; you write delivery yourself (OutboxSink) | Spring Boot starter: outbox with broker delivery + consumer-side inbox | Replaces Spring Data and JPA with its own JOOQ-based layer; the outbox only covers changes made through that layer, delivery to a broker via Debezium | Transactional events for Jakarta EE: table write + asynchronous handlers | Event stream on top of an outbox table: partitions, subscriptions and cursors in the database, no broker |
| Platform and Java | Java 17+; Spring Boot, also works without Spring | Java 21; Spring Boot | Java 17+; Spring Boot | Java 25; Spring Boot, Quarkus, Micronaut or no DI; GraalVM native | Java 11; Jakarta EE 9+ (CDI, JTA) and Quarkus; Spring not supported | Java 17; no framework integration |
| Where it delivers | Kafka; RabbitMQ (tandem-rabbitmq 0.2.0); CloudEvents format | Any target via your own OutboxSink (Kafka and REST in the demos) | Kafka 3.7+, RabbitMQ 3.12+ (Kafka and Rabbit in the demos) | Kafka via Debezium + Kafka Connect; in-process handlers | Your own code in the handler (no built-in broker) | No broker: consumers read events from the database |
| Databases | PostgreSQL 14+ (MySQL designed but not implemented) | PostgreSQL 13+ | PostgreSQL 16+, MySQL 8.0.13+, Oracle 23+ | PostgreSQL, MySQL, MariaDB | Any JDBC database via DataSource (DDL in README) | MySQL 8 (schema and stored procedures); PostgreSQL planned |
| Failures and DLQ | Retries with backoff; after 10 attempts the event is blocked; replay/discard via Admin API | max-retries, then DEAD status; RecoveryWorker for stuck records | Per-event-type retries and backoff, recovery of stuck events, DLQ with REST API | For handlers: exponential backoff, EXPIRED status after 7 days | Retries, then blocked; unblocked metric | The cursor does not advance until the partition is processed |
| Polling and fine-tuning | Adaptive polling interval (10-100 ms by default), optional pg-notify on PostgreSQL; settings shared across the relay | Wake-up queue in memory or Redis; batch, retries and lease set per channel (table) | Fixed or adaptive polling; batch size, polling, retry count and backoff set separately per event type | Debezium reads the table from the database log; background jobs for in-process handlers | Polling interval between initial and maximum; shared settings | Adaptive consumer heartbeats and lease acquisition |
| Consumer-side deduplication | No; the consumer deduplicates by event id | No | Yes: effectively-once and at-most-once consumers, Redis cache | For in-process handlers: one row per event × handler pair | No | Subscription cursors in the database |
| Observability and operations | Micrometer, OpenTelemetry, Admin REST API, CLI, Grafana demo | Micrometer, health indicator | Micrometer, Grafana dashboards, REST API for the DLQ | OpenTelemetry tracing | MicroProfile Metrics | Subscription statistics in tables |
| Maturity and maintenance | 1.0.0-rc.3 from 2026-11-05, API frozen; one main author | 0.2.3 from 2026-10-28; one author | 2.0.0 from 2026-10-15; one author | 1.1.3 from 2026-11-01; one main author | 3.4.2 from 2026-10-12; developed since 2021, one author | No releases, not published on Maven Central; last commit 2025-08-15 |
You don't have to write a transactional outbox and inbox in Spring Boot yourself: Java has ready-made libraries. This article compares six young projects that rarely show up in reviews: Tandem, oncebox, spring-boot-outbox-starter, ekbatan, transactional-event and boxy. In short: Tandem and oncebox provide ready-made event delivery from Spring Boot to Kafka; only oncebox covers RabbitMQ and an inbox for consumer-side message deduplication. Below, the libraries are compared against the same criteria. All versions and facts were checked on 7 November 2026.
How we compared
Each library was assessed against the nine criteria in the table below, the same for all of them. Facts come from READMEs, documentation, pom.xml / build.gradle.kts, Git tags and Maven Central, and each one has a source listed at the end of the article. Where the documentation is silent, we say so. More on the approach on the Methodology page.
What a transactional outbox is and how the six libraries differ
The main difference between these libraries is not just their feature set, but what each of them calls an outbox. Before comparing details, it is worth understanding which type each one belongs to.
- Tandem and oncebox are full outbox libraries (out-of-the-box solutions): you write an event to a table within your transaction, and the library delivers it to Kafka or RabbitMQ.
- spring-boot-outbox-starter handles the table, queue, workers, retries and recovery. In exchange, the target can be anything: Kafka, REST, a webhook.
- ekbatan is a JOOQ-based persistence framework: it does not complement but replaces Spring Data and Hibernate, and writes events to the
eventlog.eventstable on every model change. Debezium sends them to Kafka. - transactional-event provides transactional events for Jakarta EE: the event is stored in a table together with the transaction and later processed asynchronously by your
Handler. - boxy is an attempt to turn the outbox table into an event stream with partitions, subscriptions and cursors right in the database, so that a monolith needs no broker at all.
For a typical Spring Boot microservice that has to send events to a broker, only the first three are truly comparable. The other three are worth considering only if their model suits you.
Why ekbatan doesn't fit an existing Spring Data project
ekbatan's limitation matters more than all its other characteristics. According to its documentation, Spring Data JPA can run in the same application, but ekbatan does not integrate with JpaRepository and does not support Spring @Transactional or PlatformTransactionManager: it has its own TransactionManager, and its Action serves as the transaction boundary. An event is written atomically only together with changes made through ActionExecutor. If an order is saved via JPA and the event via ekbatan, these are two separate transactions — exactly the dual-write problem the outbox is supposed to protect against.
So to get an outbox, you have to move your entire data access layer to ekbatan: repositories in JOOQ, models as immutable ekbatan classes, business operations as Actions. In my view, rewriting a layer as fundamental and battle-tested as Spring Data and JPA just for an outbox is an unjustified risk for most projects. The other libraries in this comparison plug into the existing Spring transaction and store the event right next to your usual repository.save(...).
Spring Boot and Java support
In Tandem, Spring is a compileOnly dependency, so the Spring version is determined by your application's BOM.
| Library | Spring Boot | Java | Without Spring |
|---|---|---|---|
| Tandem | yes | 17+ | yes, dependency-free core |
| oncebox | yes | 17+ | no |
| spring-boot-outbox-starter | yes | 21 | Spring-free core |
| ekbatan | yes | 25 | Quarkus, Micronaut, plain Java |
| transactional-event | no | 11 | Jakarta EE 9+, Quarkus |
| boxy | no | 17 | plain Java only |
If your project runs on Spring Boot and Java 21, oncebox, Tandem and spring-boot-outbox-starter all fit.
Outbox for Kafka and RabbitMQ: brokers and databases
Tandem and oncebox support Kafka out of the box. oncebox also supports RabbitMQ 3.12+ with the same starter, API and settings: the broker is selected by the oncebox-kafka or oncebox-rabbit module and the oncebox.outbox.sender.type property. Tandem's RabbitMQ adapter (tandem-rabbitmq) is recent and versioned separately from the library (0.2.0 versus 1.0.0-rc.3). By default Tandem wraps events in a CloudEvents envelope; a custom format is plugged in by implementing a single interface.
spring-boot-outbox-starter has no built-in transport: the demo projects include sinks for Kafka and REST, but in production you write your own. That is a drawback for the typical "send to Kafka" case, but an advantage if you need an outbox for webhooks or external API calls. Channels are isolated from each other: each has its own table, queue and retry settings.
oncebox has the widest database support: PostgreSQL 16+, MySQL 8.0.13+ and Oracle 23+, each as a separate dialect module. On MySQL, the outbox data source must be switched to the READ COMMITTED isolation level: under InnoDB's default REPEATABLE READ, a SKIP LOCKED query takes gap locks, and multiple instances block each other instead of picking up different batches. PostgreSQL and Oracle don't have this problem. ekbatan supports PostgreSQL, MySQL and MariaDB. Tandem and spring-boot-outbox-starter work only with PostgreSQL; MySQL support in Tandem is designed but, according to the author, not implemented. boxy currently contains a schema and stored procedures for MySQL only. transactional-event needs just one table from the database, with the DDL given in the README.
Retries, backoff and DLQ
Each library decides differently what to do with an event that cannot be delivered.
oncebox offers the full set: retries and backoff are configured per event type, events "stuck" in IN_PROCESS after an instance crash are returned to the queue without counting the failed attempt, and once attempts are exhausted the event is atomically moved to the DLQ. The oncebox-dlq-api module adds a REST API for the DLQ: view, requeue or delete. The DLQ is disabled by default.
Tandem retries sending with exponential backoff, and after maxAttempts (10 by default) the event is blocked. The blocked.count metric shows the scale of the problem, and the Admin API (tandem-admin, disabled by default) lets you resend or discard the event. It also lets you pause and resume the relay.
spring-boot-outbox-starter moves an event to DEAD status after max-retries attempts, and RecoveryWorker picks up records with an expired lease. There is no API for working with DEAD records, only SQL.
ekbatan, for in-process handlers, retries the call with bounded exponential backoff and moves the notification to EXPIRED if the event is older than the retention window (7 days by default). transactional-event marks an event as blocked once retries are exhausted; metrics count both blocked and unblocked events.
Polling and per-event-type tuning
oncebox is the only one of the six where outbox behaviour is configured separately for each event type. For order-created and analytics-tracked you can set a different batch size, polling strategy, retry count and backoff, and anything not overridden is inherited from the shared defaults block.
This solves a practical problem: events within one service are not equal. A payment or an order status change must go out in a fraction of a second and be retried quickly, while analytics or search events can be sent in large batches once a minute and retried less often. With shared settings you have to choose: either poll the database often for all types at once, or delay important events along with unimportant ones.
oncebox: outbox: defaults: batch-size: 200 polling: type: adaptive min-fixed-delay: 250ms max-fixed-delay: 1m multiplier: 1.5 max-retries: 3 backoff: delay: 10s multiplier: 3.0 events: payment-completed: topic: payments polling: min-fixed-delay: 100ms # poll more often, the rest comes from defaults max-retries: 10 backoff: delay: 1s multiplier: 2.0 analytics-tracked: topic: analytics batch-size: 1000 polling: type: fixed fixed-delay: 1m # once a minute in large batches backoff: enabled: false Adaptive polling is enabled by default in oncebox: while there are no events, the pause between polls grows by the multiplier up to max-fixed-delay, and as soon as events appear it resets to min-fixed-delay. This way the service reacts quickly to an event stream and puts almost no load on the database when idle. According to the calculation in the oncebox walkthrough on DEV Community, that is about 70 polls per idle hour versus 14,400 with a fixed 250 ms pause; this is an estimate based on the scheduling rule, not a measurement. Retries use exponential backoff: with a 10 s delay and a multiplier of 3, retries happen after 10 s, 30 s, 90 s, 270 s.
Tandem also has adaptive polling: the interval stays at 10 ms while a bucket has events and grows to 100 ms when idle, and on PostgreSQL you can enable instant wake-up via pg-notify. But Tandem's settings are shared across the whole relay and change only on restart. spring-boot-outbox-starter configures batches, retries and leases per channel, that is, per separate table, not per event type within one table. transactional-event has shared polling settings for all events, and ekbatan delegates reading the table to Debezium.
Inbox pattern: consumer-side message deduplication
Inbox is the companion pattern to outbox on the consumer side: it stores the ids of already processed messages and prevents a business operation from running twice. It is needed because any outbox guarantees only at-least-once: if the application crashes after sending to the broker but before marking the record in the table, the event will be sent again. So the consumer has to be able to filter out duplicates.
Only oncebox has a ready-made inbox for a Kafka or RabbitMQ consumer. Since version 2.0.0 it has two idempotent consumers: a transactional one, where the receipt record and the business operation are committed together (effectively-once), and a non-transactional one, where the record is committed before the operation (at-most-once, for side effects such as emails that must not be repeated). The optional oncebox-inbox-cache module adds a Redis cache of records. Records are kept until cleanup (oncebox.inbox.clean-up.ttl, 24 hours by default), so a redelivery after that period is no longer recognised as a duplicate. In practice this leads to three rules: the inbox retention period must cover the longest redelivery window (for example, re-reading old Kafka offsets), the cache TTL must not exceed the inbox retention period, and the cache name must be unique per service, otherwise services sharing a Redis instance will treat each other's events as already processed. The consumer uses the same oncebox-starter, with no separate library.
Tandem does not provide an inbox: each event has a unique id, and the consumer writes deduplication by that id. ekbatan, for in-process handlers, creates one record per "event × handler" pair and ignores repeated inserts, but Kafka consumers downstream of Debezium deduplicate on their own. spring-boot-outbox-starter and transactional-event have no inbox. boxy stores subscription positions in the database and, according to its README, protects against duplicates when a lease is handed over between consumers.
Observability and operations
Tandem has the most operational tooling: Micrometer metrics (backlog age, errors, blocked events, worker health), tracing via Micrometer Tracing or OpenTelemetry, an Admin REST API with an OpenAPI contract, the tandem-cli console tool, and ready-made Grafana demos. The author also publishes benchmarks with raw data: a median latency of 13 ms from COMMIT to Kafka acknowledgement at 600 events/s on a 2 vCPU machine that the relay shares with PostgreSQL and Kafka.
oncebox provides Micrometer metrics for outbox and inbox (the oncebox-metrics module), Grafana dashboards in the repository and a REST API for the DLQ. spring-boot-outbox-starter publishes metrics per channel and event type (outbox_publish_total, outbox_queue_pressure) and a health indicator. ekbatan has OpenTelemetry tracing, transactional-event has MicroProfile metrics.
Maturity and maintenance
All six projects have a single main author, so it is worth looking at versions, release frequency and API stability.
- oncebox 2.0.0 was released on 15 October 2026; it was preceded by 1.x releases. The project has been developed since October 2025.
- Tandem is a release candidate: 1.0.0-rc.3 from 5 November 2026, with the API frozen until 1.0.0. The project started in July 2026 and had about 390 commits by November.
- spring-boot-outbox-starter is at version 0.2.3 from 28 October 2026; the project started in August 2026. A 0.x version means the API may still change; 0.2.0 already included a breaking change to internal classes.
- ekbatan 1.1.3 was released on 1 November 2026; the project has been developed since November 2025.
- transactional-event is the oldest of the six: developed since 2021, version 3.4.2 from 12 October 2026. It is the only one here under LGPL-3.0; the rest are Apache-2.0.
- boxy has no releases and is not published on Maven Central (version 1.0-SNAPSHOT); the last commit was on 15 August 2025. The README roadmap (versions planned for August and September 2025) has not been delivered.
Example: outbox in Spring Boot with Kafka
The task is the same for the three starters that send events from Spring Boot: within a transaction, save an order and write an OrderCreated event to the outbox. The examples are taken from the libraries' READMEs and shortened.
oncebox
Dependencies: io.github.dmitriy-iliyov:oncebox-starter, oncebox-postgresql and oncebox-kafka version 2.0.0; @EnableOncebox on the main class.
oncebox: outbox: sender: type: kafka events: order-created: topic: "orders" @Service @RequiredArgsConstructor public class OrderService { private final OrderRepository repository; private final OutboxPublisher outboxPublisher; @Transactional public Order create(Order order) { Order saved = repository.save(order); outboxPublisher.publish("order-created", saved); return saved; } } Instead of calling publish, you can put @OutboxPublish(eventType = "order-created") on the method: the return value then becomes the event.
Tandem
Dependencies: the com.codingful:tandem-bom:1.0.0-rc.3 BOM, tandem-spring-producer where events are written, tandem-spring-relay and tandem-kafka where the relay runs.
@Transactional public Order create(Order order) { orderRepository.save(order); outboxRepository.insert(OutboxMessage.builder() .aggregateId(order.id()) .aggregateType("Order") .type("com.acme.order.created") .payload(serialize(order)) .contentType("application/json") .build()); return order; } In Spring, instead of the low-level OutboxRepository, you can use TransactionalOutboxTemplate, the @TransactionalOutbox annotation, or Spring application events.
spring-boot-outbox-starter
Dependency: com.kholodilin:spring-boot-outbox-starter:0.2.3.
@Transactional public void create(Order order) { orderRepository.save(order); outboxService .eventType("ORDER_CREATED") .aggregateId(String.valueOf(order.id())) .payload(order) .append(); } @Component class KafkaOrdersSink implements OutboxSink { @Override public OutboxPublishResult publish(List<OutboxRecord> batch) { // send the batch to Kafka with your own code return new OutboxPublishResult.AllSucceeded(); } } Pros and cons
Tandem
- Pros: instance coordination via leases, Admin API and CLI, detailed metrics and tracing, published benchmarks, the relay can run as a separate container.
- Cons: still a release candidate; PostgreSQL only; RabbitMQ adapter at 0.x; no inbox;
bucketCountcannot be changed after the first deployment.
oncebox
- Pros: polling (fixed or adaptive), batch size, retries and backoff are configured separately per event type; adaptive polling by default; outbox and inbox in one starter; Kafka and RabbitMQ; PostgreSQL, MySQL and Oracle; DLQ with a REST API; minimal code — one call or an annotation.
spring-boot-outbox-starter
- Pros: delivery anywhere via
OutboxSink; isolated channels with their own tables and settings; a Redis queue to wake up workers across instances. - Cons: you write the broker transport yourself; PostgreSQL only; version 0.x; no API for
DEADevents.
ekbatan
- Pros: events are stored automatically together with the model change, no separate outbox call needed; Spring Boot, Quarkus and Micronaut; GraalVM native image; sharding; distributed jobs.
- Cons: the main one — the outbox only works if you fully replace Spring Data and JPA with its own JOOQ-based layer; it does not integrate with
JpaRepositoryor Spring@Transactional, so its outbox cannot be embedded in an existing project; requires Java 25; Kafka requires Debezium and Kafka Connect.
transactional-event
- Pros: the longest history of the six; standard Jakarta EE APIs; a Quarkus extension; works with any JDBC database.
- Cons: not for Spring; no built-in broker, you write sending in the handler; LGPL-3.0 license.
boxy
- Pros: an interesting model — partitions, subscriptions and weighted balancing on top of the database, with no broker.
- Cons: no releases and no artifact on Maven Central; MySQL only; development stopped in August 2025; no framework integration.
Final scores
The article's nine criteria are combined into seven columns: "What it is" and "Platform and Java" are scored together as "Spring Boot integration", and "Where it delivers" and "Databases" together as "Brokers and databases". Each score from 1 to 10 is the author's opinion based on the facts in the comparison table and the sections above; the total is a simple unweighted average of the seven columns. If one criterion matters more to you than the others (for example, the inbox), look at its column rather than the total.
| Library | Total | Spring Boot integration | Brokers and databases | Failures and DLQ | Polling and tuning | Inbox | Observability | Maturity |
|---|---|---|---|---|---|---|---|---|
| oncebox | 8.8 | 9 | 9 | 9.5 | 9.5 | 9.5 | 8 | 7 |
| Tandem | 7.0 | 8 | 6 | 8.5 | 7.5 | 3 | 9.5 | 6.5 |
| spring-boot-outbox-starter | 5.3 | 6.5 | 5 | 6 | 6.5 | 1 | 7 | 5 |
| ekbatan | 4.7 | 2.5 | 5 | 5 | 4 | 4 | 6 | 6.5 |
| transactional-event | 3.9 | 1 | 3 | 6 | 4 | 1 | 5 | 7.5 |
| boxy | 2.8 | 1.5 | 2 | 3 | 5 | 4 | 3 | 1 |
Who should choose what
- A Spring Boot microservice, Kafka or RabbitMQ. oncebox: a starter, a single
publishcall, a DLQ and an inbox for the consumer in the same library. - Events of different importance in one service: payments must go out instantly, analytics in batches. oncebox: polling, batch size, retries and backoff are set separately for each event type.
- A service on MySQL or Oracle. oncebox is the only one of the three Spring starters that supports these databases.
- An outbox for webhooks and external HTTP APIs rather than a broker. oncebox: a starter has NonTransactionalConsumer for this.
- Jakarta EE or Quarkus. transactional-event.
FAQ
What are the transactional outbox and inbox patterns?
Transactional outbox is a pattern in which a service writes an event to an outbox table in the same transaction as the business data, and a separate process sends it to Kafka or RabbitMQ. This way the event is never lost and never sent without the data change. Inbox is the companion pattern on the consumer side: it remembers the ids of processed messages and filters out the duplicates that an at-least-once outbox inevitably produces.
How do I build an inbox and an idempotent Kafka consumer in Spring Boot?
Of the six libraries compared, only oncebox ships a ready-made inbox for Kafka and RabbitMQ: the idempotent consumer wraps processing in a @KafkaListener or @RabbitListener and records the event id in the database. The transactional variant commits the record together with the business operation (effectively-once), the non-transactional one commits it before the operation (at-most-once). With the other libraries, deduplication by event id is written by hand.
Which Spring Boot outbox library supports both Kafka and RabbitMQ?
oncebox supports Kafka 3.7+ (the oncebox-kafka module) and RabbitMQ 3.12+ (oncebox-rabbit). Tandem delivers to Kafka and supports RabbitMQ through a separate tandem-rabbitmq 0.2.0 module with its own version. spring-boot-outbox-starter is broker-agnostic: you write delivery to Kafka or RabbitMQ yourself in an OutboxSink implementation.
Does transactional-event work with Spring Boot?
No. transactional-event is written for Jakarta EE 9+ and requires CDI 3.0, JTA 2.0 and Concurrency Utilities 2.0; there is a separate Quarkus extension. The project has no Spring integration.
Can ekbatan be added to an existing Spring Data JPA project?
Not as an outbox. Spring Data JPA can run in the same application, but ekbatan does not integrate with JpaRepository and does not support Spring @Transactional, and events are atomic only with changes made through its Action and ActionExecutor. A JPA entity change and an ekbatan event will not end up in one transaction, so for an outbox you would have to rewrite the data access layer in JOOQ. On top of that, ekbatan requires Java 25.
Do these libraries need Debezium?
Only ekbatan, if events must go to Kafka: there they are published by Debezium with the project's SMT plugins. Tandem and oncebox send events themselves by polling the outbox table, spring-boot-outbox-starter calls your OutboxSink, and boxy and transactional-event work without a broker.
About the author
Miguel Carvalho. Java and Kotlin developer with 7 years of Spring experience and more than 5 years working on distributed systems with Kafka and RabbitMQ. Active open source contributor.
Sources
- Tandem: README (checked November 7, 2026)
- Tandem: documentation (checked November 7, 2026)
- oncebox: README (checked November 7, 2026)
- oncebox 2.0.0: release notes (checked November 7, 2026)
- Transactional Outbox and Inbox in Spring Boot (DEV Community) (checked November 7, 2026)
- spring-boot-outbox-starter: README (checked November 7, 2026)
- ekbatan: README (checked November 7, 2026)
- ekbatan: Wiring with Spring Boot (Spring Data JPA and @Transactional) (checked November 7, 2026)
- ekbatan: In-process event handlers (checked November 7, 2026)
- transactional-event: README (checked November 7, 2026)
- boxy: README (checked November 7, 2026)
- Maven Central: latest artifact versions (checked November 7, 2026)

