orders
OrderApplicationServiceExposed + Spring Modulith + DDD
Separate state-change responsibilities even inside one application and database. A published event becomes the module contract, and Modulith verification rejects compile-time references that bypass it.
OrderApplicationServiceShippingReservationHandlerIt does more than apply DDD labels to package names. It implements order acceptance and shipping reservation as separate state changes, uses separate transactions and tables, and automatically verifies both an allowed event reference and a forbidden internal reference.
AcceptOrderCommand enters orders, and only OrderApplicationService changes order state.
orders.events is a named interface; shipping may reference only orders :: events.
The fixture adds shipping → orders.internal, and ApplicationModules.verify() returns Violations.
Orders and shipping manage their own state and Exposed tables. Only orders :: events is published between the modules, and Modulith verification prevents shipping from referencing order internals directly.
orders :: events is published as a Named Interface, while ApplicationModules.verify() rejects a shipping → orders.internal reference.
Open the full-size diagram
orders owns order state, while shipping owns shipping reservation state. Shipping never references order internals; it receives only the published OrderAcceptedEvent.
WorkshopOrdersddd_modulith_orders · orderKey unique indexOrderAcceptedEventorders :: eventsshipping → orders.internalShippingReservationsddd_modulith_shipping_reservations · orderKey unique indexThis example has no standalone Order Aggregate Root class. Instead, it combines the application service transaction, repository persistence procedure, and Exposed table constraints to enforce the consistency required for order acceptance.
OrderApplicationService handles AcceptOrderCommand, keeping the state-change entry point in one place.
The order is inserted and selected inside one TransactionTemplate callback.
WorkshopOrders.orderKey.uniqueIndex() rejects a duplicate order key.
The row maps to OrderSummary; OrderAcceptedEvent is published after the transaction callback returns.
Order Aggregate Root with domain methods and let the repository map between that Aggregate and Exposed rows. The class diagram below includes only types that exist in the current source.When several features mutate the same tables and repositories directly, state-transition rules become scattered and change impact becomes difficult to predict. Areas with different language, transitions, and change cadence need explicit responsibilities and published contracts.
AcceptOrderCommandorderKey values with a unique indexACCEPTED stateOrderAcceptedEventorders :: eventsShippingReservationHandlerExposedShippingReservationRepositoryBoundaries reduce change and test scope, while event contracts, mapping, and failure handling add work. Separate what Modulith verifies from the guarantees an operational messaging design must provide.
Orders owns order rules; shipping owns reservation rules. Neither module calls the other's internal repository.
ApplicationModules.verify() checks compile-time references against packages and named interfaces.
Define the minimum information crossing the boundary and maintain mapping and compatibility as the internal models change.
This example uses an in-process @EventListener. It does not implement or guarantee an outbox, broker durability, duplicate handling, idempotency, or replay.
The tests cover a reservation created after order acceptance and an invalid fixture that directly references orders.internal.LeakyOrderRepository. The verifier does not validate business rules or runtime event delivery.
./gradlew :08-ddd-modulith-boundaries:test
AcceptOrderCommand
→ orders transaction
→ ExposedOrderRepository
→ ddd_modulith_orders
→ OrderAcceptedEvent
→ ShippingReservationHandler
→ shipping transaction
→ ddd_modulith_shipping_reservations