Long-Running Processes

A saga, or process manager, coordinates a business process that no single transaction can guarantee. It spans several components and often several systems. It takes as long as the business takes, which may be seconds, days, or weeks. Nothing can hold a lock for that long or across that many owners, so each step commits on its own. Nothing can roll the whole process back.

Booking a trip is the classic example. A flight, a hotel and a car have to be reserved, each held by a different component, and if the hotel comes back unavailable an hour later the flight is already booked. Undoing it is not a rollback but another step of the process: the flight is cancelled by the same thing that booked it. That is compensation. Deciding when it is needed is most of what a saga does.

Already know the pattern? Jump to the implementation

If you know what a saga or process manager is and only want to see how to implement one in Axon Framework, go straight to choosing an approach. A decision tree and comparison tables route you directly to the right approach.

The same shape turns up well short of trip booking, inside a single application. A rental is not confirmed until its payment is. An order is not shipped until stock and payment both agree. A subscription is not active until the first charge clears. In Axon Framework such a process is an event handler: it remembers what it has seen so far and decides what should happen next.

Axon Framework (since 5.0.0) has no Saga class. A process makes three decisions that are better made one at a time than bundled together:

  • Where its state lives, if it needs any. A blob nobody can query, a table with real columns, its own events, or nothing at all.

  • How an incoming event is correlated to the process it concerns. A stored mapping, a tag, or a value the process computes.

  • What starting and finishing mean. A command dispatched to begin and end it, most commonly; a row appearing and disappearing; an event recording that it is over; or nothing, because being finished is a question you ask of the events.

Those decisions pull in different directions for different processes. A process that has to be reported on wants columns. A process whose correlation is derivable needs no mapping at all. A process that is really a handful of independent reactions has no life cycle to manage.

So a process is written with the same building blocks as everything else: event handlers that react, command handlers that decide, and whichever form of state the process actually needs. The rest of this guide describes the shapes that combination takes and when to reach for each, starting from a worked example and a way of choosing an approach.

What "context" means on these pages

Two different things are called a context, and telling them apart decides which approaches are open to you. On these pages the bare word context always means the second one.

Bounded context is the modelling term: a boundary within which a term has one meaning. rental and payment in the bike rental example are two bounded contexts. They are a property of your code.

Context is the event store’s unit of separation, and in Axon Server it is a named, logical partition of events with its own stream. Ordering is preserved within one context and does not exist between two. Events in different contexts cannot be placed on a single timeline at all. It is a property of your deployment.

The two do not have to line up, and usually do not. Several bounded contexts commonly share one context. In the example, rental and payment know nothing about each other, yet their events sit in the same event store context. That is what allows a process to source both.

An event-sourced entity is sourced from one context. EventCriteria selects tags and types within a context, and there is no criteria that spans two. A process therefore cannot inject an entity assembled from events in more than one context. If the other side of the process lives in a different Axon Server context, or in a system that is not an Axon event store at all, state from context events is unavailable: it is the only approach that assembles an entity across both sides. The "Works across" column in the comparison table reports that constraint.

Coming from Axon Framework 4

This guide describes how to write a process, and does not assume you have written one before. If you are migrating, the saga migration path is the companion to it. It maps every Axon Framework 4 construct to what replaces it. It explains the axon-legacy module that keeps existing saga instances running while you write their replacement. It covers testing and the drainage strategy. This guide does not repeat any of that.

Reach for Workflows first

Workflows are the recommended way to orchestrate a process, whatever the process looks like. The flow is written as a single method that reads top to bottom, so the code is close to ordinary imperative code. The position within it is persisted for you: no state to keep, no correlation to derive, and no deadline pattern to build. Extending it later means adding a line in the middle of a method, not another handler and another piece of state. The recommendation holds for small processes as much as large ones.

Workflows are part of Axoniq Framework and are available as a preview, at no cost to try. Evaluate them before choosing one of the approaches below.

Workflows are not the only answer, and cost is not the reason. Not every reaction to an event needs to be written as a flow. The approaches in this guide are the right shape for those cases. Each needs nothing beyond Axon Framework, and each is proven by the same contract test in the example module.

Saga or process manager

The saga and the process manager are two names for this shape, and they carry a classical distinction. A saga matches each event to a command in a mostly linear flow; a process manager holds the state of the sequence and branches on it to pick the next step. A rule of thumb: once the flow needs if/else to decide what happens next, it is a process manager. Axon Framework draws no line between them: the same building blocks cover both, and the shapes this guide describes scale from the plainest event-to-command matching to the most branching.

That if is exactly what forces the first decision this guide is about. A branch has to read something the process remembered. The moment you have a process to manage rather than a single reaction, you have state to keep, and the question becomes where it lives. That is what choosing an approach answers.

Reactions, and when one becomes a process

Not everything that listens to an event and sends a command is a process. Most such things are a single reaction, and they need none of this guide.

A reaction is stateless when the trigger event carries everything the command needs:

@EventHandler
public CompletableFuture<?> react(BikeRequested event, CommandDispatcher dispatcher) {
    return dispatcher.send(new PreparePayment(paymentReferenceFor(event.rentalId()), PRICE))
                     .getResultMessage();
}

There is nothing to remember. The event processor already keeps the record of what it has handled, so the process needs no state of its own. If your whole "process" looks like this, write the handler and stop here.

It stops being a reaction the moment the decision depends on something that happened earlier. Consider approving a rental once its payment arrives. The payment event says a payment was confirmed and which reference it belongs to. It does not say which bike, or who asked for it. It certainly does not say whether the rental was ever requested, or has since been cancelled. Deciding needs two things the event does not carry:

  • Correlation. Which rental does this payment concern?

  • Prior state. Was that rental requested, and is it still waiting? Approving a rental nobody asked for, or one already turned down, is wrong.

Once a decision depends on prior state, that state has to come from somewhere: kept in a table, rebuilt from events, or recorded by the process itself. Choosing where is what the rest of this guide is about. Coordinating several such decisions towards an outcome is what makes it orchestration rather than a reaction.

"Needs prior state" is not the same as "needs a process class". A single slice can look up what it needs and stay independent of every other slice. Whether the process lives in one class or several is a separate question, decided below.

Choosing an event processor

Pick the processor before the approach. The recommended pooled streaming processor records what it has handled in a tracking token and recovers from a failed handler by delivering the event again. A subscribing processor keeps no token and handles the event inside the publisher’s transaction, so a failure surfaces to whoever published the event instead of being retried for you.

Two rules that apply to every approach

Whatever shape you choose, a process reacts to events and sends commands. That combination has two consequences you cannot design away. Most process bugs are a violation of one of them.

Commands a process sends must be idempotent

An event processor delivers at least once. A failure while handling a batch means those events are handled again, whether the processor redelivers them or the publication that produced them is retried. Every command a process sends will therefore sometimes be sent twice.

The fix belongs in the receiver, not the sender. Approving an already-approved request, or cancelling an already-settled payment, should append nothing and report success:

@CommandHandler
void handle(ApproveRequest command, @InjectEntity @Nullable Bike bike, EventAppender appender) {
    if (bike == null || !Objects.equals(bike.reservedBy(), command.renter()) || bike.reservationConfirmed()) {
        return; (1)
    }
    appender.append(new BikeInUse(command.bikeId(), command.renter(), bike.rentalId()));
}
1 Silently doing nothing is the point. A redelivery must be indistinguishable from the first delivery.

A useful trick is to make the receiver’s decision model naturally exclusive. If a payment is keyed by the caller’s own reference rather than by an identifier the payment mints, then "prepare a payment for this reference" cannot happen twice, and no guard has to be remembered.

Recorded progress must not outlive a failed command

A process that remembers anything writes in two places: its own state, and whatever records that the event was handled. The two do not have to commit atomically. Because the commands a process sends are idempotent, an event that arrives again re-dispatches, the receiver absorbs it, and the process catches up.

What no redelivery corrects is state that survives a command that failed, because the process then skips work it never did. Keep the write inside the handler, where a failed command rolls it back.

Returning the CompletableFuture from the event handler is what makes the processor wait for the command and treat a failed command as a failure of the event. Dropping the return turns the whole thing into fire-and-forget: the processor records the event as handled, the command is lost, and the process waits forever. Retrying is the processor’s job, and returning the future is how you give it back.

Two of the state approaches sidestep this rule entirely, because they never record anything of their own.