Skip to content

Quickstart

Namastack Outbox for Spring Boot is a robust Spring Boot library for Java and Kotlin projects that implements the Transactional Outbox Pattern for reliable record publishing in distributed systems. Ensures records are never lost through atomic persistence and automatic retry logic with handler-based processing and partition-aware horizontal scaling.

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


Add Dependency

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

Enable Outbox

Annotate your application class with @EnableOutbox:

@SpringBootApplication
@EnableOutbox
@EnableScheduling  // Required for automatic outbox processing
class Application

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

Create Handlers

@Component
class OrderHandlers {
    // Typed handler - processes specific payload type
    @OutboxHandler
    fun handleOrder(payload: OrderCreatedEvent) {
        eventPublisher.publish(payload)
    }

    // Generic handler - processes any payload type
    @OutboxHandler
    fun handleAny(payload: Any, metadata: OutboxRecordMetadata) {
        when (payload) {
            is OrderCreatedEvent -> eventPublisher.publish(payload)
            is PaymentProcessedEvent -> paymentService.process(payload)
            else -> logger.warn("Unknown payload type")
        }
    }
}
@Component
public class OrderHandlers {
    // Typed handler - processes specific payload type
    @OutboxHandler
    public void handleOrder(OrderCreatedEvent payload) {
        eventPublisher.publish(payload);
    }

    // Generic handler - processes any payload type
    @OutboxHandler
    public void handleAny(Object payload, OutboxRecordMetadata metadata) {
        if (payload instanceof OrderCreatedEvent) {
            eventPublisher.publish((OrderCreatedEvent) payload);
        } else if (payload instanceof PaymentProcessedEvent) {
            paymentService.process((PaymentProcessedEvent) payload);
        } else {
            logger.warn("Unknown payload type");
        }
    }
}

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 event - saved atomically with the order
        outbox.schedule(
            payload = OrderCreatedEvent(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 event - saved atomically with the order
        outbox.schedule(
            new OrderCreatedEvent(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 = "#this.orderId",  // SpEL: uses 'orderId' field
    context = [
        OutboxContextEntry(key = "customerId", value = "#this.customerId"),
        OutboxContextEntry(key = "region", value = "#this.region")
    ]
)
data class OrderCreatedEvent(
    val orderId: String,
    val customerId: String,
    val region: 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.region, order.amount)
        )
    }
}
@OutboxEvent(
    key = "#this.orderId",  // SpEL: uses 'orderId' field
    context = {
        @OutboxContextEntry(key = "customerId", value = "#this.customerId"),
        @OutboxContextEntry(key = "region", value = "#this.region")
    }
)
public class OrderCreatedEvent {
    private String orderId;
    private String customerId;
    private String region;
    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.getRegion(), order.getAmount())
        );
    }
}

Both approaches work equally well. Choose based on your preference:

  • Explicit outbox.schedule(): More control, clearer intent, supports any payload type
  • @OutboxEvent + ApplicationEventPublisher: More Spring idiomatic for domain events

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 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
  • Zero Message Loss: Database-backed with at-least-once delivery
  • Horizontal Scaling: Automatic partition assignment across instances
  • Automatic Retry: Exponential backoff, fixed delay, or jittered policies
  • Handler-Based: Annotation-based or interface-based handler registration
  • Type-Safe Handlers: Generic or typed handler support
  • Fallback Handlers: Graceful degradation when retries are exhausted
  • Flexible Payloads: Store any type - events, commands, notifications, etc.
  • Context Propagation: Trace IDs, tenant info, correlation IDs across async boundaries
  • Ordered Processing: Records with same key processed sequentially
  • Built-in Metrics: Micrometer integration for monitoring

Supported Databases

Any JPA-compatible database is supported. Automatic schema creation is currently available for:

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

Next Steps