Skip to content

namastack/namastack-outbox

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

423 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Version CodeFactor codecov javadoc namastack-outbox CI GitHub Release Date GitHub last commit dependabot License Sponsor

Namastack Outbox for Spring Boot

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.


❤️ Support the Project

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!

Become a Sponsor


Key Features

  • 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 @OutboxFallbackHandler when 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.

Check out the new Spring Modulith Integration!

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


Documentation

For detailed information about features, configuration, and advanced topics, visit the complete documentation.

Quick links:


Quick Start

1. Add Dependency

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.

2. Create Handlers

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.

3. Schedule Records Atomically

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))

4. Configure (Optional)

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.0

For a complete list of all configuration options, see Configuration Reference.

That's it! Your records are now reliably persisted and processed.


JPA Setup

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.


MongoDB Setup

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.

Custom Collection Prefix

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.


Features at a Glance

Handlers

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.

Handler Documentation

Retry Policies

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()

Retry Documentation

Context Propagation

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

Messaging Integrations

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")

Messaging Documentation


How It Works

The Transactional Outbox Pattern

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.

Partitioning & Scaling

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


Observability & Operations

Metrics (Micrometer)

outbox.records.count{status="new|failed|completed"}
outbox.partitions.assigned.count
outbox.partitions.pending.records.total
outbox.partitions.pending.records.max

Distributed Tracing

Add namastack-outbox-tracing for automatic Micrometer Observation spans on every handler invocation with trace context propagation across the async boundary.

Actuator Endpoint

Add namastack-outbox-actuator for a management endpoint to query and clean up outbox records by status.

Observability Documentation


Auto-Configuration Highlights

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

Configuration Reference


Supported Databases

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 Projects

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

All examples


Requirements

  • Java 17+
  • Spring Boot 4.0.0+
  • Kotlin 2.2+ (optional, Java is fully supported)

Support


License

Apache License 2.0 - See LICENSE


Acknowledgments