Skip to content

[#4619] Add saga-recipes example and a Sagas and Process Managers guide - #4969

Open
MateuszNaKodach wants to merge 51 commits into
mainfrom
saga-recipes-module-setup
Open

[#4619] Add saga-recipes example and a Sagas and Process Managers guide#4969
MateuszNaKodach wants to merge 51 commits into
mainfrom
saga-recipes-module-setup

Conversation

@MateuszNaKodach

@MateuszNaKodach MateuszNaKodach commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Adds examples/saga-recipes, which implements the bike-rental payment saga five ways (repository, injected entity, process events, direct append, automation slices) behind one shared contract test, so the approaches are demonstrably interchangeable rather than merely claimed to be, plus the deadline replacement as a projection of outstanding work and a sweep that sends a command.

Adds docs/saga-guide, a standalone guide alongside meta-annotations-guide and message-handler-customization-guide, with a page per approach, a comparison table, and Workflows presented as the recommended first choice where an Axoniq licence allows it. The guide describes writing a process on its own terms and never explains what Axon Framework 4 used to do.

Everything version-4-specific lives in migration:paths/sagas.adoc instead: what maps to what, why there is no replacement construct, the workflow translation, why the timeout became a command, and the axon-legacy module (#3728, #3065) that will keep existing saga instances running while their replacement is written.

Also fixes university-java-springboot-4, which could not start its Spring context on Spring Boot 4.1.0, by moving to 4.1.1.

In doing the above, this PR resolved #4619.

@MateuszNaKodach
MateuszNaKodach requested a review from a team as a code owner August 21, 2026 14:14
@MateuszNaKodach
MateuszNaKodach requested review from hatzlj, jangalinski, laura-devriendt-lemon, smcvb and zambrovski and removed request for a team August 21, 2026 14:14
@MateuszNaKodach MateuszNaKodach self-assigned this Aug 21, 2026
@MateuszNaKodach MateuszNaKodach added Priority 1: Must Highest priority. A release cannot be made if this issue isn’t resolved. Type: Documentation Use to signal issues that describe documentation work. labels Aug 21, 2026
@MateuszNaKodach MateuszNaKodach added this to the Release 5.3.2 milestone Aug 21, 2026
@smcvb smcvb modified the milestones: Release 5.3.2, Release 5.4.0 Aug 24, 2026

@smcvb smcvb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of comments on this one. But, over arching, I have three larger concerns:

  1. I am missing the guide to steer people to a plain event handler if the process is simple enough that it doesnt require any state.
  2. The stateful event handler approach is called event sourced. Although correct, the state can come from anything. You essentially have 3 examples here, where one detailed, and 2 just mentioned shortly would suffice
  3. I am afraid we are giving to much options now. We want to simplify the users choice on what to do, not give them too much choice.

Comment thread docs/saga-guide/modules/ROOT/pages/workflows.adoc
Comment thread docs/reference-guide/modules/migration/pages/paths/sagas.adoc
@@ -0,0 +1,124 @@
= State From the Process's Own Events

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this LLM comment as is:

Documentation — [MID] — the guide's own index states as a blanket rule that deferring a write into a callback on the command's future breaks atomicity with the tracking token. This recipe (.../examples/sagas/stateprocessevents/RentalPaymentProcess.java:118-133) does exactly that via EventAppender and is correct — EventAppender binds to the ProcessingContext, not a thread-local transaction — but the guide never reconciles the exception with its own stated rule, leaving a careful reader wondering whether the recipe is broken.

Comment thread docs/saga-guide/modules/ROOT/pages/index.adoc Outdated

*This is the recommended way to orchestrate a business process, and the first option to consider.* Orchestration is
what a workflow is for, and writing the flow as a flow removes most of what the other pages spend their length on. The
reason those pages exist is that a workflow is not available to everyone:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frankly, I'd prefer we state that we have other options because not everything should be a workflow or saga, not because of licensing reasons.

Comment thread docs/saga-guide/modules/ROOT/pages/workflows.adoc Outdated
* limitations under the License.
*/

package sagas.statedomainevents;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious: Why call this domain events? Triggered a bit as we're deliberately staying away from the term and now it's being reintroduced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to "context events" but please remember that domain event and saga and maybe more are not Axon concepts, so I do not refer to "DomainEventMessage" which was coupled to the single aggregate :)

Comment thread docs/saga-guide/modules/ROOT/pages/state-from-process-events.adoc

@smcvb smcvb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of comments on this one. But, over arching, I have three larger concerns:

  1. I am missing the guide to steer people to a plain event handler if the process is simple enough that it doesnt require any state.
  2. The stateful event handler approach is called event sourced. Although correct, the state can come from anything. You essentially have 3 examples here, where one detailed, and 2 just mentioned shortly would suffice
  3. I am afraid we are giving to much options now. We want to simplify the users choice on what to do, not give them too much choice.

@MateuszNaKodach

Copy link
Copy Markdown
Contributor Author

Lots of comments on this one. But, over arching, I have three larger concerns:

  1. I am missing the guide to steer people to a plain event handler if the process is simple enough that it doesnt require any state.
  2. The stateful event handler approach is called event sourced. Although correct, the state can come from anything. You essentially have 3 examples here, where one detailed, and 2 just mentioned shortly would suffice
  3. I am afraid we are giving to much options now. We want to simplify the users choice on what to do, not give them too much choice.

I read the main comment so far and your concerns are common with mine. But I was waiting for same conclusions… whereas it's difficult to make it thinner. I w need to be silent about some possibilities then ;D

@smcvb

smcvb commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Lots of comments on this one. But, over arching, I have three larger concerns:

  1. I am missing the guide to steer people to a plain event handler if the process is simple enough that it doesnt require any state.
  2. The stateful event handler approach is called event sourced. Although correct, the state can come from anything. You essentially have 3 examples here, where one detailed, and 2 just mentioned shortly would suffice
  3. I am afraid we are giving to much options now. We want to simplify the users choice on what to do, not give them too much choice.

I read the main comment so far and your concerns are common with mine. But I was waiting for same conclusions… whereas it's difficult to make it thinner. I w need to be silent about some possibilities then ;D

Entirely fair Mateusz! I think we've discussed this quite well yesterday on how to proceed. Once those changes are in place, I am confident we'll be able to merge this extensive guide.

@hatzlj hatzlj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave it skim and like how detailed it is and that it describes the different options available, but i second @smcvb 's concern that it might overwhelm users with too much choice.

One thing i noticed across the whole documentation included in this PR is the difference in the tone compared to the rest of the documentation. It's a lot more rhetorical and discursive (supposedly LLMs style) than the rest of the reference guide in my opinion, which is noticeable even more especially because it's a lot of content en-bloc. I want to make clear that this is my subjective perception, I am not judging the style per-se, but personally I struggle a lot with comprehending when reading docs in that style.
I am curious if you also perceive that difference in tone and if so, I think we should address it and either consciously decide to allow this or add guardrails for LLMs to keep the style and tone of the written docs more concise. I understand it will be more and more LLMs reading our docs but from time to time still humans might consult it (and i don't mean that sarcastic).

Comment thread examples/university-java-springboot-4/pom.xml

@smcvb smcvb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structure definitely improved, thanks for that!
However, still quite a collection of comments to process.

Two larger pointers I saw reoccurring are (1) the assumption tokens are present and (2) the assumption transactions "safe everything" whereas the sources aren't in the same location. Both need to be addressed for correctness reasons.

Lastly, @hatzlj his remark on "how" this reads is on-par. Some parts just scream LLM output to me. If I'd be reading a documentation that screams "we did not invest time in writing this", I am not willing to invest time in reading it. Ergo, it builds a negative connotation with the product, even if genuine time and effort has been put into it. We should ensure this "LLM-film" doesn't overtake our documentation and JavaDoc.

Comment thread docs/reference-guide/modules/migration/pages/paths/sagas.adoc Outdated
Comment thread docs/saga-guide/modules/ROOT/pages/index.adoc Outdated
= Long-Running Processes
:navtitle: Long-running processes
:reftext: Long-running processes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am missing some guidance on the rest of the pages that are present. Our other index.adoc pages tend to start with some paragraphs, followed by a listing of everything that's contained in this chapter. I'd wager it's good if we do the same here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pages here are the approaches, and choosing-an-approach.adoc already lists them in a table with when to reach for each. A second listing on the index would repeat that, so I’d rather leave the index leading with the decisions a process makes and let the comparison table do the routing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it would be nice if there's a quick link to choosing-an-approach from this first introduction, for people that do not care about the entire story that's drafted here. That's what the context tables on all other index.adoc pages is for; to not force people to go through everything if they don't need it.

Comment thread docs/saga-guide/modules/ROOT/pages/choosing-an-approach.adoc Outdated
* The closest of the recipes to an Axon Framework 4 saga. Two things happen per step, writing a row and dispatching a
* command, and getting them to agree is this recipe's whole difficulty.
* <p>
* <b>Where the transaction comes from.</b> Nowhere in this class, which is the point. When a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left a comment in the guide why I think it's off, as it anticipates the place where the state and events are published reside in the same data source, which is simply not a given.

Comment thread docs/reference-guide/modules/migration/pages/paths/sagas.adoc
Comment thread docs/CLAUDE.md
Comment thread docs/CLAUDE.md Outdated
MateuszNaKodach added a commit that referenced this pull request Sep 1, 2026
The testing and deadlines pages carried their Java inline, so nothing
checked it. Compiling the three snippets shows why that mattered: they
referenced a payloadsOf helper, a PaymentId type and a BikeRegistered
event that do not exist in the guide's example sources. They are now
real code in sagas/testing and sagas/deadlines, included by tag like
every other sample.

This needs the saga-guide examples as test sources rather than main
ones, since AxonTestFixture, JUnit, AssertJ and Awaitility are all
test-scoped. Test scope is a superset of compile scope, so the rest of
the module still compiles, which is the same reasoning already applied
to the migration examples.

Also split VerticalSlices into one file per slice. The slices were
public static nested classes purely so two tagged snippets could share a
file, and an example should not suggest writing an event handling
component that way. One class per slice also matches the layout the
saga-recipes module uses.
Axon Framework 5 has no Saga SPI, no SagaStore and no SagaTestFixture, so
projects migrating an Axon Framework 4 Saga need worked alternatives rather
than prose. This module will host four of them, applied to the bike-rental
payment process from the version 4 demo, so the migration path documentation
can point at running, tested code instead of describing it.

The rental context lands first because it fixes the constraint every later
recipe has to work around. It keeps the version 4 shape deliberately: one Bike
entity, no Rental entity, and commands that target bikeId while naming the
renter. Introducing a Rental entity would let ApproveRequest address itself,
which would leave the saga with nothing to remember and reduce the recipes to
a solution for a problem that had been designed away. Keeping the original
shape keeps the saga's job real: it must hold bikeId and renter, which no
single entity owns.

Events additionally carry rentalId and tag the renter. Tags cannot be added
retroactively to an existing stream, and the renter tag immediately earns its
place: the request-bike slice sources across the bike tag and the renter tag
at once to enforce that a renter holds at most one bike, an invariant no
single-aggregate model can express. Its criteria is symmetric on purpose, as
omitting a releasing event type would let a concurrent return slip past the
append condition and permit a second simultaneous rental.

There is no JPA. Spring Boot 4 plus Hibernate plus the Axon starter currently
deadlock on a circular reference between applicationTaskExecutor and Axon's
UnitOfWorkFactory, which also breaks the untouched university-java-springboot-4
example; that is latent there because its tests are named *IT and therefore
skipped by Surefire. Dropping JPA sidesteps it, and costs nothing worth having:
with Axon Server the events and tokens live there anyway, so persistence was
only serving state that later recipes can hold in memory while documenting what
a production implementation must do instead.
…Axon build

The previous commit dropped JPA to escape a circular reference between
applicationTaskExecutor and Axon's UnitOfWorkFactory. That diagnosis was
incomplete: the cycle is a Spring Boot 4.1.0 regression, fixed in 4.1.1, and
not a consequence of combining JPA with the Axon starter at all. Moving to
4.1.1 lets JPA come back, which matters because the repository recipe's whole
argument is that its process state commits in the same transaction as the
tracking token. An in-memory map cannot demonstrate that.

Version alignment follows from the same reasoning. The Axoniq Framework BOM
pins a released Axon Framework rather than the one built here, so importing
axon-framework-bom at ${project.version} ahead of it keeps the example honest:
it exercises the branch under development instead of silently testing against
a published artifact. The Axoniq artifacts move to the matching 5.4.0-SNAPSHOT
line so the commercial starter and the framework stay in step.

Restoring JPA also brings back the aggregate-based JPA event storage engine,
which is ordered ahead of the in-memory engine and rejects multiple tags per
event outright. Every append in the request-bike slice carries three tags, so
that engine has to be excluded for the Dynamic Consistency Boundary criteria
this example is built around to work at all. No @EntityScan is declared: Spring
Boot already registers entities under the application package and Axon adds its
own through @RegisterDefaultEntities, so declaring one would replace both sets
rather than extend them.
The example cannot start its application context. Spring Boot 4.1.0 combined
with JPA and the Axon starter deadlocks on a circular reference: creating
applicationTaskExecutor resolves Axon's UnitOfWorkFactory, which needs the
transaction manager, which needs the entity manager factory, which needs the
entity manager factory builder, which needs applicationTaskExecutor again.
Spring Boot 4.1.1 resolves it, so this is a version bump rather than a change
to the example.

The breakage went unnoticed because both tests in this module are named *IT and
are therefore run by Failsafe under the integration-test profile, not by
Surefire during an ordinary build. Nothing in the default build exercised the
context, so a module that could not start still reported success. Both tests
pass again after the bump.

A comment records why 4.1.0 must not come back, since a routine dependency
bump would otherwise reintroduce a failure that is invisible to the default
build.
…slice

The payment context is deliberately generic: it can be paid for anything. It
carries no rental identifier, no rental type, no rental tag and no import from
the rental context. What a caller passes instead is an opaque paymentReference
that this context stores and echoes on every event without interpreting it,
exactly as Axon Framework 4 did. Two ArchUnit rules and a reflective check on
record components enforce that, because a boundary this easy to erode by reflex
is worth asserting rather than merely documenting.

Keying the prepare and cancel decision models on that reference rather than on
the payment identifier is what makes preparing idempotent by construction: a
redelivered command sources the payment that already exists and appends
nothing, and two racing commands cannot both win because the append condition
covers precisely the events the decision read. It also lets a caller cancel a
payment using only the key it chose itself, which removes the reason the
version 4 saga had to remember a generated payment identifier at all.

Writing the payment tests exposed a wrong assumption that had already spread
through the rental context. A plain @EntityCreator is only invoked once the
stream holds at least one selected event, so a handler that must cope with a
missing entity has to say so; otherwise EntityNotFoundException is raised
before the handler runs. Every slice now takes a nullable entity, which is the
mechanism the framework documents for create-if-missing flows and which its own
javadoc prefers over forcing creation. Registering a bike, approving or
rejecting a request that was never made, returning a bike that was never taken
out, and confirming a payment that does not exist were all affected.

Two further problems surfaced from the same tests. Entities keyed by a tag
source every event carrying it, so a bike-keyed decision model met
BikeRegistered first and had no creator for it; each slice now narrows its
criteria to the event types it actually evolves on, which fixes the failure and
tightens the consistency boundary at the same time. And the request-bike test
for an unregistered bike had been passing only because a shared renter name let
other tests' events create the entity for it. Renters are now unique per test:
the renter is a tag, and the Spring context and its event store are shared
across the whole run, so a fixed name silently couples unrelated tests.
Everything that knows both about renting and about paying now has a home. Its
one piece of privileged knowledge is that a rental identifier and a payment
reference are the same value; deriving the reference rather than storing it is
what lets the recipes that keep no state correlate in both directions at all.

Sequencing needs more than a property name. Rental events keep the correlation
in rentalId and payment events keep it in paymentReference, so no single
PropertySequencingPolicy spans them, and the two composition helpers the
framework offers both work by attempting a conversion per candidate policy.
That is a poor fit twice over: reading one event type as another is not
reliably an error, since a lenient converter can yield a record of nulls rather
than throwing, and every miss costs a conversion. Routing on the QualifiedName
avoids both, because the name is available without touching the payload.

Two details in the routing table are load-bearing and easy to get wrong. Every
extractor yields the raw String: returning RentalId from one side and
PaymentReference from the other would never compare equal, so the two contexts
would land in separate sequences and the policy would quietly do nothing. And
each entry names a concrete payload type rather than a shared supertype, which
would work in memory, where the payload is already an object, and fail against
a real event store, where it arrives as bytes a converter cannot turn into an
interface.

The test builds every message the way the store delivers one, from a byte array
plus a converter. That is deliberate: GenericMessage short-circuits payloadAs
when the requested type is already assignable from the payload, so a test using
plain objects never invokes the converter and would pass even where conversion
is impossible in production.

CancelRentalPayment replaces the version 4 deadline. Expressing a timeout as a
command rather than a scheduled callback also makes it reachable on purpose,
which is what allows the timeout path to be tested at all now that the test
fixture has no way to manipulate time.
…satisfy

The contract test is the point of this module. Claiming that several
implementations of a saga are interchangeable is easy; running identical
scenarios against each and watching them agree is what makes it true. The
scenarios mirror the Axon Framework 4 PaymentSagaTest method for method so the
migration guide can put them side by side, plus two cases version 4 never had:
a redelivered trigger, and a timeout arriving after the payment already
settled.

What keeps the contract shareable is a rule about what it may assert on: only
commands and events crossing the rental and payment boundaries. Repositories,
entities and process events are precisely what the recipes disagree about, so
they belong in a subclass or nowhere.

The first recipe keeps no state at all. Everything it needs was already
recorded by one context or the other, so instead of copying it, the process
rebuilds the answer per event from a decision model spanning both tag keys.
That leaves exactly one effect per handler, which means the ordering hazard the
state-keeping recipes must work around cannot arise here: a failed command
appends nothing, the token stays put, and the redelivery rebuilds an identical
model.

Reacting to two contexts that name their correlation differently needs an
identifier resolver of its own, since neither the idProperty shortcut nor the
TargetEntityId lookup spans rentalId and paymentReference. It routes on the
qualified name for the same reason the sequencing policy does: the name is
readable without touching the payload, and each event converts once to its own
concrete type rather than to a supertype that no converter could produce from
bytes.

Two testing details worth keeping. Assertions filter to each test's own
identifiers rather than matching the recorded list exactly, because the
recording command bus accumulates across every test sharing a Spring context
and is only reset by a when phase these event-driven scenarios do not use. And
no recipe is active in tests by default: a running process reacts to the events
a slice test publishes in its given phase and appends more of its own, which is
how the return-bike test started seeing a PaymentPrepared it never asked for.
Three implementations now satisfy the same contract, which is the first point
at which the module actually demonstrates anything: the scenarios are identical
and only the way the process remembers things differs.

The repository recipe is the closest to an Axon Framework 4 saga and the one
that has to be most careful, because two things happen per step and no
transaction spans both. It dispatches first and records only once the command
succeeded; the reverse order is a quiet bug that wedges a process forever, as
the failed dispatch leaves the token where it is, the event is redelivered, and
the row now claims the work was done. The write is deferred to
runOnPrepareCommit rather than performed in the callback completing the
command's future, so it lands in the same unit of work that stores the tracking
token: that callback may run on a thread with no transaction bound to it, and
it would run whether or not the batch later commits.

The event-sourced recipe records its own facts instead. That is what makes it
work where the derived-state recipe cannot, since it needs nothing from the
payment context beyond being told something happened, and it is the only recipe
that leaves an audit trail of the process itself. Recording goes through a
command so that every event still comes from one, keeping the process's own
write visible on an event model as a write slice; the variant that appends
directly follows separately.

Two problems surfaced while wiring these up. Nested entity classes are not
covered by the condition on their outer class, so every recipe's entity was
registered at once and Spring rejected the second for deriving the same bean
name; each entity now carries the condition itself. And the sequencing policy
test no longer starts a Spring context: it needs nothing from the application,
and sharing the default context exposed it to Spring's pause-and-restart of
cached contexts, on which Axon re-subscribes its handlers and fails with a
duplicate command handler subscription.
MateuszNaKodach and others added 19 commits September 1, 2026 23:52
The recommendation was stated once, at the top of the index, and then not
mentioned again. A reader arriving on a state page from search or from the nav
never saw it, and the pages themselves read as a flat menu of equals.

Every approach page now carries it, phrased in that page's own terms: the
repository page notes there is no table to design, the context-events page that
workflows ask nothing of the events at all, the deadlines page that waiting is
an ordinary statement in the flow so none of that page applies.

The recommendation is also no longer conditional on the process. It previously
carved out an exception for processes small enough that the flow was never in
doubt, which reads as advice to grow into a workflow rather than start in one.
Processes grow, and adding a step to a workflow is adding a line where every
other approach makes it a handler, a piece of state and a correlation, so the
recommendation holds at any size. The automation-slices page keeps its "start
here" advice, now scoped to the case where workflows are not available.

Titles fixed to Chicago style, which Vale treats as a build error.
Naming, mostly. "Version 4" is ambiguous on a page whose whole subject is two
versions, so every occurrence now names Axon Framework 4 outright.

Two rewordings go further than wording. Workflows are no longer introduced as
"if you are on Axoniq Framework", because the recommendation does not depend on
which framework the reader is on; the caveat moves into a note, and says that
trying them out is free even though production use is licensed. "Choosing a
replacement without Axoniq Framework" becomes "Choosing a replacement", since
the reason to pick something else is that not every saga is a flow, not that
workflows are unavailable.

The deadline section opened on a sentence nobody could parse. It was trying to
say that sagas scheduled deadlines for two reasons that no longer call for one:
retrying a step, now handled by leaving the token where it is, and cancelling a
deadline, now unnecessary because nothing is scheduled. It says that.

The testing section claimed SagaTestFixture has no replacement, which reads as
worse news than it is: a saga still running on the legacy module keeps its
existing tests, and it is the replacement process that needs a new one. The
expectActiveSagas note now says why nothing counts live instances any more.

Also pins the legacy module to 5.4.0 in both this page and the migration index,
rather than the vaguer "not released yet".
Review point: the guide explained the four non-workflow approaches by saying
workflows need a licence, which is both the wrong reason and discouraging. The
real reason is that not everything reacting to an event is a process worth
writing as a flow. A handful of independent reactions is better modelled as
that, and forcing it into one method buys a centre it never needed.

So the licence moves into a note, phrased as what it is: trying workflows costs
nothing, only running them in production is licensed. What argues against a
workflow is now the shape of the work.

The idempotency rule leaned on tracking-token behaviour the reader was assumed
to know. It now links the streaming processor's tracking token section and says
what a commit failure actually causes.

The testing section ended on a bare pointer to the deadlines page. It now says
what makes the timeout path different, namely that the fixture cannot move the
clock, so "now" has to be a parameter.

The examples folder was still called statedomainevents after its page became
state-from-context-events, reintroducing a term the documentation deliberately
avoids.
…ariants

Review points, all on the example module.

The classes were called PaymentSaga even though nothing here is a saga in the
framework sense. They are PaymentProcess now, which happens to follow the saga
paradigm rather than announcing it. PaymentSagaState and its repository follow.

The two event-sourced recipes shared their events, their decision model and
their behaviour, and differed only in the two lines that write a fact down, yet
lived in separate packages with a duplicated nested State. They now share one
package and one ProcessState, so what the choice between them actually costs is
visible rather than asserted, and the "same as" in the Javadoc can name what it
means. Splitting the state out needs a condition covering both recipes, hence
the expression.

Test method names lose their underscores. The style was borrowed from an older
example that predates the current conventions.

Smaller ones: two payment commands were documented as though they were events;
an event handler took a parameter called command; the rental context had no
package-info while payment did; a @value was fully qualified inline; a join()
had no timeout; @SInCE has no place on a test class; and the module name in the
POM was longer than it needed to be.

The README now says the module follows Vertical Slice Architecture, which was
visible in the layout but never stated.
The guide used one word for two things. "rental and payment are two contexts"
meant bounded contexts; "works across one context only" meant event store
contexts. Both appeared on the same page, and the second is the one that decides
which approaches are open to a reader, so the ambiguity landed exactly where it
could not afford to.

The bare word now always means the event store context: Axon Server's named,
logical partition of events, within which ordering is preserved and between
which it does not exist. Bounded contexts are named in full or called sides.

Stating that lets the guide state the constraint behind the "works across"
column, which it previously only implied. An event-sourced entity is sourced
from one context, EventCriteria selects within one, and none spans two, so an
entity assembled from two contexts cannot be injected at all. Deriving state
from context events therefore needs both sides in the same event store context.
Within one, the number of bounded contexts the criteria reaches across does not
matter, which is why the rental and payment example works despite the two
knowing nothing about each other.

That also sharpens the reason to pick the other two approaches. A repository
never sources an entity, and the process-events approach sources only what it
wrote itself, so neither is bound by the rule. Splitting a process across two
Axon Server contexts now appears alongside "the other side is a third-party
provider" wherever that case is discussed, because they are the same case.
The page began with "some business processes are longer than a single
decision", which is true of a two-step wizard. It took another paragraph to
reach the properties that actually matter, and never said outright that the
thing cannot be a transaction.

The first sentence now carries all three: a saga spans several components and
often several systems, it runs for as long as the business takes, and it
therefore cannot be a transaction. Nothing can hold a lock across that, so
nothing can roll it back.

Trip booking earns its place as the second paragraph, because it is the example
everyone already has, and because it makes compensation concrete: the hotel
refuses an hour after the flight is booked, and undoing the flight is another
step of the process rather than the absence of one. Compensation is now
introduced here rather than first appearing halfway through the automation
slices page.

The smaller examples stay, moved after the big one and reworded so none of them
is another booking.
…t models

The concept the framework leans on here is Vertical Slice Architecture, so
the recipe and its guide page now lead with that name rather than "automation
slices", which is the Event Modelling term for the same shape. Both the guide
and the sample keep a pointer back to automation slices so readers who model
that way still recognise it.

The rename runs end to end: the saga-recipes package and its paired
saga.recipe selector value become verticalslices, the docs sample and its
include paths follow, and the guide page, navigation, cross-references and the
saga migration path are updated to match.

The saga guide index also gains a "Saga or process manager" section drawing
the classical distinction (linear event-to-command matching versus stateful
branching) and tying it to the state question the guide answers. Two prooph
board event models are added: the whole bike rental payment process on the
index page, and the payment deadline projection-and-sweep on the deadlines
page.
Sequencing applies to every state approach, so it reads better as a page
of its own than as a section buried in the overview. Move the full
treatment (single-property policy, the two-bounded-context QualifiedName
routing case, and the two-mistakes warning) into sequencing.adoc, link to
the reference guide's canonical SequencingPolicy docs rather than
duplicating them, and leave a short pointer plus the retained anchor in
the overview. Repoint the repository page and cross-link vertical slices.

Also refine the overview opening so it no longer reads as a paradox,
record dispatching a command as the common start/finish marker, name
state-from-context-events as the single approach the one-context
constraint rules out, link the tracking-token reference, and make both
event-model images open full size on click.
The overview was doing five jobs at once: concepts, the decision tree and
comparison tables, the worked example, sequencing, and testing. Break it up so
the front door carries only the concepts, and give the reusable material its own
pages.

New pages:
- Bike rental event model: the domain, the event model, and the shared events.
  It pins the behaviour every approach must produce with a link to the shared
  contract test in examples/saga-recipes rather than reproducing it.
- Choosing an approach: the decision tree and the two comparison tables.
- Testing: the AxonTestFixture how-to plus a pointer to the shared contract test.

Group the three state strategies under a nested "Keeping state" nav node, and
rewire every cross-reference to the moved anchors.

Also fix the deadlines model: drop the paragraph describing a redundant last
automation that re-cancels an already-cancelled payment, and crop that
automation out of the event model image so it ends at RejectRequest.
Apply the Axoniq feature role to the Workflows page and its saga migration section so readers can distinguish the licensed framework capability consistently with the rest of the reference guide.
The guide was written as though every process runs on a streaming event
processor. A subscribing processor is equally valid: it handles the event
inside the publisher's transaction and keeps no token at all, so every
rule phrased in terms of "the tracking token" was stated more broadly
than it holds. State the choice once, briefly, on the overview page and
let the other pages point at it. Where a claim only needed loosening, it
now names the outcome ("the event is not recorded as handled") rather
than the mechanism.

The same sentences over-claimed transactionality twice over. First, they
credited approaches with atomicity between state and progress that only
holds when the two share a transactional resource, which no approach
whose state is events can have, since the event store and the token
store are separate systems. The appending recipe is protected instead by
the append condition covering what its entity sourced, making that
entity the consistency boundary.

Second, and more fundamentally, atomicity was presented as a rule when
idempotent commands already make non-atomic commits survivable: an event
delivered again re-dispatches and the receiver absorbs it. What is
actually unsurvivable is state that outlives a failed command, because
the process then skips work it never did. Rule two now says that, which
is what the example module's package documentation already said.
Both were flagged in review, and neither carries content of its own.

Sequencing does not apply to a subscribing processor, which handles the
event on the publishing thread and has nothing to sequence, so a page
presenting it as a general concern of processes was wrong to begin with.
What it explained was already documented twice over: the mechanics in
the reference guide's sequential processing section, and the two traps
specific to spanning two correlation names, in more detail, on the
example module's RentalPaymentSequencingPolicy. Its one code sample was
the same snippet the vertical slices page includes. The two call sites
now state the problem in a sentence and link to both.

The write-ordering section argued at length that ordering carries no
correctness weight, which the rule it belongs to now says in a clause.
Its only unique content was the case of a process that cannot know what
to store until the command answers; that survives as the note about
registering the write on the processing context.

Also make the remaining pages independent of the processor choice.
Stating a fact "on a streaming processor" is still an assumption when
the reader may have picked the other one, so the record of what has been
handled is now described by what it does rather than by what it is
called, and the two places where the distinction genuinely matters say
so outright.
The sweep caveat told readers to add a lock or a leader election. That
is the wrong remedy: firing on every instance follows from Spring's
@scheduled, not from the projection-and-sweep pattern, and the
schedulers Axon already depends on (Quartz on a clustered job store,
JobRunr, db-scheduler) coordinate across instances themselves.

Also drop "duplicated across instances" from both closing summaries,
since it describes the same @scheduled property rather than a property
of the pattern.
Remove five Javadoc paragraphs that explained the author's reasoning
rather than the code: why an injected entity is nullable, what the bike
rental sample called a rental reference, why a record-only command
exists, why EventAppender is a supported handler parameter, and how the
recipe's sequencing differs from the one-class recipes. Where a claim in
one of them was a design fact rather than commentary, it survives as a
clause on the class summary.

Make the two JPA entities package-private. Both repositories are already
package-private and every reference is in the same package, so nothing
outside needed them.

Link the slice packages in the vertical slices documentation instead of
naming them in {@code}, so the references navigate.
The testing and deadlines pages carried their Java inline, so nothing
checked it. Compiling the three snippets shows why that mattered: they
referenced a payloadsOf helper, a PaymentId type and a BikeRegistered
event that do not exist in the guide's example sources. They are now
real code in sagas/testing and sagas/deadlines, included by tag like
every other sample.

This needs the saga-guide examples as test sources rather than main
ones, since AxonTestFixture, JUnit, AssertJ and Awaitility are all
test-scoped. Test scope is a superset of compile scope, so the rest of
the module still compiles, which is the same reasoning already applied
to the migration examples.

Also split VerticalSlices into one file per slice. The slices were
public static nested classes purely so two tagged snippets could share a
file, and an example should not suggest writing an event handling
component that way. One class per slice also matches the layout the
saga-recipes module uses.
State what sweeping once requires instead of recommending named
schedulers.
@MateuszNaKodach
MateuszNaKodach force-pushed the saga-recipes-module-setup branch from 10e10d7 to beccb88 Compare September 1, 2026 21:54
The migration path introduced its list of alternatives with a link to
index.adoc#long_running_processes, which is the overview page's own top
anchor rather than the comparison. The tables that actually compare the
approaches are on choosing-an-approach.adoc.

Pin every reference-guide to saga-guide xref to {page-component-version}
while here. Unversioned cross-component xrefs resolve against the target
component's latest version, which is what broke the docs build in the
other direction. Both components derive their version from the branch
identically, so the attribute resolves to the same version in any build.
Turn the three conditions for sourcing context events into a list, and
drop the paragraph after it: the note higher up the page already states
that the one-context rule is structural and that process events are the
alternative when it does not hold.

Merge the process's own events and its decision model into one section.
Neither carried enough on its own to earn a heading.

Remove the testing page's note about filtering assertions and keeping
correlation values unique. Both describe constraints of the example
module's test harness, one shared Spring context and one shared event
store, rather than anything about testing a process, and both are
already documented where they apply: the first in SagaRecipeAssertions'
class documentation, the second enforced in SagaRecipeContractTest,
which generates fresh identifiers per test.
Drop the framing paragraph and name the section for what it describes.
MateuszNaKodach and others added 4 commits September 2, 2026 00:35
The module pinned axoniq-framework.version to 5.4.0-SNAPSHOT, which is
not published. Maven could read the BOM from a local cache but never its
parent, so scanning the reactor failed with a non-resolvable parent POM
and two dependencies left without a version. That broke the build before
a single module compiled, on every public runner.

Inherit the 5.3.1 the examples parent pins, as every other example does.
Nothing is lost: the module already imports axon-framework-bom at
${project.version} ahead of the inherited Axoniq BOM, so every
org.axonframework artifact still resolves to the build from this branch,
which is what the SNAPSHOT pin was reaching for.
The documentation build runs Vale over the generated HTML and stops the
Antora run when Vale exits non-zero. Two headings on this page tripped
AxonIQ.Headings, which is an error-level rule, so the site build failed
on every push. The extension reports this through the exit code alone,
which is why the job logged nothing.

"Replacing sagas with Workflows" and "Migrating deadlines for sagas"
both fail the sentence-capitalization check. Reword them to forms the
rule accepts, keeping the lowercase "saga" the other headings on this
page already use. The section anchors are unchanged, so the cross
references in the mapping table still resolve.
The documentation build dies before its extensions run, printing
nothing, where a passing build logs the search-index and Vale steps
within seconds. That places the failure in navigation and site
generation rather than in page conversion, which succeeds locally.

The only structural change to docs since the last green documentation
build is this nav file, and the construct it gained is a plain-text
"Keeping state" grouping entry. No other nav file in the repository has
an entry that is not an xref, so the site UI is never asked to render
one anywhere else.

List the three state pages directly, as every other guide does.

@smcvb smcvb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bunch of minor comments left. Nothing you cannot tackle without another look from me. Great work, @MateuszNaKodach! And thanks for the tone-of-voice addition, @hatzlj!

The cost is a second dispatch per step, and a command that exists only to record a fact: it adds modelling discipline
without adding behaviour.

=== By appending directly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit lost on this pointer from the LLM...I think it's BS. But, you have more context here, so I'll let you be the final judge on this one, @MateuszNaKodach:

Prior Review -- [MID] -- This recipe appends via EventAppender inside a .thenRun() callback on the command's future, which is exactly what the guide's own stated rule says not to do ("Keep the write inside the handler... and out of a callback on the command's future", see index.adoc). It's correct because EventAppender binds to ProcessingContext, not a thread, but the guide never reconciles the exception with its own rule -- still unresolved from two rounds ago.


| `saga.recipe` | Where the process remembers | Package |
|-----------------------|------------------------------------------------------|-------------------------|
| `repository` | a JPA row, committed with the tracking token | `saga/repository` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still mention tracking token details here. Think we need to resolve these as well.

*/
public class RentalPaymentApi {

// tag::tags[]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not wrong necessarily, but there are several tag regions in this file that are never used.

}
// end::process-events[]

// tag::record-command[]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same unused-tag-region pointer. They're not used, so we can drop them, but they don't hurt if you want to safe yourself some effort.

Comment thread docs/reference-guide/modules/migration/pages/paths/sagas.adoc
= Long-Running Processes
:navtitle: Long-running processes
:reftext: Long-running processes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it would be nice if there's a quick link to choosing-an-approach from this first introduction, for people that do not care about the entire story that's drafted here. That's what the context tables on all other index.adoc pages is for; to not force people to go through everything if they don't need it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 1: Must Highest priority. A release cannot be made if this issue isn’t resolved. Type: Documentation Use to signal issues that describe documentation work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add migration path for Saga alternatives to the reference guide

3 participants