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-jdbc:1.0.0-RC2")
}
<dependency>
<groupId>io.namastack</groupId>
<artifactId>namastack-outbox-starter-jdbc</artifactId>
<version>1.0.0-RC2</version>
</dependency>
JDBC vs JPA
We recommend the JDBC starter for quick start as it supports automatic schema creation. For JPA/Hibernate projects, see JPA Setup below.
Enable Scheduling & Schema Creation
@SpringBootApplication
@EnableScheduling // Required for automatic outbox processing
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
@SpringBootApplication
@EnableScheduling // Required for automatic outbox processing
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Enable automatic schema creation in your configuration:
outbox:
jdbc:
schema-initialization:
enabled: true # Auto-creates outbox tables on startup
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/JDBC-compatible database is supported. Automatic schema creation is available in the JDBC module for:
- ✅ H2 (development)
- ✅ MySQL / MariaDB
- ✅ PostgreSQL
- ✅ SQL Server
Schema Files for Flyway/Liquibase:
If you manage your database schema manually, you can find the SQL schema files here: 👉 Schema Files on GitHub
JPA Setup
If you prefer using JPA/Hibernate instead of JDBC:
dependencies {
implementation("io.namastack:namastack-outbox-starter-jpa:1.0.0-RC2")
}
<dependency>
<groupId>io.namastack</groupId>
<artifactId>namastack-outbox-starter-jpa</artifactId>
<version>1.0.0-RC2</version>
</dependency>
Schema Management Required
The JPA module does not support automatic schema creation. Choose one of these options:
Option 1: Hibernate DDL Auto (Development only)
spring:
jpa:
hibernate:
ddl-auto: create # or create-drop
Option 2: Flyway/Liquibase (Recommended for Production)
Use the SQL schema files from our repository and configure Hibernate to validate:
spring:
jpa:
hibernate:
ddl-auto: validate
Next Steps
- Explore the Features Guide for advanced capabilities
- Check out the API Reference for detailed documentation
- Report issues at GitHub Issues
- Join GitHub Discussions for community support