Upgrade to Pro — share decks privately, control downloads, hide ads and more …

It Takes Two to Tango – Designing Module Intera...

It Takes Two to Tango – Designing Module Interactions in Modulithic Spring Applications

According to Russell Ackoff “A system is not the sum of the behaviors of its parts, but the product of their interactions”. That’s why the design of those interactions is of the uttermost importance. In a Spring application, the primary means to establish relationships between application components is dependency injection (DI). With higher-level structuring approaches like Spring Modulith’s application modules in place, should the interaction of those be implemented by DI as well?

The talk presents different approaches to designing application module interactions and compares them regarding their applicability, effect on testability, consistency, error scenarios and how they affect the modularity of the system overall.

Oliver Drotbohm

July 31, 2024
Tweet

More Decks by Oliver Drotbohm

Other Decks in Programming

Transcript

  1. "A system is a whole that consists of parts, each

    of which can affect its behavior and properties. … Each part of the system is dependent on some other part."
  2. A B

  3. "A system is never the sum of its parts, it's

    the product of their interactions."
  4. Controller Service Repository Layered Hexagonal Onion Presentation → Business →

    Persistence Driving Adapter → ← Driven Adapter Port ← Application → Port Infrastructure → ← Infrastructure Application / Domain Spring DI DI
  5. @Service @RequiredArgsConstructor class Checkout { private final OrderRepository orders; @Transactional

    void complete(Order order) { orders.save(order.complete()); } } Primary State transition Internal, technical decomposition
  6. @Service @RequiredArgsConstructor class Checkout { private final OrderRepository orders; private

    final Inventory inventory; private final RewardsProgram rewards; private final Notifications notifications; @Transactional void complete(Order order) { orders.save(order.complete()); inventory.updateStock(…); rewards.registerRewards(…); notifications.sendOrderConfirmation(…); } } @Service class Inventory { } @Service class RewardsProgram { } @Service class Notifications { } Secondary Required at startup / in tests Primary Interaction expands transaction
  7. @SpringBootTest class CheckoutTests { @Autowired Checkout checkout; @MockBean Inventory inventory;

    /" Other mocks @Test void completesOrder() { var order = new Order(…); checkout.complete(order); verify(inventory).updateStock(…); } } Collaborator are mocked Given / When / Then Testing focussed on orchestration
  8. @Service @RequiredArgsConstructor class Checkout { private final OrderRepository orders; private

    final ApplicationEventPublisher events; @Transactional void complete(Order order) { orders.save(order.complete()); events.publishEvent( OrderCompleted.of(order.getId())); } } @Service class Inventory { @EventListener void on(OrderCompleted event) {} } … Primary Secondary Invokes Only internal dependencies
  9. @RecordApplicationEvents /# or *% @ApplicationModuleTest class CheckoutTests { private final

    Checkout checkout; @Test void completesOrder(AssertableApplicationEvents events) { var order = new Order(…); checkout.complete(order); assertThat(events) .contains(OrderCompleted.class) .matching(OrderCompleted:#id, order.getId()); } } Given / When / Then Spring Modulith Records events published during test execution Spring Framework Testing focussed on signal
  10. @Service @RequiredArgsConstructor class Checkout { private final OrderRepository orders; private

    final ApplicationEventPublisher events; @Transactional void complete(Order order) { orders.save(order.complete()); events.publishEvent( OrderCompleted.of(order.getId())); } } @Service class Notifications { @EventListener void on(OrderCompleted event) { /" Interact with SMTP server } } Primary Secondary What if this takes long?
  11. @Service @RequiredArgsConstructor class Checkout { private final OrderRepository orders; private

    final ApplicationEventPublisher events; @Transactional void complete(Order order) { orders.save(order.complete()); events.publishEvent( OrderCompleted.of(order.getId())); } } @Service class Notifications { @EventListener void on(OrderCompleted event) { /" Interact with SMTP server } } Primary Secondary What if this succeeds but this rolls back?
  12. @Service @RequiredArgsConstructor class Checkout { private final OrderRepository orders; private

    final ApplicationEventPublisher events; @Transactional void complete(Order order) { orders.save(order.complete()); events.publishEvent( OrderCompleted.of(order.getId())); } } @Service class Notifications { @TransactionalEventListener void on(OrderCompleted event) { /" Interact with SMTP server } } Primary Secondary Triggered on transaction commit
  13. @Service @RequiredArgsConstructor class Checkout { private final OrderRepository orders; private

    final ApplicationEventPublisher events; @Transactional void complete(Order order) { orders.save(order.complete()); events.publishEvent( OrderCompleted.of(order.getId())); } } @Service class Notifications { @Async @TransactionalEventListener void on(OrderCompleted event) { /" Interact with SMTP server } }
  14. @Service @RequiredArgsConstructor class Checkout { private final OrderRepository orders; private

    final ApplicationEventPublisher events; @Transactional void complete(Order order) { orders.save(order.complete()); events.publishEvent( OrderCompleted.of(order.getId())); } } @Service class Inventory { @ApplicationModuleListener void on(OrderCompleted event) { /" Interact with SMTP server } }
  15. @ApplicationModuleTest class CheckoutTests { private final Checkout checkout; @Test void

    completesOrder(Scenario scenario) { var order = new Order(…); scenario.stimulate(() -% checkout.complete(order)) .andWaitForEventOfType(InventoryUpdated.class) .matchingMappedValue(InventoryUpdated:#getOrderId, order.getId()) .toArrive(); } } Spring Modulith Given / When / Then
  16. Summary Functional decomposition first, technical decomposition second Align consistency scope

    and testing with functional boundaries In modulithic arrangements, use spectrum of integration options and consistency levels Spring Modulith for advanced support
  17. Classic Event Listener Application Module Listener Module references via Spring

    beans via @EventListener via @ApplicationModuleListener (@Async @TransactionalEventListener) Integration style synchronous synchronous Asynchronous / Transaction-bound Tests Orientation vertical / horizontal vertical vertical Focus Module interaction Module operation outcome signaled by an event Module operation outcome signaled by an event Approach via mocks / @MockBean via AssertablePublishedEvents via Scenario Risk Integration tests including multiple modules Tests exclude code that could break transaction at runtime Asynchronous module interaction Consistency Boundaries Potentially (accidentally) spanning multiple modules Potentially (accidentally) spanning multiple modules Aligned with module boundaries / Eventual Consistency / Mechanism Transaction Transaction Event Publication Registry Module coupling