Skip to content

Quickstart

Namastack Outbox for Spring Boot is a robust library for Java and Kotlin projects that implements the Transactional Outbox Pattern for reliable record publishing in distributed systems.

This guide will get you up and running in 5 minutes with minimal configuration.


Add Dependency

dependencies {
    implementation("io.namastack:namastack-outbox-starter-jpa:0.4.0")
}
<dependency>
    <groupId>io.namastack</groupId>
    <artifactId>namastack-outbox-starter-jpa</artifactId>
    <version>0.4.0</version>
</dependency>

Enable Outbox

Annotate your application class with @EnableOutbox:

@SpringBootApplication
@EnableOutbox
@EnableScheduling
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}
@SpringBootApplication
@EnableOutbox
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Create a Handler

Create a typed handler for your payload:

@Component
class OrderRecordHandler : OutboxTypedHandler<OrderCreatedRecord> {
    override fun handle(payload: OrderCreatedRecord) {
        // Process the record - this will be called automatically when ready
        eventPublisher.publish(payload)
    }
}
@Component
public class OrderRecordHandler implements OutboxTypedHandler<OrderCreatedRecord> {
    @Override
    public void handle(OrderCreatedRecord payload) {
        // Process the record - this will be called automatically when ready
        eventPublisher.publish(payload);
    }
}

Or use annotations for multiple handlers:

@Component
class MyHandlers {
    @OutboxHandler
    fun handleOrderCreated(payload: OrderCreatedRecord) {
        // ...
    }

    @OutboxHandler
    fun handlePaymentProcessed(payload: PaymentProcessedRecord) {
        // ...
    }
}
@Component
public class MyHandlers {
    @OutboxHandler
    public void handleOrderCreated(OrderCreatedRecord payload) {
        // ...
    }

    @OutboxHandler
    public void handlePaymentProcessed(PaymentProcessedRecord payload) {
        // ...
    }
}

Schedule Records Atomically

@Service
class OrderService(
    private val outbox: Outbox,
    private val orderRepository: OrderRepository
) {
    @Transactional
    fun createOrder(command: CreateOrderCommand) {
        val order = Order.create(command)
        orderRepository.save(order)

        // Schedule record - saved atomically with the order
        outbox.schedule(
            payload = OrderCreatedRecord(order.id, order.customerId),
            key = "order-${order.id}"  // Groups records for ordered processing
        )
    }
}
@Service
public class OrderService {
    private final Outbox outbox;
    private final OrderRepository orderRepository;

    public OrderService(Outbox outbox, OrderRepository orderRepository) {
        this.outbox = outbox;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void createOrder(CreateOrderCommand command) {
        Order order = Order.create(command);
        orderRepository.save(order);

        // Schedule record - saved atomically with the order
        outbox.schedule(
            new OrderCreatedRecord(order.getId(), order.getCustomerId()),
            "order-" + order.getId()  // Groups records for ordered processing
        );
    }
}

Alternative: Using Spring's ApplicationEventPublisher

If you prefer Spring's native event publishing, annotate your events with @OutboxEvent:

@OutboxEvent(key = "#event.orderId")  // SpEL expression for key resolution
data class OrderCreatedEvent(
    val orderId: String,
    val customerId: String,
    val amount: BigDecimal
)

@Service
class OrderService(
    private val orderRepository: OrderRepository,
    private val eventPublisher: ApplicationEventPublisher
) {
    @Transactional
    fun createOrder(command: CreateOrderCommand) {
        val order = Order.create(command)
        orderRepository.save(order)

        // Publish event - automatically saved to outbox atomically
        eventPublisher.publishEvent(
            OrderCreatedEvent(order.id, order.customerId, order.amount)
        )
    }
}
@OutboxEvent(key = "#event.orderId")  // SpEL expression for key resolution
public class OrderCreatedEvent {
    private String orderId;
    private String customerId;
    private BigDecimal amount;
    // constructor, getters...
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(OrderRepository orderRepository, 
                       ApplicationEventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
    }

    @Transactional
    public void createOrder(CreateOrderCommand command) {
        Order order = Order.create(command);
        orderRepository.save(order);

        // Publish event - automatically saved to outbox atomically
        eventPublisher.publishEvent(
            new OrderCreatedEvent(order.getId(), order.getCustomerId(), order.getAmount())
        );
    }
}

Configure (Optional)

outbox:
  poll-interval: 2000
  batch-size: 10
  retry:
    policy: "exponential"
    max-retries: 3
    exponential:
      initial-delay: 1000
      max-delay: 60000
      multiplier: 2.0
outbox.poll-interval=2000
outbox.batch-size=10
outbox.retry.policy=exponential
outbox.retry.max-retries=3
outbox.retry.exponential.initial-delay=1000
outbox.retry.exponential.max-delay=60000
outbox.retry.exponential.multiplier=2.0

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


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

Key Features

  • Transactional Atomicity: Records saved in same transaction as domain data
  • Automatic Retry: Exponential backoff, fixed delay, or jittered policies
  • Ordered Processing: Records with same key processed sequentially
  • Handler-Based: Annotation-based or interface-based handler registration
  • Horizontal Scaling: Automatic partition assignment across instances
  • Zero Message Loss: Database-backed with at-least-once delivery
  • Type-Safe Handlers: Generic or typed handler support
  • Built-in Metrics: Micrometer integration for monitoring
  • Flexible Payloads: Store any type - records, commands, notifications, etc.

Supported Databases

  • ✅ H2 (development)
  • ✅ MySQL / MariaDB
  • ✅ PostgreSQL
  • ✅ SQL Server

Next Steps