Namastack Outbox is a modern, production-grade library for Spring Boot (Java & Kotlin) that implements the Transactional Outbox Pattern for reliable, scalable, and observable event-driven architectures. It guarantees that business events are never lost, are delivered on an at-least-once basis to matching handlers, and can be published to any system, whether via custom handlers, Kafka, RabbitMQ, SNS, or other integrations.
Namastack Outbox started as a personal passion project around a problem I kept running into while building distributed systems: reliable event publishing is much harder than it initially looks.
What began as an internal idea slowly evolved into an open-source project focused on making transactional messaging, durable event publication and event-driven Spring Boot architectures easier to build and operate in production.
A lot of time goes into maintaining, improving and documenting open-source software. If Namastack Outbox helps you or your team, sponsoring the project is a great way to support its continued development and long-term sustainability.
Your support helps us dedicate more time to:
- improving documentation and examples
- building new integrations and features
- maintaining long-term stability
- continuing to invest in the Spring and open-source ecosystem
Thank you for supporting open source!
- Transactional Guarantees - Outbox records are persisted atomically with your business data. No lost events, ever.
- At-Least-Once Delivery - Robust retry logic with exponential backoff, linear, fixed, and jittered strategies.
- Ordered Processing - Records with the same key are always processed sequentially, guaranteeing strict ordering.
- Horizontal Scaling - Partitioned processing with automatic rebalancing across 256 partitions.
- Flexible Handler Model - Annotation-based (
@OutboxHandler) or interface-based (OutboxTypedHandler<T>) handlers. - Fallback & Dead Letter - Graceful degradation with
@OutboxFallbackHandlerwhen all retries are exhausted. - Context Propagation - Trace IDs, tenant info, and correlation IDs flow automatically across async boundaries.
- Adaptive Polling - Dynamically adjusts polling interval based on workload for optimal DB efficiency.
- Observability - Built-in Micrometer metrics, Actuator endpoint, and distributed tracing.
- Messaging Integrations - Ready-to-use Kafka, RabbitMQ, and AWS SNS handlers with flexible routing.
- Virtual Thread Support - Automatic detection and use of virtual threads when available.
- Auto-Configuration - Sensible defaults, automatic
@EnableScheduling, and deep Spring Boot integration. - Broad Database Support - H2, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, and MongoDB.
Use Namastack Outbox as a production-grade, transactional outbox for Spring Modulith!
Seamlessly externalize events with full support for retry, partitioning, observability, and more.
- Effortless migration from Event Publication Registry to real outbox mode
- Works with both JPA and JDBC persistence
- All Namastack features available out of the box
Modulith Example | Read the Modulith Integration Guide
For detailed information about features, configuration, and advanced topics, visit the complete documentation.
Quick links:
Gradle (Kotlin DSL):
dependencies {
implementation(platform("io.namastack:namastack-outbox-bom:1.7.1"))
implementation("io.namastack:namastack-outbox-starter-jdbc")
}Maven:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.namastack</groupId>
<artifactId>namastack-outbox-bom</artifactId>
<version>1.7.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.namastack</groupId>
<artifactId>namastack-outbox-starter-jdbc</artifactId>
</dependency>
</dependencies>Note: The JDBC starter includes automatic schema creation. For JPA/Hibernate projects, see JPA Setup below. For MongoDB projects, see MongoDB Setup below.
Kotlin
@Component
class OrderHandlers {
@OutboxHandler
fun handleOrder(payload: OrderCreatedEvent) {
eventPublisher.publish(payload)
}
@OutboxHandler
fun handleAny(payload: Any, metadata: OutboxRecordMetadata) {
when (payload) {
is OrderCreatedEvent -> eventPublisher.publish(payload)
is PaymentProcessedEvent -> paymentService.process(payload)
}
}
}Java
@Component
public class OrderHandler implements OutboxTypedHandler<OrderCreatedEvent> {
@Override
public void handle(OrderCreatedEvent payload, OutboxRecordMetadata metadata) {
eventPublisher.publish(payload);
}
}Handlers that accept OutboxRecordMetadata can inspect the current delivery state:
failureCount == 0 is the first attempt, failureCount > 0 is a retry, attempt is
failureCount + 1, and isRetry is a convenience flag.
Kotlin
@Service
class OrderService(
private val outbox: Outbox,
private val orderRepository: OrderRepository
) {
@Transactional
fun createOrder(command: CreateOrderCommand) {
val order = Order.create(command)
orderRepository.save(order)
outbox.schedule(
payload = OrderCreatedEvent(order.id, order.customerId),
key = "order-${order.id}"
)
}
}Java
@Service
public class OrderService {
private final Outbox outbox;
private final OrderRepository orderRepository;
@Transactional
public void createOrder(CreateOrderCommand command) {
Order order = Order.create(command);
orderRepository.save(order);
outbox.schedule(
new OrderCreatedEvent(order.getId(), order.getCustomerId()),
"order-" + order.getId()
);
}
}Alternative: Using Spring's ApplicationEventPublisher
Annotate your events with @OutboxEvent to automatically persist them to the outbox:
@OutboxEvent(key = "#this.orderId")
data class OrderCreatedEvent(val orderId: String, val customerId: String)
// Then simply use Spring's event publishing inside a @Transactional method
eventPublisher.publishEvent(OrderCreatedEvent(order.id, order.customerId))namastack:
outbox:
polling:
batch-size: 10
trigger: fixed # or "adaptive"
fixed:
interval: 2s
retry:
policy: exponential
max-retries: 3
exponential:
initial-delay: 1s
max-delay: 60s
multiplier: 2.0For a complete list of all configuration options, see Configuration Reference.
That's it! Your records are now reliably persisted and processed.
If you prefer using JPA/Hibernate instead of JDBC, use the JPA starter:
dependencies {
implementation(platform("io.namastack:namastack-outbox-bom:1.7.1"))
implementation("io.namastack:namastack-outbox-starter-jpa")
}The JPA module does not support automatic schema creation. Use Flyway/Liquibase with
our SQL schema files,
or ddl-auto: create for development.
See example-flyway-jpa for a complete example.
For MongoDB projects, use the MongoDB starter:
dependencies {
implementation(platform("io.namastack:namastack-outbox-bom:1.7.1"))
implementation("io.namastack:namastack-outbox-starter-mongodb")
}The MongoDB module automatically creates collections and indexes on startup via Spring Data MongoDB's
auto-index-creation. No manual schema management is required.
For production environments, you can manage indexes manually using the provided mongosh setup script. See the MongoDB Schema documentation for details.
Customize collection names for multi-tenant deployments or naming conventions:
namastack:
outbox:
mongodb:
collection-prefix: "myapp_" # Results in: myapp_outbox_records, myapp_outbox_instances, etc.See example-mongodb for a complete example.
Process outbox records using annotation-based or interface-based handlers. Typed handlers match specific payload types; generic handlers catch all.
@OutboxHandler
fun handleOrder(payload: OrderCreatedEvent) { /* ... */
}
@OutboxFallbackHandler
fun handleFailure(payload: OrderCreatedEvent, context: OutboxFailureContext) {
deadLetterQueue.publish(payload)
}OutboxRecordMetadata is available during normal handler invocation and includes retry state
for the current attempt. OutboxFailureContext is available to fallback handlers after retries
are exhausted or an exception is non-retryable.
Configure retry behavior globally via properties (exponential, fixed, linear) or per-handler
via @OutboxRetryable. Use the OutboxRetryPolicy.Builder API for programmatic control.
val policy = OutboxRetryPolicy.builder()
.maxRetries(5)
.exponentialBackoff(Duration.ofSeconds(10), 2.0, Duration.ofMinutes(5))
.jitter(Duration.ofSeconds(2))
.retryOn(IOException::class.java)
.noRetryOn(IllegalArgumentException::class.java)
.build()Preserve context (trace IDs, tenant info) across async boundaries using OutboxContextProvider or
SpEL expressions in @OutboxEvent.
@Component
class TracingContextProvider(private val tracer: Tracer) : OutboxContextProvider {
override fun provide() =
mapOf("traceId" to tracer.currentSpan()?.context()?.traceId().orEmpty())
}→ Context Propagation Documentation
Ready-to-use modules for Kafka, RabbitMQ, and AWS SNS with flexible routing, header mapping, and payload transformation.
implementation(platform("io.namastack:namastack-outbox-bom:1.7.1"))
implementation("io.namastack:namastack-outbox-kafka")
implementation("io.namastack:namastack-outbox-rabbit")
implementation("io.namastack:namastack-outbox-sns")The outbox record is saved in the same database transaction as your business data. A background scheduler polls for new records and dispatches them to handlers. This guarantees atomicity - either both succeed or both fail together.
Records are distributed across 256 partitions using consistent hashing on the record key. Each application instance is assigned a subset of partitions, enabling horizontal scaling while maintaining ordering guarantees per key.
Instance 1 → partitions 0-84 → processes "order-123", "order-456"
Instance 2 → partitions 85-169 → processes "payment-789"
Instance 3 → partitions 170-255 → processes other keys
When instances join or leave, partitions are automatically rebalanced. Stale instances are detected via heartbeats and their partitions are reassigned.
→ Core and Partitioning Documentation
→ Processing Documentation
outbox.records.count{status="new|failed|completed"}
outbox.partitions.assigned.count
outbox.partitions.pending.records.total
outbox.partitions.pending.records.max
Add namastack-outbox-tracing for automatic Micrometer Observation spans on every handler
invocation with trace context propagation across the async boundary.
Add namastack-outbox-actuator for a management endpoint to query and clean up outbox records by
status.
The library auto-configures everything you need with sensible defaults:
| Feature | Default | Property |
|---|---|---|
| Outbox enabled | true |
namastack.outbox.enabled |
@EnableScheduling activation |
Automatic (if not already active) | - |
| Polling strategy | Fixed (2s interval) | namastack.outbox.polling.trigger |
| Virtual threads | Auto-detected | spring.threads.virtual.enabled |
| Delete completed records | false |
namastack.outbox.processing.delete-completed-records |
| Stop on first failure | true |
namastack.outbox.processing.stop-on-first-failure |
@OutboxEvent multicaster |
true |
namastack.outbox.multicaster.enabled |
| Database | Auto Schema | Tested |
|---|---|---|
| H2 | ✅ | ✅ |
| MySQL | ✅ | ✅ |
| MariaDB | ✅ | ✅ |
| PostgreSQL | ✅ | ✅ |
| SQL Server | ✅ | ✅ |
| Oracle | ✅ | ✅ |
| MongoDB | ✅ | ✅ |
Schema files for Flyway/Liquibase: Schema Files on GitHub
MongoDB setup script: mongodb-setup.js on GitHub
| Example | Description |
|---|---|
| example-h2 | Basic setup with H2 - perfect starting point |
| example-java | Pure Java implementation |
| example-annotation | Annotation-based handler registration |
| example-kafka | Kafka integration |
| example-rabbit | RabbitMQ integration |
| example-sns | AWS SNS integration |
| example-modulith | Spring Modulith integration with outbox and Kafka |
| example-retry | Retry policies |
| example-fallback | Fallback handlers |
| example-tracing | Distributed tracing with Micrometer |
| example-flyway-jpa | Flyway schema management |
| example-table-prefix-jdbc | Custom table prefixes |
| example-mongodb | MongoDB with custom collection prefixes |
- Java 17+
- Spring Boot 4.0.0+
- Kotlin 2.2+ (optional, Java is fully supported)
Apache License 2.0 - See LICENSE
- Built with ❤️ by Namastack
- Inspired by the Transactional Outbox Pattern
- Powered by Spring Boot & Kotlin