<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Miguel Carvalho</title>
    <description>The latest articles on DEV Community by Miguel Carvalho (@miguel_carvalho_22b80551c).</description>
    <link>https://dev.to/miguel_carvalho_22b80551c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4235900%2F20c08f2c-3050-4a28-94e6-a175f99be79b.png</url>
      <title>DEV Community: Miguel Carvalho</title>
      <link>https://dev.to/miguel_carvalho_22b80551c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/miguel_carvalho_22b80551c"/>
    <language>en</language>
    <item>
      <title>My first article</title>
      <dc:creator>Miguel Carvalho</dc:creator>
      <pubDate>Tue, 10 Nov 2026 01:21:39 +0000</pubDate>
      <link>https://dev.to/miguel_carvalho_22b80551c/-2dm7</link>
      <guid>https://dev.to/miguel_carvalho_22b80551c/-2dm7</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/miguel_carvalho_22b80551c/transactional-outbox-and-inbox-for-spring-boot-which-library-to-choose-in-2026-3cpo" class="crayons-story__hidden-navigation-link"&gt;Transactional outbox and inbox for Spring Boot: which library to choose in 2026&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/miguel_carvalho_22b80551c" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4235900%2F20c08f2c-3050-4a28-94e6-a175f99be79b.png" alt="miguel_carvalho_22b80551c profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/miguel_carvalho_22b80551c" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Miguel Carvalho
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Miguel Carvalho
                
                
              
              &lt;div id="story-author-preview-content-5007780" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/miguel_carvalho_22b80551c" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4235900%2F20c08f2c-3050-4a28-94e6-a175f99be79b.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Miguel Carvalho&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/miguel_carvalho_22b80551c/transactional-outbox-and-inbox-for-spring-boot-which-library-to-choose-in-2026-3cpo" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 10&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/miguel_carvalho_22b80551c/transactional-outbox-and-inbox-for-spring-boot-which-library-to-choose-in-2026-3cpo" id="article-link-5007780"&gt;
          Transactional outbox and inbox for Spring Boot: which library to choose in 2026
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/springboot"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;springboot&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/java"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;java&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/miguel_carvalho_22b80551c/transactional-outbox-and-inbox-for-spring-boot-which-library-to-choose-in-2026-3cpo" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;4&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/miguel_carvalho_22b80551c/transactional-outbox-and-inbox-for-spring-boot-which-library-to-choose-in-2026-3cpo#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            20 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Transactional outbox and inbox for Spring Boot: which library to choose in 2026</title>
      <dc:creator>Miguel Carvalho</dc:creator>
      <pubDate>Tue, 10 Nov 2026 01:20:58 +0000</pubDate>
      <link>https://dev.to/miguel_carvalho_22b80551c/transactional-outbox-and-inbox-for-spring-boot-which-library-to-choose-in-2026-3cpo</link>
      <guid>https://dev.to/miguel_carvalho_22b80551c/transactional-outbox-and-inbox-for-spring-boot-which-library-to-choose-in-2026-3cpo</guid>
      <description>&lt;p&gt;Author: Miguel Carvalho&lt;/p&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;How we compared&lt;/li&gt;
&lt;li&gt;What a transactional outbox is and how the six libraries differ&lt;/li&gt;
&lt;li&gt;Spring Boot and Java support&lt;/li&gt;
&lt;li&gt;Outbox for Kafka and RabbitMQ: brokers and databases&lt;/li&gt;
&lt;li&gt;Retries, backoff and DLQ&lt;/li&gt;
&lt;li&gt;Polling and per-event-type tuning&lt;/li&gt;
&lt;li&gt;Inbox pattern: consumer-side message deduplication&lt;/li&gt;
&lt;li&gt;Observability and operations&lt;/li&gt;
&lt;li&gt;Maturity and maintenance&lt;/li&gt;
&lt;li&gt;Example: outbox in Spring Boot with Kafka&lt;/li&gt;
&lt;li&gt;Pros and cons&lt;/li&gt;
&lt;li&gt;Final scores&lt;/li&gt;
&lt;li&gt;Who should choose what&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  In short
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tandem&lt;/strong&gt; for Kafka and PostgreSQL, multiple instances, Admin API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;oncebox&lt;/strong&gt; 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)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spring-boot-outbox-starter&lt;/strong&gt; for PostgreSQL and delivery beyond brokers (REST, webhooks), several isolated channels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ekbatan&lt;/strong&gt; 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&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transactional-event&lt;/strong&gt; for Jakarta EE (WildFly, Liberty) or Quarkus rather than Spring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;boxy&lt;/strong&gt; for exploring the idea of "outbox as an event stream"; not production-ready yet&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Libraries compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Links&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tandem&lt;/td&gt;
&lt;td&gt;&lt;code&gt;com.codingful:tandem-spring-relay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.0.0-rc.3&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://tandem.codingful.com" rel="noopener noreferrer"&gt;site&lt;/a&gt; · &lt;a href="https://github.com/alirux/tandem-transactional-outbox-kafka" rel="noopener noreferrer"&gt;repo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-boot-outbox-starter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;com.kholodilin:spring-boot-outbox-starter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.2.3&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/KHolodilin/spring-boot-outbox-starter" rel="noopener noreferrer"&gt;site&lt;/a&gt; · &lt;a href="https://github.com/KHolodilin/spring-boot-outbox-starter" rel="noopener noreferrer"&gt;repo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;oncebox&lt;/td&gt;
&lt;td&gt;&lt;code&gt;io.github.dmitriy-iliyov:oncebox-starter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.0.0&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/dmitriy-iliyov/oncebox" rel="noopener noreferrer"&gt;site&lt;/a&gt; · &lt;a href="https://github.com/dmitriy-iliyov/oncebox" rel="noopener noreferrer"&gt;repo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ekbatan&lt;/td&gt;
&lt;td&gt;&lt;code&gt;io.github.ekbatan-io:ekbatan-spring-boot-starter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.1.3&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://ekbatan-io.github.io/ekbatan/" rel="noopener noreferrer"&gt;site&lt;/a&gt; · &lt;a href="https://github.com/ekbatan-io/ekbatan" rel="noopener noreferrer"&gt;repo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transactional-event&lt;/td&gt;
&lt;td&gt;&lt;code&gt;io.github.jonasrutishauser:transactional-event-core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3.4.2&lt;/td&gt;
&lt;td&gt;LGPL-3.0&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://jonasrutishauser.github.io/transactional-event/" rel="noopener noreferrer"&gt;site&lt;/a&gt; · &lt;a href="https://github.com/jonasrutishauser/transactional-event" rel="noopener noreferrer"&gt;repo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boxy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mvsm:boxy-core (not published)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.0-SNAPSHOT&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/rbilleci/boxy" rel="noopener noreferrer"&gt;site&lt;/a&gt; · &lt;a href="https://github.com/rbilleci/boxy" rel="noopener noreferrer"&gt;repo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Criteria
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform and Java.&lt;/strong&gt; The library has to fit your stack without switching frameworks or upgrading the JDK just for its sake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it delivers.&lt;/strong&gt; A library is useless if it cannot deliver to where your services publish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databases.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures and DLQ.&lt;/strong&gt; A broker or consumer can be unavailable for hours, and what matters is what happens to an event once retries are exhausted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polling and fine-tuning.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer-side deduplication.&lt;/strong&gt; An outbox gives at-least-once delivery, so duplicates are unavoidable, and something has to filter them out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability and operations.&lt;/strong&gt; You need to notice a growing outbox table before your users do, and be able to deal with stuck events without SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maturity and maintenance.&lt;/strong&gt; All six projects are young or niche, so API stability, releases on Maven Central and the number of maintainers matter more than stars.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Outbox libraries compared by criteria
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Tandem&lt;/th&gt;
&lt;th&gt;spring-boot-outbox-starter&lt;/th&gt;
&lt;th&gt;oncebox&lt;/th&gt;
&lt;th&gt;ekbatan&lt;/th&gt;
&lt;th&gt;transactional-event&lt;/th&gt;
&lt;th&gt;boxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it is&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Outbox library: table write + relay to a broker; Spring starters for both parts&lt;/td&gt;
&lt;td&gt;Spring Boot starter: outbox table + workers; you write delivery yourself (OutboxSink)&lt;/td&gt;
&lt;td&gt;Spring Boot starter: outbox with broker delivery + consumer-side inbox&lt;/td&gt;
&lt;td&gt;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&lt;/td&gt;
&lt;td&gt;Transactional events for Jakarta EE: table write + asynchronous handlers&lt;/td&gt;
&lt;td&gt;Event stream on top of an outbox table: partitions, subscriptions and cursors in the database, no broker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Platform and Java&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Java 17+; Spring Boot, also works without Spring&lt;/td&gt;
&lt;td&gt;Java 21; Spring Boot&lt;/td&gt;
&lt;td&gt;Java 17+; Spring Boot&lt;/td&gt;
&lt;td&gt;Java 25; Spring Boot, Quarkus, Micronaut or no DI; GraalVM native&lt;/td&gt;
&lt;td&gt;Java 11; Jakarta EE 9+ (CDI, JTA) and Quarkus; Spring not supported&lt;/td&gt;
&lt;td&gt;Java 17; no framework integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Where it delivers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kafka; RabbitMQ (tandem-rabbitmq 0.2.0); CloudEvents format&lt;/td&gt;
&lt;td&gt;Any target via your own OutboxSink (Kafka and REST in the demos)&lt;/td&gt;
&lt;td&gt;Kafka 3.7+, RabbitMQ 3.12+  (Kafka and Rabbit in the demos)&lt;/td&gt;
&lt;td&gt;Kafka via Debezium + Kafka Connect; in-process handlers&lt;/td&gt;
&lt;td&gt;Your own code in the handler (no built-in broker)&lt;/td&gt;
&lt;td&gt;No broker: consumers read events from the database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Databases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL 14+ (MySQL designed but not implemented)&lt;/td&gt;
&lt;td&gt;PostgreSQL 13+&lt;/td&gt;
&lt;td&gt;PostgreSQL 16+, MySQL 8.0.13+, Oracle 23+&lt;/td&gt;
&lt;td&gt;PostgreSQL, MySQL, MariaDB&lt;/td&gt;
&lt;td&gt;Any JDBC database via DataSource (DDL in README)&lt;/td&gt;
&lt;td&gt;MySQL 8 (schema and stored procedures); PostgreSQL planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failures and DLQ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Retries with backoff; after 10 attempts the event is blocked; replay/discard via Admin API&lt;/td&gt;
&lt;td&gt;max-retries, then DEAD status; RecoveryWorker for stuck records&lt;/td&gt;
&lt;td&gt;Per-event-type retries and backoff, recovery of stuck events, DLQ with REST API&lt;/td&gt;
&lt;td&gt;For handlers: exponential backoff, EXPIRED status after 7 days&lt;/td&gt;
&lt;td&gt;Retries, then blocked; unblocked metric&lt;/td&gt;
&lt;td&gt;The cursor does not advance until the partition is processed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Polling and fine-tuning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adaptive polling interval (10-100 ms by default), optional pg-notify on PostgreSQL; settings shared across the relay&lt;/td&gt;
&lt;td&gt;Wake-up queue in memory or Redis; batch, retries and lease set per channel (table)&lt;/td&gt;
&lt;td&gt;Fixed or adaptive polling; batch size, polling, retry count and backoff set separately per event type&lt;/td&gt;
&lt;td&gt;Debezium reads the table from the database log; background jobs for in-process handlers&lt;/td&gt;
&lt;td&gt;Polling interval between initial and maximum; shared settings&lt;/td&gt;
&lt;td&gt;Adaptive consumer heartbeats and lease acquisition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consumer-side deduplication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No; the consumer deduplicates by event id&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes: effectively-once and at-most-once consumers, Redis cache&lt;/td&gt;
&lt;td&gt;For in-process handlers: one row per event × handler pair&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Subscription cursors in the database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability and operations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Micrometer, OpenTelemetry, Admin REST API, CLI, Grafana demo&lt;/td&gt;
&lt;td&gt;Micrometer, health indicator&lt;/td&gt;
&lt;td&gt;Micrometer, Grafana dashboards, REST API for the DLQ&lt;/td&gt;
&lt;td&gt;OpenTelemetry tracing&lt;/td&gt;
&lt;td&gt;MicroProfile Metrics&lt;/td&gt;
&lt;td&gt;Subscription statistics in tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maturity and maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.0.0-rc.3 from 2026-11-05, API frozen; one main author&lt;/td&gt;
&lt;td&gt;0.2.3 from 2026-10-28; one author&lt;/td&gt;
&lt;td&gt;2.0.0 from 2026-10-15; one author&lt;/td&gt;
&lt;td&gt;1.1.3 from 2026-11-01; one main author&lt;/td&gt;
&lt;td&gt;3.4.2 from 2026-10-12; developed since 2021, one author&lt;/td&gt;
&lt;td&gt;No releases, not published on Maven Central; last commit 2025-08-15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;
  
  
  How we compared
&lt;/h2&gt;

&lt;p&gt;Each library was assessed against the nine criteria in the table below, the same for all of them. Facts come from READMEs, documentation, &lt;code&gt;pom.xml&lt;/code&gt; / &lt;code&gt;build.gradle.kts&lt;/code&gt;, 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 &lt;a href="https://dev.to/methodology/"&gt;Methodology&lt;/a&gt; page.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a transactional outbox is and how the six libraries differ
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tandem&lt;/strong&gt; and &lt;strong&gt;oncebox&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spring-boot-outbox-starter&lt;/strong&gt; handles the table, queue, workers, retries and recovery. In exchange, the target can be anything: Kafka, REST, a webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ekbatan&lt;/strong&gt; is a JOOQ-based persistence framework: it does not complement but replaces Spring Data and Hibernate, and writes events to the &lt;code&gt;eventlog.events&lt;/code&gt; table on every model change. Debezium sends them to Kafka.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transactional-event&lt;/strong&gt; provides transactional events for Jakarta EE: the event is stored in a table together with the transaction and later processed asynchronously by your &lt;code&gt;Handler&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;boxy&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why ekbatan doesn't fit an existing Spring Data project
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;JpaRepository&lt;/code&gt; and does not support Spring &lt;code&gt;@Transactional&lt;/code&gt; or &lt;code&gt;PlatformTransactionManager&lt;/code&gt;: it has its own &lt;code&gt;TransactionManager&lt;/code&gt;, and its &lt;code&gt;Action&lt;/code&gt; serves as the transaction boundary. An event is written atomically only together with changes made through &lt;code&gt;ActionExecutor&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;Action&lt;/code&gt;s. 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 &lt;code&gt;repository.save(...)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Spring Boot and Java support
&lt;/h2&gt;

&lt;p&gt;In Tandem, Spring is a &lt;code&gt;compileOnly&lt;/code&gt; dependency, so the Spring version is determined by your application's BOM.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Spring Boot&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;th&gt;Without Spring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tandem&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;td&gt;yes, dependency-free core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;oncebox&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-boot-outbox-starter&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;Spring-free core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ekbatan&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Quarkus, Micronaut, plain Java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transactional-event&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Jakarta EE 9+, Quarkus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boxy&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;plain Java only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your project runs on Spring Boot and Java 21, oncebox, Tandem and spring-boot-outbox-starter all fit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Outbox for Kafka and RabbitMQ: brokers and databases
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;oncebox-kafka&lt;/code&gt; or &lt;code&gt;oncebox-rabbit&lt;/code&gt; module and the &lt;code&gt;oncebox.outbox.sender.type&lt;/code&gt; property. Tandem's RabbitMQ adapter (&lt;code&gt;tandem-rabbitmq&lt;/code&gt;) 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;READ COMMITTED&lt;/code&gt; isolation level: under InnoDB's default &lt;code&gt;REPEATABLE READ&lt;/code&gt;, a &lt;code&gt;SKIP LOCKED&lt;/code&gt; 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.&lt;/p&gt;
&lt;h2&gt;
  
  
  Retries, backoff and DLQ
&lt;/h2&gt;

&lt;p&gt;Each library decides differently what to do with an event that cannot be delivered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;oncebox&lt;/strong&gt; offers the full set: retries and backoff are configured per event type, events "stuck" in &lt;code&gt;IN_PROCESS&lt;/code&gt; 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 &lt;code&gt;oncebox-dlq-api&lt;/code&gt; module adds a REST API for the DLQ: view, requeue or delete. The DLQ is disabled by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tandem&lt;/strong&gt; retries sending with exponential backoff, and after &lt;code&gt;maxAttempts&lt;/code&gt; (10 by default) the event is blocked. The &lt;code&gt;blocked.count&lt;/code&gt; metric shows the scale of the problem, and the Admin API (&lt;code&gt;tandem-admin&lt;/code&gt;, disabled by default) lets you resend or discard the event. It also lets you pause and resume the relay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;spring-boot-outbox-starter&lt;/strong&gt; moves an event to &lt;code&gt;DEAD&lt;/code&gt; status after &lt;code&gt;max-retries&lt;/code&gt; attempts, and &lt;code&gt;RecoveryWorker&lt;/code&gt; picks up records with an expired lease. There is no API for working with &lt;code&gt;DEAD&lt;/code&gt; records, only SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ekbatan&lt;/strong&gt;, for in-process handlers, retries the call with bounded exponential backoff and moves the notification to &lt;code&gt;EXPIRED&lt;/code&gt; if the event is older than the retention window (7 days by default). &lt;strong&gt;transactional-event&lt;/strong&gt; marks an event as blocked once retries are exhausted; metrics count both blocked and unblocked events.&lt;/p&gt;
&lt;h2&gt;
  
  
  Polling and per-event-type tuning
&lt;/h2&gt;

&lt;p&gt;oncebox is the only one of the six where outbox behaviour is configured separately for each event type. For &lt;code&gt;order-created&lt;/code&gt; and &lt;code&gt;analytics-tracked&lt;/code&gt; you can set a different batch size, polling strategy, retry count and backoff, and anything not overridden is inherited from the shared &lt;code&gt;defaults&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;oncebox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;batch-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
      &lt;span class="na"&gt;polling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;adaptive&lt;/span&gt;
        &lt;span class="na"&gt;min-fixed-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;250ms&lt;/span&gt;
        &lt;span class="na"&gt;max-fixed-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
        &lt;span class="na"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;
      &lt;span class="na"&gt;max-retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
        &lt;span class="na"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;payment-completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&lt;/span&gt;
        &lt;span class="na"&gt;polling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;min-fixed-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100ms&lt;/span&gt;   &lt;span class="c1"&gt;# poll more often, the rest comes from defaults
&lt;/span&gt;        &lt;span class="na"&gt;max-retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
          &lt;span class="na"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;
      &lt;span class="na"&gt;analytics-tracked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics&lt;/span&gt;
        &lt;span class="na"&gt;batch-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="na"&gt;polling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fixed&lt;/span&gt;
          &lt;span class="na"&gt;fixed-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;          &lt;span class="c1"&gt;# once a minute in large batches
&lt;/span&gt;        &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Adaptive polling is enabled by default in oncebox: while there are no events, the pause between polls grows by the multiplier up to &lt;code&gt;max-fixed-delay&lt;/code&gt;, and as soon as events appear it resets to &lt;code&gt;min-fixed-delay&lt;/code&gt;. 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 &lt;a href="https://dev.to/sayner_79d00706660f58a746/transactional-outbox-and-inbox-in-spring-boot-57h3"&gt;oncebox walkthrough on DEV Community&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tandem&lt;/strong&gt; 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 &lt;code&gt;pg-notify&lt;/code&gt;. But Tandem's settings are shared across the whole relay and change only on restart. &lt;strong&gt;spring-boot-outbox-starter&lt;/strong&gt; 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.&lt;/p&gt;
&lt;h2&gt;
  
  
  Inbox pattern: consumer-side message deduplication
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Only &lt;strong&gt;oncebox&lt;/strong&gt; 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 &lt;code&gt;oncebox-inbox-cache&lt;/code&gt; module adds a Redis cache of records. Records are kept until cleanup (&lt;code&gt;oncebox.inbox.clean-up.ttl&lt;/code&gt;, 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 &lt;code&gt;oncebox-starter&lt;/code&gt;, with no separate library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tandem&lt;/strong&gt; does not provide an inbox: each event has a unique id, and the consumer writes deduplication by that id. &lt;strong&gt;ekbatan&lt;/strong&gt;, 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.&lt;/p&gt;
&lt;h2&gt;
  
  
  Observability and operations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tandem&lt;/strong&gt; 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 &lt;code&gt;tandem-cli&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;oncebox&lt;/strong&gt; provides Micrometer metrics for outbox and inbox (the &lt;code&gt;oncebox-metrics&lt;/code&gt; module), Grafana dashboards in the repository and a REST API for the DLQ. &lt;strong&gt;spring-boot-outbox-starter&lt;/strong&gt; publishes metrics per channel and event type (&lt;code&gt;outbox_publish_total&lt;/code&gt;, &lt;code&gt;outbox_queue_pressure&lt;/code&gt;) and a health indicator. ekbatan has OpenTelemetry tracing, transactional-event has MicroProfile metrics.&lt;/p&gt;
&lt;h2&gt;
  
  
  Maturity and maintenance
&lt;/h2&gt;

&lt;p&gt;All six projects have a single main author, so it is worth looking at versions, release frequency and API stability.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;oncebox&lt;/strong&gt; 2.0.0 was released on 15 October 2026; it was preceded by 1.x releases. The project has been developed since October 2025.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tandem&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spring-boot-outbox-starter&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ekbatan&lt;/strong&gt; 1.1.3 was released on 1 November 2026; the project has been developed since November 2025.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transactional-event&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;boxy&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Example: outbox in Spring Boot with Kafka
&lt;/h2&gt;

&lt;p&gt;The task is the same for the three starters that send events from Spring Boot: within a transaction, save an order and write an &lt;code&gt;OrderCreated&lt;/code&gt; event to the outbox. The examples are taken from the libraries' READMEs and shortened.&lt;/p&gt;
&lt;h3&gt;
  
  
  oncebox
&lt;/h3&gt;

&lt;p&gt;Dependencies: &lt;code&gt;io.github.dmitriy-iliyov:oncebox-starter&lt;/code&gt;, &lt;code&gt;oncebox-postgresql&lt;/code&gt; and &lt;code&gt;oncebox-kafka&lt;/code&gt; version 2.0.0; &lt;code&gt;@EnableOncebox&lt;/code&gt; on the main class.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;oncebox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;order-created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"orders"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OutboxPublisher&lt;/span&gt; &lt;span class="n"&gt;outboxPublisher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;outboxPublisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"order-created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead of calling &lt;code&gt;publish&lt;/code&gt;, you can put &lt;code&gt;@OutboxPublish(eventType = "order-created")&lt;/code&gt; on the method: the return value then becomes the event.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tandem
&lt;/h3&gt;

&lt;p&gt;Dependencies: the &lt;code&gt;com.codingful:tandem-bom:1.0.0-rc.3&lt;/code&gt; BOM, &lt;code&gt;tandem-spring-producer&lt;/code&gt; where events are written, &lt;code&gt;tandem-spring-relay&lt;/code&gt; and &lt;code&gt;tandem-kafka&lt;/code&gt; where the relay runs.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;outboxRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OutboxMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregateId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregateType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"com.acme.order.created"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In Spring, instead of the low-level &lt;code&gt;OutboxRepository&lt;/code&gt;, you can use &lt;code&gt;TransactionalOutboxTemplate&lt;/code&gt;, the &lt;code&gt;@TransactionalOutbox&lt;/code&gt; annotation, or Spring application events.&lt;/p&gt;
&lt;h3&gt;
  
  
  spring-boot-outbox-starter
&lt;/h3&gt;

&lt;p&gt;Dependency: &lt;code&gt;com.kholodilin:spring-boot-outbox-starter:0.2.3&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;outboxService&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ORDER_CREATED"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregateId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KafkaOrdersSink&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;OutboxSink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;OutboxPublishResult&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OutboxRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// send the batch to Kafka with your own code&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OutboxPublishResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AllSucceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Pros and cons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tandem&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pros: instance coordination via leases, Admin API and CLI, detailed metrics and tracing, published benchmarks, the relay can run as a separate container.&lt;/li&gt;
&lt;li&gt;Cons: still a release candidate; PostgreSQL only; RabbitMQ adapter at 0.x; no inbox; &lt;code&gt;bucketCount&lt;/code&gt; cannot be changed after the first deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;oncebox&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;spring-boot-outbox-starter&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pros: delivery anywhere via &lt;code&gt;OutboxSink&lt;/code&gt;; isolated channels with their own tables and settings; a Redis queue to wake up workers across instances.&lt;/li&gt;
&lt;li&gt;Cons: you write the broker transport yourself; PostgreSQL only; version 0.x; no API for &lt;code&gt;DEAD&lt;/code&gt; events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;ekbatan&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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 &lt;code&gt;JpaRepository&lt;/code&gt; or Spring &lt;code&gt;@Transactional&lt;/code&gt;, so its outbox cannot be embedded in an existing project; requires Java 25; Kafka requires Debezium and Kafka Connect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;transactional-event&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pros: the longest history of the six; standard Jakarta EE APIs; a Quarkus extension; works with any JDBC database.&lt;/li&gt;
&lt;li&gt;Cons: not for Spring; no built-in broker, you write sending in the handler; LGPL-3.0 license.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;boxy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pros: an interesting model — partitions, subscriptions and weighted balancing on top of the database, with no broker.&lt;/li&gt;
&lt;li&gt;Cons: no releases and no artifact on Maven Central; MySQL only; development stopped in August 2025; no framework integration.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Final scores
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;


&lt;div class="table-wrapper-paragraph"&gt;
Scores from 1 to 10 across the article's nine criteria, combined into seven columns; the total is an unweighted average
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Spring Boot integration&lt;/th&gt;
&lt;th&gt;Brokers and databases&lt;/th&gt;
&lt;th&gt;Failures and DLQ&lt;/th&gt;
&lt;th&gt;Polling and tuning&lt;/th&gt;
&lt;th&gt;Inbox&lt;/th&gt;
&lt;th&gt;Observability&lt;/th&gt;
&lt;th&gt;Maturity&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;oncebox&lt;/th&gt;
&lt;td&gt;8.8&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;Tandem&lt;/th&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;spring-boot-outbox-starter&lt;/th&gt;
&lt;td&gt;5.3&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;ekbatan&lt;/th&gt;
&lt;td&gt;4.7&lt;/td&gt;
&lt;td&gt;2.5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;transactional-event&lt;/th&gt;
&lt;td&gt;3.9&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;boxy&lt;/th&gt;
&lt;td&gt;2.8&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Who should choose what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Spring Boot microservice, Kafka or RabbitMQ.&lt;/strong&gt; oncebox: a starter, a single &lt;code&gt;publish&lt;/code&gt; call, a DLQ and an inbox for the consumer in the same library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events of different importance in one service: payments must go out instantly, analytics in batches.&lt;/strong&gt; oncebox: polling, batch size, retries and backoff are set separately for each event type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A service on MySQL or Oracle.&lt;/strong&gt; oncebox is the only one of the three Spring starters that supports these databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An outbox for webhooks and external HTTP APIs rather than a broker.&lt;/strong&gt; oncebox: a starter has NonTransactionalConsumer for this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jakarta EE or Quarkus.&lt;/strong&gt; transactional-event.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are the transactional outbox and inbox patterns?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I build an inbox and an idempotent Kafka consumer in Spring Boot?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Spring Boot outbox library supports both Kafka and RabbitMQ?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does transactional-event work with Spring Boot?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can ekbatan be added to an existing Spring Data JPA project?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do these libraries need Debezium?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Miguel Carvalho.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/alirux/tandem-transactional-outbox-kafka" rel="noopener noreferrer"&gt;Tandem: README&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tandem-codingful.readthedocs.io/" rel="noopener noreferrer"&gt;Tandem: documentation&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dmitriy-iliyov/oncebox" rel="noopener noreferrer"&gt;oncebox: README&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dmitriy-iliyov/oncebox/releases/tag/v2.0.0" rel="noopener noreferrer"&gt;oncebox 2.0.0: release notes&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sayner_79d00706660f58a746/transactional-outbox-and-inbox-in-spring-boot-57h3"&gt;Transactional Outbox and Inbox in Spring Boot (DEV Community)&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/KHolodilin/spring-boot-outbox-starter" rel="noopener noreferrer"&gt;spring-boot-outbox-starter: README&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ekbatan-io/ekbatan" rel="noopener noreferrer"&gt;ekbatan: README&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ekbatan-io/ekbatan/blob/main/docs/wiring/spring.md" rel="noopener noreferrer"&gt;ekbatan: Wiring with Spring Boot (Spring Data JPA and @Transactional)&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ekbatan-io/ekbatan/blob/main/docs/events/local-event-handler.md" rel="noopener noreferrer"&gt;ekbatan: In-process event handlers&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/jonasrutishauser/transactional-event" rel="noopener noreferrer"&gt;transactional-event: README&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/rbilleci/boxy" rel="noopener noreferrer"&gt;boxy: README&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://central.sonatype.com/" rel="noopener noreferrer"&gt;Maven Central: latest artifact versions&lt;/a&gt; (checked November 7, 2026)&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>beginners</category>
      <category>springboot</category>
      <category>java</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
