Bike Rental Event Model
Every approach in this guide implements the same process, so they can be compared line for line. A renter asks for a bike. The rental is not confirmed until payment is. If payment is refused, cancelled, or never arrives, the request is turned down and the bike released.
The domain
Two bounded contexts carry the process, and neither knows the other exists.
-
rentalholds bikes and rental requests. It never mentions payment. -
paymentis a generic payment context that can be paid for anything. It never mentions renting.
The process is the only place allowed to know both; that is what distinguishes it from a feature of either side. In the example, an ArchUnit test enforces this boundary, down to a check that no payment record carries a field named after a rental concept.
The two contexts share one event store context, even though they share nothing else. That is a deployment fact, not a modelling one. It is what leaves every state approach in this guide open. See what "context" means for how the distinction affects which approaches are available.
The event model
Read the model left to right:
-
Blue stickies are commands.
-
Orange stickies are events.
-
Green stickies are information a reader can see.
-
Purple stickies are automations: the process reacting to something and sending a command because of it.
The bottom two rows are the two bounded contexts, and the row between them is where every command enters and every view leaves.
The flow it draws is this. An operator registers a bike; the registered bikes are what the renter picks from. The
renter requests one, and the bike is held but not theirs: BikeRequested is a reservation, not a rental. The first
automation reacts to it by asking the payment side to prepare a payment. The renter pays, and a second automation
turns PaymentConfirmed into ApproveRequest, the moment the rental becomes real. Returning the bike ends
it.
The rest of the model handles the process failing partway through. A refused payment releases the bike. A released bike calls off its payment, so a rental turned down on grounds of its own cannot leave money outstanding. A cancelled payment releases the bike in turn.
Note what is not on the model: nothing labelled "saga". The process is the purple row, and how much of it lives in one class is the question the rest of this guide answers. Timeouts are not here either, since nothing on this model notices that a payment simply never arrived. That is a model of its own, on the deadlines page.
The timeout is drawn as its own model rather than edited into this one: adding it does not change anything already drawn, built or tested. Vertical Slice Architecture relies on the same property, adding behaviour without modifying it.
The shared events
The two sides know nothing about each other, so each publishes its own events and neither imports the other’s:
public record BikeRequested(
@EventTag(key = BIKE_ID) String bikeId,
String renter,
@EventTag(key = RENTAL_ID) String rentalId
) {
}
public record BikeInUse(
@EventTag(key = BIKE_ID) String bikeId,
String renter,
@EventTag(key = RENTAL_ID) String rentalId
) {
}
public record RequestRejected(
@EventTag(key = BIKE_ID) String bikeId,
String renter,
@EventTag(key = RENTAL_ID) String rentalId
) {
}
public record PaymentPrepared(
@EventTag(key = "paymentId") String paymentId,
int amount,
@EventTag(key = PAYMENT_REFERENCE) String paymentReference (1)
) {
}
public record PaymentConfirmed(
@EventTag(key = "paymentId") String paymentId,
@EventTag(key = PAYMENT_REFERENCE) String paymentReference
) {
}
public record PaymentRejected(
@EventTag(key = "paymentId") String paymentId,
@EventTag(key = PAYMENT_REFERENCE) String paymentReference
) {
}
public record PaymentCancelled(
@EventTag(key = "paymentId") String paymentId,
@EventTag(key = PAYMENT_REFERENCE) String paymentReference
) {
}
| 1 | The payment side stores and echoes the caller’s reference without ever interpreting it. It has no idea rentals exist, so a process can be the only thing that knows both sides. |
The full, runnable source lives in examples/saga-recipes in the
Axon Framework repository.
The behaviour every approach must produce is pinned by one shared
contract test,
subclassed once per approach, so the approaches are interchangeable, not merely claimed to be. Its scenarios mirror
the Axon Framework 4 sample application’s saga test, plus a redelivered trigger and a timeout that arrives after the
payment already settled. Testing covers how they are written and run.
