Skip to content

feat(router): support causation_id: nil to mark events as chain roots#106

Merged
yordis merged 3 commits into
straw-hat-team:mainfrom
apre:allow-nullify-causation
May 21, 2026
Merged

feat(router): support causation_id: nil to mark events as chain roots#106
yordis merged 3 commits into
straw-hat-team:mainfrom
apre:allow-nullify-causation

Conversation

@apre

@apre apre commented May 15, 2026

Copy link
Copy Markdown
Contributor

Callers can supply a :causation_id and have it persisted on resulting events — but there was still no way to express "this event has no cause." Keyword.get(opts, :causation_id) returns nil whether the option is absent or explicitly nil, so the dispatcher fell back to command_uuid in both cases.

Resolve :causation_id at the router boundary using Keyword.fetch/2 so absence and explicit-nil can be distinguished:

Side benefit: the dispatcher's to_execution_context/2 no longer carries the fallback || and reads pipeline.causation_id directly. Middleware that inspects pipeline.causation_id continues to see a uuid-or-nil, never a sentinel.

Backwards compatible:

WHERE causation_id IS NULL now identifies chain roots without a self-join against the events table — significantly cheaper on large event stores.

  • lib/commanded/commands/router.ex: resolve :causation_id with Keyword.fetch/2; document the new option behaviour on dispatch/2
  • lib/commanded/commands/dispatcher.ex: drop the || command_uuid fallback; stop destructuring command_uuid (now unused here)
  • test/commands/correlation_causation_test.exs: assert causation_id: nil produces nil on the resulting event

Callers can supply a `:causation_id` and have it persisted on
resulting events — but there was still no way to express "this event has
no cause." `Keyword.get(opts, :causation_id)` returns `nil` whether the
option is absent or explicitly `nil`, so the dispatcher fell back to
`command_uuid` in both cases.

Resolve `:causation_id` at the router boundary using `Keyword.fetch/2`
so absence and explicit-`nil` can be distinguished:

  - opt absent              -> fall back to `command_uuid` (legacy)
  - `causation_id: <uuid>`  -> use the uuid (since straw-hat-team#103)
  - `causation_id: nil`     -> use `nil` (new)

Side benefit: the dispatcher's `to_execution_context/2` no longer
carries the fallback `||` and reads `pipeline.causation_id` directly.
Middleware that inspects `pipeline.causation_id` continues to see a
uuid-or-nil, never a sentinel.

Backwards compatible:
  - callers that never pass `:causation_id` get identical behaviour
  - callers that pass a uuid get the same behaviour as straw-hat-team#103's fix
  - the only new behaviour is opt-in via `causation_id: nil`

`WHERE causation_id IS NULL` now identifies chain roots without a
self-join against the events table — significantly cheaper on large
event stores.

  - lib/commanded/commands/router.ex: resolve `:causation_id` with
    `Keyword.fetch/2`; document the new option behaviour on `dispatch/2`
  - lib/commanded/commands/dispatcher.ex: drop the `|| command_uuid`
    fallback; stop destructuring `command_uuid` (now unused here)
  - test/commands/correlation_causation_test.exs: assert
    `causation_id: nil` produces `nil` on the resulting event
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@apre has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 30 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 28ef511b-c83c-4f9a-96c9-efba10f2da7c

📥 Commits

Reviewing files that changed from the base of the PR and between 818655b and 739199e.

📒 Files selected for processing (1)
  • lib/commanded/commands/router.ex

Walkthrough

Router dispatch now resolves causation_id (absent → command_uuid, provided → used, explicit nil → preserved) and passes it into the dispatcher. The dispatcher stops falling back to command_uuid and uses payload.causation_id directly. A test verifies events persisted with explicit nil causation_id.

Changes

Causation ID resolution at router boundary

Layer / File(s) Summary
Router boundary documentation and resolution logic
lib/commanded/commands/router.ex
dispatch/2 docs specify three causation_id behaviors. do_dispatch/2 resolves causation_id with Keyword.fetch(opts, :causation_id), falling back to command_uuid only when the option is absent and preserving explicit nil into the dispatcher payload.
Dispatcher adaptation to pre-resolved causation_id
lib/commanded/commands/dispatcher.ex
to_execution_context/2 no longer destructures command_uuid and assigns ExecutionContext.causation_id directly from payload.causation_id, removing the previous `causation_id
Test verification for explicit nil causation_id
test/commands/correlation_causation_test.exs
New test dispatches OpenAccount with causation_id: nil and asserts the persisted event has event.causation_id == nil, verifying chain-root event persistence.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • straw-hat-team/commanded#103: Related changes around honoring caller-supplied :causation_id and persisted causation_id handling in the dispatch flow.

Poem

🐰 I hop where routers draw the line,

nil roots planted, no fallback vine,
Dispatcher rests with causation clear,
Events remember what callers steer.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding support for explicitly passing causation_id: nil to mark events as chain roots.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation for distinguishing between absent and explicitly-nil causation_id options, the implementation approach, backward compatibility, and benefits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@yordis

yordis commented May 21, 2026

Copy link
Copy Markdown
Member

@apre I just realized, HOW DID I MISS THIS PR! I am so sorry! Your changes are already in, thank you so much regardless, my apologies for missing this one.

Side note, are you using the fork? 👀

@yordis yordis closed this May 21, 2026
@apre

apre commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Side note, are you using the fork? 👀

Hi @yordis !
Don’t worry, I am patient :-)

I still wonder which version I should use (between this fork and the "original".
Is the fork battle tested too ? 

It’s a hard choice and I still struggle on it

@yordis yordis reopened this May 21, 2026
@yordis

yordis commented May 21, 2026

Copy link
Copy Markdown
Member

https://github.com/straw-hat-team/commanded/blob/main/guides/explanations/fork-differences.md

I try to document most of the changes there, definitely they are improvements and major breaking changes since I removed some components; clean up tests, make some stuff faster, and added features I tried to contribute upstream until, I got nowhere.

@yordis

yordis commented May 21, 2026

Copy link
Copy Markdown
Member

Can you address CI failure 🙏🏻 ?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
lib/commanded/commands/router.ex (1)

207-222: 💤 Low value

Consider documenting the causation_id: nil case in this section.

This new documentation section covers the causation_id: <uuid> and omitted cases, but doesn't mention that passing causation_id: nil explicitly marks events as chain roots. While the dispatch/2 options documentation (lines 467-473) does cover this, users reading only this section might miss the chain-root feature that is the primary addition in this PR.

📝 Suggested addition
 When `:causation_id` is not provided, the command's own `command_uuid` is
 used as the resulting events' `causation_id`.
+
+Pass `causation_id: nil` explicitly to mark the resulting events as chain
+roots — they will be persisted with `causation_id` set to `NULL`.

 Process managers and event handlers that dispatch follow-up commands
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/commanded/commands/router.ex` around lines 207 - 222, Update the
"Causation id" section to explicitly document the special-case when callers pass
causation_id: nil: state that setting causation_id: nil on BankApp.dispatch/2
(or the referenced dispatch/2 options) explicitly marks the resulting events as
chain roots (i.e., no causation_id will be propagated), include a short example
showing :ok = BankApp.dispatch(command, causation_id: nil), and add a brief
cross-reference to the existing dispatch/2 options paragraph so readers find the
more detailed behavior described there.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/commanded/commands/router.ex`:
- Around line 207-222: Update the "Causation id" section to explicitly document
the special-case when callers pass causation_id: nil: state that setting
causation_id: nil on BankApp.dispatch/2 (or the referenced dispatch/2 options)
explicitly marks the resulting events as chain roots (i.e., no causation_id will
be propagated), include a short example showing :ok = BankApp.dispatch(command,
causation_id: nil), and add a brief cross-reference to the existing dispatch/2
options paragraph so readers find the more detailed behavior described there.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6665eed8-778c-4716-be85-35b6906f8e43

📥 Commits

Reviewing files that changed from the base of the PR and between 674d230 and 818655b.

📒 Files selected for processing (1)
  • lib/commanded/commands/router.ex

The added causation_id resolution pushed `__before_compile__`'s quote
block past credo's `LongQuoteBlocks` threshold. Replace the multi-line
explanation with a single-line comment — the case clauses are
self-explanatory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrien Pré <apre@dxo.com>
@yordis

yordis commented May 21, 2026

Copy link
Copy Markdown
Member

DCO 😭 😭 😭 😭 😭 😭 😭 😭 😭 painful

@yordis yordis merged commit 4da2396 into straw-hat-team:main May 21, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants