Kotlin library implementing the transactional outbox pattern — reliable message delivery alongside local database operations.
Messages are stored in a database table within the same transaction as your business operation, then asynchronously delivered to external transports (HTTP webhooks, Kafka). This guarantees at-least-once delivery without distributed transactions.
Add dependencies using the BOM for version alignment:
dependencies {
implementation(platform("com.softwaremill.okapi:okapi-bom:$okapiVersion"))
implementation("com.softwaremill.okapi:okapi-core")
implementation("com.softwaremill.okapi:okapi-postgres")
implementation("com.softwaremill.okapi:okapi-http")
implementation("com.softwaremill.okapi:okapi-spring-boot")
}Provide a MessageDeliverer bean — this tells okapi how to deliver messages.
ServiceUrlResolver maps the logical service name (set per message) to a base URL:
@Bean
fun httpDeliverer(): HttpMessageDeliverer =
HttpMessageDeliverer(ServiceUrlResolver { serviceName ->
when (serviceName) {
"notification-service" -> "https://notifications.example.com"
else -> error("Unknown service: $serviceName")
}
})Publish inside any @Transactional method — inject SpringOutboxPublisher via constructor:
@Service
class OrderService(
private val orderRepository: OrderRepository,
private val springOutboxPublisher: SpringOutboxPublisher
) {
@Transactional
fun placeOrder(order: Order) {
orderRepository.save(order)
springOutboxPublisher.publish(
OutboxMessage("order.created", order.toJson()),
httpDeliveryInfo {
serviceName = "notification-service"
endpointPath = "/webhooks/orders"
}
)
}
}Autoconfiguration handles scheduling, retries, and delivery automatically.
Using Kafka instead of HTTP? Swap the deliverer bean and delivery info:
@Bean
fun kafkaDeliverer(producer: KafkaProducer<String, String>): KafkaMessageDeliverer =
KafkaMessageDeliverer(producer)springOutboxPublisher.publish(
OutboxMessage("order.created", order.toJson()),
kafkaDeliveryInfo { topic = "order-events" }
)Using MySQL instead of PostgreSQL? Replace okapi-postgres with okapi-mysql in your dependencies — no code changes needed.
Note:
okapi-postgresandokapi-mysqlrequire Exposed ORM dependencies in your project. Spring and Kafka versions are not forced by okapi — you control them.
Okapi implements the transactional outbox pattern (see also: microservices.io description):
- Your application writes an
OutboxMessageto the outbox table in the same database transaction as your business operation - A background
OutboxSchedulerpolls for pending messages and delivers them to the configured transport (HTTP, Kafka) - Failed deliveries are retried according to a configurable
RetryPolicy(max attempts, backoff)
Delivery guarantees:
- At-least-once delivery — okapi guarantees every message will be delivered, but duplicates are possible (e.g., after a crash between delivery and status update). Consumers should handle idempotency, for example by checking the
OutboxIdreturned bypublish(). - Concurrent processing — multiple processors can run in parallel using
FOR UPDATE SKIP LOCKED, so messages are never processed twice simultaneously. - Delivery result classification — each transport classifies errors as
Success,RetriableFailure, orPermanentFailure. For example, HTTP 429 is retriable while HTTP 400 is permanent.
graph BT
PG[okapi-postgres] --> CORE[okapi-core]
MY[okapi-mysql] --> CORE
HTTP[okapi-http] --> CORE
KAFKA[okapi-kafka] --> CORE
SPRING[okapi-spring-boot] --> CORE
SPRING -.->|compileOnly| PG
SPRING -.->|compileOnly| MY
BOM[okapi-bom]
style CORE fill:#4a9eff,color:#fff
style BOM fill:#888,color:#fff
| Module | Purpose |
|---|---|
okapi-core |
Transport/storage-agnostic orchestration, scheduling, retry policy |
okapi-postgres |
PostgreSQL storage via Exposed ORM (FOR UPDATE SKIP LOCKED) |
okapi-mysql |
MySQL 8+ storage via Exposed ORM |
okapi-http |
HTTP webhook delivery (JDK HttpClient) |
okapi-kafka |
Kafka topic publishing |
okapi-spring-boot |
Spring Boot autoconfiguration (auto-detects store and transports) |
okapi-bom |
Bill of Materials for version alignment |
| Dependency | Supported Versions | Notes |
|---|---|---|
| Java | 21+ | Required |
| Spring Boot | 3.5.x, 4.0.x | okapi-spring-boot module |
| Kafka Clients | 3.9.x, 4.x | okapi-kafka — you provide kafka-clients |
| Exposed | 1.x | okapi-postgres, okapi-mysql — you provide Exposed |
./gradlew build # Build all modules
./gradlew test # Run tests (Docker required — Testcontainers)
./gradlew ktlintFormat # Format codeRequires JDK 21.
All suggestions welcome :)
To compile and test, run:
./gradlew build
./gradlew ktlintFormat # Mandatory before committingSee the list of issues and pick one! Or report your own.
If you are having doubts on the why or how something works, don't hesitate to ask a question on Discourse or via GitHub. This probably means that the documentation or code is unclear and can be improved for the benefit of all.
Tests use Testcontainers — Docker must be running.
When you have a PR ready, take a look at our "How to prepare a good PR" guide. Thanks! :)
We offer commercial development services. Contact us to learn more about us!
Copyright (C) 2026 SoftwareMill https://softwaremill.com.