Interview question
Save an order and outbox event in one Spring transaction
Persists business state and durable publication intent atomically, leaving broker delivery to an idempotent retrying publisher.
TL;DR
Persists business state and durable publication intent atomically, leaving broker delivery to an idempotent retrying publisher.
Spring transactions, transactional outbox, event identity, payload ownership, rollback, publisher separation, and duplicate delivery.
Practice the problem like a real interview: restate, reason, implement, and test.
When an order is placed, persist the order and an OrderPlaced outbox row in the same database transaction. Do not call the message broker from that transaction. The outbox row needs a stable event ID, aggregate ID, event type/version, payload, creation time, and pending state.
Next in Java & Spring Practical Labs: PostgreSQL Testcontainers test
order save succeeds + outbox save succeeds -> both commit
outbox save fails -> order rolls back
publisher crashes after broker accepts -> row remains/retries; consumer deduplicates event ID
@org.springframework.stereotype.Service
class PlaceOrderService {
private final OrderRepository orders;
private final OutboxRepository outbox;
private final com.fasterxml.jackson.databind.ObjectMapper json;
PlaceOrderService(OrderRepository orders, OutboxRepository outbox,
com.fasterxml.jackson.databind.ObjectMapper json) {
this.orders = orders;
this.outbox = outbox;
this.json = json;
}
@org.springframework.transaction.annotation.Transactional
java.util.UUID place(PlaceOrder command) {
var order = orders.save(Order.place(
java.util.UUID.randomUUID(), command.customerId(), command.lines()));
var event = new OrderPlacedV1(
java.util.UUID.randomUUID(), order.getId(), order.getCustomerId(),
java.time.Instant.now());
outbox.save(OutboxMessage.pending(
event.eventId(), order.getId(), "OrderPlaced", 1,
writeJson(event), event.occurredAt()));
return order.getId();
}
private String writeJson(Object value) {
try { return json.writeValueAsString(value); }
catch (com.fasterxml.jackson.core.JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize outbox event", e);
}
}
}
// A separate bounded publisher claims pending rows and retries delivery.
// Consumers deduplicate by eventId because publish and mark-complete are not atomic.
The command performs two bounded inserts in one transaction. Publisher work is proportional to claimed batch size; indexes on pending status and creation time keep scanning bounded.
Spot a weak answer, missing edge case, or clearer explanation? Send it in.
Say thanks with a standalone, one-time $5 contribution. No account required.