exposed-workshop / Visual Companion

Exposed + Spring Modulith + DDD

Orders persist through their own repository; shipping creates reservations only from a published event

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.

Orderorders transaction
Contractorders :: events
Shippingshipping transaction
Accept order → reserve shippingApplicationModules.verify()
Bounded Context

orders

OrderApplicationService
ddd_modulith_orders: ORD-1001 / ACCEPTED
OrderAcceptedEvent
Bounded Context

shipping

ShippingReservationHandler
ddd_modulith_shipping_reservations: ORD-1001 / RESERVED
    Modules connect through a published eventVerification passed

    Why this example represents applied DDD

    It 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.

    Business rule

    State-change responsibility is visible

    AcceptOrderCommand enters orders, and only OrderApplicationService changes order state.

    Module contract

    The published surface is executable

    orders.events is a named interface; shipping may reference only orders :: events.

    Verification

    A bad design becomes a failing test

    The fixture adds shipping → orders.internal, and ApplicationModules.verify() returns Violations.

    Modulith boundaries and verification flow

    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 Context connected to Shipping Context through a published event, with ApplicationModules verify enforcing the boundary
    Spring Modulith published boundary and verification flow Only orders :: events is published as a Named Interface, while ApplicationModules.verify() rejects a shipping → orders.internal reference. Open the full-size diagram

    Architecture diagram

    orders owns order state, while shipping owns shipping reservation state. Shipping never references order internals; it receives only the published OrderAcceptedEvent.

    How Exposed implements the order Aggregate consistency boundary

    This 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.

    1. Command boundary

    OrderApplicationService handles AcceptOrderCommand, keeping the state-change entry point in one place.

    2. Transaction boundary

    The order is inserted and selected inside one TransactionTemplate callback.

    3. Database constraint

    WorkshopOrders.orderKey.uniqueIndex() rejects a duplicate order key.

    4. Persisted result

    The row maps to OrderSummary; OrderAcceptedEvent is published after the transaction callback returns.

    Current scope: this is a compact Transaction Script for order acceptance. If transition rules grow, introduce an 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.

    Class diagram

    OrderAcceptedEvent crosses the published contract

    When DDD becomes necessary

    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.

    orders

    • Interpret AcceptOrderCommand
    • Reject duplicate orderKey values with a unique index
    • Insert the new ACCEPTED state
    • Publish OrderAcceptedEvent
    Published contractorders :: events

    shipping

    • Receive the event in ShippingReservationHandler
    • Apply reservation rules
    • Persist with ExposedShippingReservationRepository
    • Never reference the order's internal repository

    DDD effects and added costs belong in the same design

    Boundaries 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.

    State-change responsibility stays local

    Orders owns order rules; shipping owns reservation rules. Neither module calls the other's internal repository.

    Reference rules are verified automatically

    ApplicationModules.verify() checks compile-time references against packages and named interfaces.

    Event contract maintenance is added

    Define the minimum information crossing the boundary and maintain mapping and compatibility as the internal models change.

    Durability remains a separate concern

    This example uses an in-process @EventListener. It does not implement or guarantee an outbox, broker durability, duplicate handling, idempotency, or replay.

    Run the accepted flow and the boundary violation in one test suite

    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.

    Command and processing path
    ./gradlew :08-ddd-modulith-boundaries:test
    
    AcceptOrderCommand
      → orders transaction
      → ExposedOrderRepository
      → ddd_modulith_orders
      → OrderAcceptedEvent
      → ShippingReservationHandler
      → shipping transaction
      → ddd_modulith_shipping_reservations