Kontakt
stefan.bente[at]th-koeln.de
+49 2261 8196 6367
Discord Server
Prof. Bente Personal Zoom
Adresse
Steinmüllerallee 1
51643 Gummersbach
Raum 3219
Sprechstunde nach Vereinbarung
Terminanfrage: calendly.com Wenn Sie dieses Tool nicht nutzen wollen, schicken Sie eine Mail und ich weise Ihnen einen Termin zu.

A variant of the service without a broker: instead of a handler that calls the aggregates one after the other as an orchestrator, the receiver publishes the incoming event as a Spring event, and each aggregate reacts to it itself (choreography). The page shows what changes for that, what you gain and what you pay for it.

Language
Deutsche Fassung
Example code repository
https://gitlab.com/archi-lab/public/learning/learn-event-driven-architecture/-/tree/main/service-development-2a-with-spring-events
Keywords
Event-driven architecture, Microservices, Spring events, ApplicationEventPublisher, EventListener, Orchestrator, Choreography, Handler, Receiver

The same reaction with Spring events

English version. The German README.de.md is the source of truth and is the text embedded by the archi-lab.io infopage.

This module is a variant of service-development-2-with-interfaces-and-tests. It solves the same task: the billing service reacts to the event RentalEnded, prices the ride and counts it. The difference is who calls the aggregates.

In module 2, the RentalEndedHandler does that. It is an orchestrator, that is, it knows all reactions to the event and calls them one after the other. In this variant there is no handler. The receiver publishes the event inside the service as a Spring event, and each aggregate reacts to it itself. This is called choreography. Everything else is as in module 2: the core, the mapper, the interface ChargeEvents and the tests.

The path of a message

flowchart LR
    Receiver["RentalEndedReceiver<br/>receive(payloadJson)"]:::new
    subgraph Charge["charge"]
        ChargeListener["RentalEndedChargeListener<br/>@EventListener"]:::new
        Charging["ChargingService"]:::existing
        Events["ChargeEvents<br/>(interface)"]:::existing
        ChargeListener --> Charging
        Charging --> Events
    end
    subgraph Ride["ride"]
        RideListener["RentalEndedRideListener<br/>@EventListener"]:::new
        Rides["RideCountService"]:::existing
        RideListener --> Rides
    end

    Receiver -- "RentalEnded<br/>as a Spring event" --> ChargeListener
    Receiver -- "RentalEnded<br/>as a Spring event" --> RideListener

    classDef existing fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px
    classDef new fill:#fff3cd,stroke:#ff9800,stroke-width:3px
    style Charge fill:#fafafa,stroke:#9e9e9e
    style Ride fill:#fafafa,stroke:#9e9e9e

Orange with a thick border is what is new or changed compared to module 2, green is what comes unchanged from module 2.

What changes compared to module 2

thkoeln.archilab.servicedevelopment
├── eventhandlers
│   ├── RentalEnded                     unchanged, now published as a Spring event
│   └── infrastructure
│       └── RentalEndedReceiver         changed: publishes the event
├── charge
│   └── application
│       └── RentalEndedChargeListener   new
└── ride
    └── application
        └── RentalEndedRideListener     new

The RentalEndedHandler and its test are gone.

The receiver publishes the event

public void receive( String payloadJson ) {
    events.publishEvent( mapper.toRentalEnded( payloadJson ) );
}

events is Spring’s ApplicationEventPublisher. The receiver no longer knows who reacts to the event. It only hands it to Spring.

Each aggregate reacts itself

@EventListener
public void on( RentalEnded rental ) {
    chargingService.chargeForRide( rental.rentalId(), rental.customerId(), rental.rideDuration() );
}

Spring calls every method with @EventListener whose parameter matches the type of the event. These classes really are called listeners, because they carry an annotation through which Spring calls them. That is the difference to the receiver, which carries none. Each listener lives in the application package of its aggregate. There it calls the service that knows the business rule.

The listeners run synchronously, in the same thread as the receiver. As in module 2, each service opens its own transaction.

Orchestrator or choreography?

The advantage of choreography: a new reaction to the event is a new class. Nobody has to change an existing class for it, not even the receiver.

The price: there is no longer one place where you see all reactions to the event and their order. Spring decides in which order the listeners run, unless you set it with @Order. And in the debugger you jump from publishEvent into the listeners, which is harder to follow than the two lines in the handler.

In our conventions the handler stays the standard, because it is easier to understand and to debug when you start out. This variant shows how it can be done differently.

Testing

The tests are the same as in module 2, with one exception. Instead of the handler, RentalEndedEventIT tests what the aggregates do with the event. To do so, the test publishes the event directly, in domain types:

@Test
void aFinishedRentalIsChargedCountedAndAnnounced() {
    events.publishEvent( new RentalEnded( RENTAL_ID, CUSTOMER_ID, Duration.ofMinutes( 20 ) ) );

    List<Charge> charges = chargingService.chargesOf( CUSTOMER_ID );
    assertThat( charges ).hasSize( 1 );
    assertThat( rideCountService.ridesOf( CUSTOMER_ID ) ).isEqualTo( 1 );
}

RentalEndedReceiverIT is unchanged. It still checks the whole path from the JSON to the published event ChargeCreated, now through the Spring events.

Running the module

Requires Java 21 and Maven. From the root directory of the repository:

mvn verify -pl service-development-2a-with-spring-events

The tests run without Kafka and without Docker; the database is an in-memory H2.