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