Reliable Message Delivery: the Transactional Outbox Pattern With Okapi

Fri, 07 Aug 2026 12:48:00 +0000
Kafka, Apache Kafka, Spring Boot, Kotlin, PostgreSQL, MySQL

While working on a project with the team, we kept running into the same small but stubborn problem. We would save something to the database, and then need to notify another service about it with a webhook, a Kafka event, or a downstream call. The trouble is that "save to the database" and "notify the other service" don't share a transaction. Calling the downstream inside the transaction and a slow or flaky service drags your write down with it. Do it after the commit, and a crash or a dropped connection leaves you with the row saved, but the notification is never sent.

This is the dual-write problem, and the fix has been understood for years: the transactional outbox pattern. Instead of calling the downstream directly, you write the message to a table in the same transaction as your business data, and a separate process delivers it afterward. The transaction gives you atomicity; the background processor gives you eventual delivery. We won't re-explain the pattern here. We covered it, along with the inbox side and the trade-offs, in the Microservices 101: Transactional Outbox and Inbox article. This post assumes you know its shape and asks a narrower question: what do you actually add to a Kotlin service to get it? We'll walk through a Spring Boot app, because that's where this problem usually comes up, but the library itself isn't tied to Spring.

We did what most teams do first: we hand-rolled an outbox. It worked, so we extended it over time, and eventually it was solid enough that we started wondering whether anyone else could use it. Our stack was Kotlin-native, so before reinventing more of it, we looked for an existing library and came up short. There were pieces here and there, but nothing established that fit the way a Kotlin service actually wants to work. So we took what we had built and turned it into okapi: a small Kotlin library for the transactional outbox pattern, with Postgres and MySQL storage and HTTP and Kafka delivery. On Spring Boot it is mostly autoconfiguration, and in a Kotlin service without Spring you assemble the same pieces by hand, which we'll come back to at the end. In this post, we'll show what it does and exactly what changes in your code when you add it.

What's in the box

Okapi is a handful of small modules with one rule: everything depends on a framework-agnostic core, never the other way around. The core holds the abstractions, the publisher that writes entries, the store that persists them, the deliverer that sends them, the retry policy, and the background scheduler, and it knows nothing about your database, your transport, or your framework. Everything else plugs into it. In a picture:

okapi-spring-boot knows how to wire the storage and Micrometer modules, but it doesn’t pull them into your build: you add the ones you want and its autoconfiguration detects what’s there. okapi-bom aligns the versions of every module above; okapi-integration-tests and
...