Order Pipeline
Moved order processing out of a monolith into event-driven Spring Boot services with an outbox pattern, so a payment provider outage no longer takes the whole shop down.
- Spring Boot
- Kafka
- PostgreSQL
- Kubernetes
The problem
Placing an order called payment, inventory, invoicing and notifications synchronously inside one database transaction. Any slow dependency meant failed orders and double charges on retry.
Constraints
- Exactly-once effect for payments, at-least-once delivery everywhere else
- Monolith and new services run side by side for months
- Audit trail for every state change (finance requirement)
Architecture
Key decisions
The order and its OrderPlaced event are written in the same Postgres transaction. A relay publishes the outbox to Kafka, so an event is never lost or sent for a rolled-back order.
Trade-off: A second of extra latency between commit and publish.
Consumers record processed event ids in the same transaction as their side effect. Redelivery becomes a no-op instead of a double charge.
Trade-off: A dedupe table per service that needs pruning.
Order traffic moved to the new service per market behind a feature flag, with instant rollback to the monolith path.
Trade-off: Two code paths alive for one quarter.
In the code
1@Service2@RequiredArgsConstructor3public class OrderService {45 private final OrderRepository orders;6 private final OutboxRepository outbox;78 @Transactional9 public Order place(PlaceOrder cmd) {10 var order = orders.save(Order.from(cmd));1112 // Same transaction: the event exists if and only if the order does.13 outbox.save(OutboxEvent.of(14 "order.placed",15 order.getId(),16 new OrderPlaced(order.getId(), order.total(), cmd.idempotencyKey())));1718 return order;19 }20}What I learned
- Draw the failure modes before the happy path.
- Consumer lag is the metric that predicts incidents, not CPU.