docs(examples): add outbox, the transactional-outbox recipe - #66
Merged
Conversation
laconc
force-pushed
the
docs/outbox-example
branch
2 times, most recently
from
August 29, 2026 16:00
9f7296d to
f69d58c
Compare
The seam a journal exposes for the caller's transaction — `Journal::Tx`, `execute_in`, `Pending` — is the reason the trait was reshaped, and it had no runnable example. An adopter could only learn it from `ironstate-journal/tests/transactional.rs`, which is not indexed as an example and is not where anyone looks. Every other tier has one. An order ships, and three things must become true together: the `Shipped` event is in the log, the read model says `shipped`, and a confirmation job is on the queue. If the append commits and the job does not, the customer is never told and nothing knows — which is not a failure you can retry your way out of afterwards, because the fact that it needed doing is gone. That is the motivation for one transaction rather than a queue publish, stated in the domain rather than in the abstract. `Store` is a few dozen lines of in-memory tables standing in for SQLite or Postgres; `Transaction` is the caller's type, and the journal writes into it rather than around it. Four tests pin the behaviour: a rolled-back transaction leaves no event, no projection, no queued job **and** an un-evolved aggregate; a commit lands all four; a retry after a rollback behaves like a first attempt (no sequence consumed, the job queued exactly once); and staged writes are invisible to the journal's own reads until commit — which is why `execute_in` is one call per transaction. The README covers the two things an adopter gets wrong: why `execute_in` does not evolve the aggregate, and the one-call-per-transaction constraint. Indexed in all three places AGENTS.md requires — the examples table, the root README, and docs/testing.md. It stays out of the guide, like the other adoption recipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
laconc
force-pushed
the
docs/outbox-example
branch
from
August 29, 2026 16:05
f69d58c to
34b0b5f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #62 (it uses
VersionedEvent::new, which that PR adds).The seam a journal exposes for the caller's transaction —
Journal::Tx,execute_in,Pending— is the reason the trait was reshaped, and it had no runnable example. An adopter could only learn it fromironstate-journal/tests/transactional.rs, which isn't indexed as an example and isn't where anyone looks. Every other tier has one:ledger,async-store,catalog-ctx.The domain does the arguing
An order ships. Three things must become true at once:
Shippedevent is in the log,shipped,If the append commits and the job doesn't, the customer is never told their order shipped, and nothing in the system knows. That isn't a failure you can retry your way out of afterwards — the information that it needed doing is gone. Which is why it's one transaction and not a queue publish.
Stating it in a domain rather than abstractly matters here: "atomicity" as a word convinces nobody to restructure their write path.
What it shows
Storeis a few dozen lines of in-memory tables standing in for SQLite or Postgres. The point isn't the storage — it's thatTransactionis the caller's type, the journal writes into it rather than around it, and nothing is visible untilcommit. Dropping it is the rollback, and needs no code.Four tests, not one
execute_inis one call per transaction.The README covers the two traps
Why
execute_indeliberately does not evolve the aggregate (a rollback would otherwise leave it silently ahead of the durable log — the failure the seam exists to prevent, reintroduced one layer up), and the one-call-per-transaction constraint, sincehead/entropy_postake notxand see committed state only.Indexing
All three places AGENTS.md requires: the examples table, the root README, and
docs/testing.md. It stays out of the guide, like the other adoption recipes — the guide's arc is core → aggregate → journal, and this is a recipe for an integration rather than a step in learning the family.Verification
cargo run -p outboxprints the store before, after a rollback, and after a commit.make check,make msrv,make doc,make wasm,make denyall pass.🤖 Generated with Claude Code