Skip to content

adr: jobs replace services#8

Draft
0xgleb wants to merge 1 commit into
masterfrom
adr/associated-job
Draft

adr: jobs replace services#8
0xgleb wants to merge 1 commit into
masterfrom
adr/associated-job

Conversation

@0xgleb

@0xgleb 0xgleb commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Motivation

EventSourced handlers compute state transitions and run side effects through
type Services, all before cqrs-es persists the returned events. A crash between
a side effect and the event write leaves an action in the outside world with no
event to record it — nothing to suppress a retry on restart, nothing to react
to. Handlers need to become pure (state, command) -> Vec<Event> and side
effects need to move onto durable, crash-safe machinery. That reshape touches
every impl EventSourced, so it needs a recorded decision before any code
changes.

Solution

ADR-0001 records the decision to replace type Services with
type Jobs: JobList and move all side effects into durable, retryable
apalis-backed jobs whose enqueue commits in the same SQLite transaction as the
events that trigger them.

  • Lift the battle-tested job machinery from the st0x.liquidity consumer
    (conductor::job) into event-sorcery, generalizing the consumer-specific bits.
  • Job carries its dependency bundle as an associated Input type (vs the
    reference's Job<Ctx> generic), so a job impl is self-describing.
  • type Jobs is a type-level JobList (Cons/Nil), so one aggregate can
    dispatch many job types — each with its own worker and retry policy — with
    compile-time membership via HasJob<J>.
  • Handlers become sync and pure; reads they did via services (clock, config,
    idempotency keys) move into the Command.
  • Atomic enqueue: the handler buffers pushes onto a JobQueue; the framework
    drains the buffer inside the event-commit transaction.
  • Jobs are delivered at-least-once, so perform must be idempotent.
  • Records consequences, alternatives considered, and the SPEC/docs follow-ups.

Design doc only; #11-#16 implement it.

Summary by CodeRabbit

  • Documentation
    • Added architecture decision record documenting a comprehensive event sourcing framework redesign. Proposes transitioning to synchronous pure handlers with type-safe, job-based side effects and atomic persistence semantics for improved reliability and consistency.

Closes RAI-913.

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ee6b365-f67e-4ac4-9cbe-2e5d38e7af5d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR adds ADR-0001 documenting a framework redesign that replaces EventSourced::Services with job-based side effects. Handlers become synchronous pure functions enqueuing retryable jobs through a buffered JobQueue. A lifted Job trait provides self-describing Input/Output/Error types with worker naming metadata. Type-level job registration via JobList/HasJob enforces compile-time job membership. Event-sorcery machinery includes handler-facing JobQueue and worker-side JobBackend storage with atomic enqueue-plus-commit semantics in SQLite transactions, eliminating crash windows.

Changes

ADR-0001: Services to Job-Based Side Effects Redesign

Layer / File(s) Summary
Problem Statement and Motivation
adrs/0001-jobs-replace-services.md
Explains the crash-safety gap where side effects can execute without persisted events, motivating a shift to atomic job enqueueing within the event commit transaction.
EventSourced Trait Redesign
adrs/0001-jobs-replace-services.md
Removes type Services, adds type Jobs: JobList, changes initialize/transition to synchronous pure functions accepting &mut JobQueue<Self::Jobs> and returning Vec<Event>.
Job Trait and Type-Level Registration
adrs/0001-jobs-replace-services.md
Lifts Job trait with Input/Output/Error types, WORKER_NAME and KIND metadata, label() and perform() returning Send futures. Introduces JobList/HasJob marker traits to enforce compile-time job membership in JobQueue::push.
Framework Integration: JobQueue and JobBackend with Atomic Commit
adrs/0001-jobs-replace-services.md
Splits handler-facing buffered JobQueue from worker-side JobBackend storage. Specifies atomic semantics: job flush occurs within the same SQLite transaction as event persistence, eliminating crash windows. Documents generalized worker wiring and failure injection keyed by KIND.
Worker Wiring and Integration
adrs/0001-jobs-replace-services.md
Describes consumer worker wiring via build_supervised_worker!, specifying that only Arc<MyJob::Input> is provided as context and re-export responsibilities for apalis types.
Breaking Changes and Delivery Contract
adrs/0001-jobs-replace-services.md
Enumerates breaking changes for all EventSourced impls: removal of services, addition of job queue plumbing, handler signature changes, side effect migration to job variants, and test/wiring helper updates. Records at-least-once delivery contract requiring idempotent Job::perform.
Documentation Updates, Alternatives Considered, and Out-of-Scope
adrs/0001-jobs-replace-services.md
Details required spec/docs updates replacing services patterns with job dispatch semantics and buffered constraints. Lists rejected alternatives (single job type, marker trait, reactor-based enqueue, services alongside jobs). Declares out-of-scope items (input construction, schema migrations, multi-queue prioritization).
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'adr: jobs replace services' directly and concisely summarizes the main change: an Architecture Decision Record documenting the replacement of the Services-based approach with a Jobs-based approach for handling side effects.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch adr/associated-job

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.

0xgleb commented May 14, 2026

Copy link
Copy Markdown
Collaborator Author

How to use the Graphite Merge Queue

Add the label add-to-gt-merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has required the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@0xgleb 0xgleb mentioned this pull request May 14, 2026
@0xgleb 0xgleb force-pushed the adr/associated-job branch 4 times, most recently from 6be35b6 to a7950b2 Compare May 14, 2026 09:40
Base automatically changed from docs/examples to feat/deferred-types May 14, 2026 15:38
Base automatically changed from feat/deferred-types to feat/ci May 14, 2026 15:39
Base automatically changed from feat/ci to feat/docs May 14, 2026 15:39
Base automatically changed from feat/docs to feat/copy-crates May 14, 2026 15:40
Base automatically changed from feat/copy-crates to feat/nix-flake May 14, 2026 15:41
Base automatically changed from feat/nix-flake to feat/license May 14, 2026 15:41
Base automatically changed from feat/license to master May 14, 2026 16:05
@0xgleb 0xgleb force-pushed the adr/associated-job branch from a7950b2 to c581197 Compare June 5, 2026 00:32
@0xgleb 0xgleb changed the base branch from master to graphite-base/8 June 5, 2026 00:45
@0xgleb 0xgleb force-pushed the adr/associated-job branch from c581197 to b4abd04 Compare June 5, 2026 00:45
@0xgleb 0xgleb changed the base branch from graphite-base/8 to chore/bump-up-deps June 5, 2026 00:45
@0xgleb 0xgleb force-pushed the adr/associated-job branch from b4abd04 to be4025d Compare June 5, 2026 00:45
@0xgleb 0xgleb force-pushed the chore/bump-up-deps branch from f418054 to cd43e9b Compare June 5, 2026 00:45
@0xgleb 0xgleb force-pushed the adr/associated-job branch from be4025d to a000b43 Compare June 5, 2026 00:46
@0xgleb 0xgleb force-pushed the chore/bump-up-deps branch from cd43e9b to 0dd1f45 Compare June 5, 2026 00:46
@0xgleb 0xgleb force-pushed the adr/associated-job branch from a000b43 to fcfdad7 Compare June 5, 2026 00:47
@0xgleb 0xgleb force-pushed the adr/associated-job branch from 4e33364 to d64da60 Compare June 8, 2026 23:41
@0xgleb 0xgleb requested review from JuaniRios and findolor June 8, 2026 23:44
@0xgleb 0xgleb added the documentation Improvements or additions to documentation label Jun 8, 2026 — with Graphite App
@0xgleb 0xgleb force-pushed the chore/bump-up-cqrs-es branch from 7217caf to efcba38 Compare June 9, 2026 20:39
@0xgleb 0xgleb force-pushed the adr/associated-job branch from d64da60 to a8e03bb Compare June 9, 2026 20:39
@0xgleb 0xgleb mentioned this pull request Jun 9, 2026
@0xgleb 0xgleb changed the base branch from chore/bump-up-cqrs-es to graphite-base/8 June 9, 2026 21:21
@0xgleb 0xgleb force-pushed the graphite-base/8 branch from efcba38 to 5b1ff80 Compare June 9, 2026 21:22
@0xgleb 0xgleb force-pushed the adr/associated-job branch from a8e03bb to 2213a7d Compare June 9, 2026 21:22
@0xgleb 0xgleb changed the base branch from graphite-base/8 to chore/pr-template June 9, 2026 21:22
@0xgleb 0xgleb force-pushed the chore/pr-template branch from 5b1ff80 to 86e3950 Compare June 9, 2026 23:24

@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.

Actionable comments posted: 9

🤖 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.

Inline comments:
In `@adrs/0001-jobs-replace-services.md`:
- Around line 79-80: The ADR currently buries a major migration detail: handlers
no longer read runtime context from Services (clock, config, idempotency keys)
and that responsibility moves to Command construction; update the Consequences →
Negative section with a dedicated bullet that explicitly states callers must
gather and embed runtime context when creating Command instances (e.g., include
Instant::now(), config values, and external idempotency keys) and give a short
example sentence describing the caller-side burden (commands must be constructed
with these values before dispatch rather than handlers reading them on-demand);
reference the terms Command, Handlers, and Services in the new bullet so readers
can locate the change in the ADR.
- Around line 27-44: Condense the "Context" inventory by keeping only the
high-level components (Job<Ctx> trait, JobQueue<Task> newtype,
build_supervised_worker! macro, work::<Ctx, J> apalis handler, FailureInjector,
Label) and move the detailed configuration points (poll-interval tuning,
FAIL_STOP_RECOVERY_TIMEOUT, retry/circuit-breaker policy, terminal-failure
notifier, test vs production handler variants, SLO reasoning) into the
"Machinery moved into event-sorcery" / Decision section where implementation
decisions are justified; update the ADR text to remove those specifics from the
Context list and add them as supporting details in the Decision subsection
referencing the same symbols.
- Around line 256-263: Document the task-local buffer's assumptions and
limitations: in the ADR text near the paragraph describing "per-command
task-local buffer" and the flow through Store::send, with_pending_jobs, the
Lifecycle handler and JobQueue draining into the event repository, add a concise
note that this design assumes handlers execute single-threaded per command (no
spawned child tasks), Store::send is not called re-entrantly, and multiple
commands are not processed concurrently on the same task; also state how these
edge cases are prevented or should be handled (e.g., prohibit spawning async
tasks that rely on the buffer, detect/deny re-entrant Store::send, or switch to
an explicit Transaction-bound buffer if needed).
- Around line 228-229: Clarify whether the FailureInjector design choice is
internal or API-affecting: explicitly state if FailureInjector<KIND: &'static
str> will expose consumer-facing registration differences (e.g., requiring
consumers to register by KIND when using a HashMap of mutexes) or if those
details are hidden behind the same public API regardless of the internal
approach; if undecided, add a single sentence deferring the exact implementation
(HashMap of one Mutex per KIND vs. small typestate) to the implementation PR and
confirm that the public contract for registering failure points will remain
unchanged.
- Line 245: Clarify and implement the intended transaction contract by
refactoring persist_events so it no longer opens its own transaction (remove the
self.pool.begin() usage) and instead accepts a &mut Transaction parameter that
callers must pass through; update all call sites that previously relied on
persist_events to thread the existing tx (the same Transaction used for other
CQRS/ES writes) into persist_events, and document this contract change in the
ADR and the event repository interfaces so implementations know they must use
the provided &mut Transaction rather than starting their own or relying on
task-local tx injection.
- Around line 152-154: Clarify how consumers invoke register_jobs!: add a short
usage example in the ADR showing jobs![A, B] -> Cons<A, Cons<B, Nil>> and then
demonstrate whether register_jobs!(Jobs) is called per-aggregate (e.g., inside
the aggregate module where type Jobs = jobs![SendEmail, ChargeCard];
register_jobs!(Jobs);) or globally, and state if it is internal machinery
consumers never touch; reference the macro names register_jobs!, jobs!, and the
trait HasJob (and types Cons/Nil) so readers can locate the implementation and
understand the intended invocation pattern.
- Around line 267-272: Update the ADR to document the six parameters of the
build_supervised_worker! macro used in Monitor::register: explain that index is
the worker numeric index used for naming/metrics, queue is the
JobBackend::<MyJob> instance, input is Arc<MyJob::Input> (already noted),
fail_stop is the Duration used by the circuit-breaker for recovery timeout, and
failure_notify is the callback invoked on terminal failure; add this short
parameter list near the macro example and add a pointer to the macro’s full docs
(or file) for more detail.
- Around line 30-32: The parenthetical in the sentence referencing Job<Ctx>
interrupts flow by previewing a later design decision; edit the sentence that
mentions "`trait Job<Ctx>` with `perform(&self, ctx: &Ctx)`, `WORKER_NAME`,
`label()`, `Output`, `Error`" to remove the parenthetical entirely or replace it
with a brief neutral note such as "using `Job<Ctx>` as written" so the ADR does
not forward-reference the later change to an associated `Input` type; update the
line containing `Job<Ctx>` accordingly.
- Around line 17-21: Rewrite the paragraph in adrs/0001-jobs-replace-services.md
to lead with the concrete crash failure mode: state transitions and side effects
are both computed in the handler that returns Vec<Event>, so if the process
crashes after performing an external side effect but before cqrs-es persists
those returned events, the external action occurred with no event recorded and
restart cannot suppress or compensate; then follow with the explanation that
this tight coupling creates a crash window and motivates separating side effects
from pure event generation. Keep references to the handler, Vec<Event>, and
cqrs-es persistence to make the causality explicit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f0593f5e-7fa4-4f80-a7e5-5150ffefa3a1

📥 Commits

Reviewing files that changed from the base of the PR and between 86e3950 and 1a39eb7.

📒 Files selected for processing (1)
  • adrs/0001-jobs-replace-services.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.md

⚙️ CodeRabbit configuration file

Focus on the contents of the docs and not on cosmetic things like markdown formatting. We use markdown files for various docs including but not limited to guidelines for AI contributors (AGENTS.md), project overview and instructions for human contributors (README.md), and topic-focused references under docs/ (cqrs.md, sqlx.md, ttdd.md). Think about the target audience of a document when deciding what comment to leave. For instructions, suggest better rules and guidelines and point out missing instructions. For topic references, suggest improvements that would make non-obvious framework behavior or pitfalls easier to discover. In all cases, flag needless bloat, prefer clear concise writing, and consider the structure of the document and order of the sections

Files:

  • adrs/0001-jobs-replace-services.md
🔇 Additional comments (5)
adrs/0001-jobs-replace-services.md (5)

3-5: LGTM!


98-136: LGTM!


277-318: LGTM!


320-335: LGTM!


337-342: LGTM!

Comment thread adrs/0001-jobs-replace-services.md
Comment thread adrs/0001-jobs-replace-services.md
Comment thread adrs/0001-jobs-replace-services.md
Comment thread adrs/0001-jobs-replace-services.md
Comment thread adrs/0001-jobs-replace-services.md
Comment thread adrs/0001-jobs-replace-services.md Outdated
Comment thread adrs/0001-jobs-replace-services.md Outdated
Comment thread adrs/0001-jobs-replace-services.md Outdated
Comment thread adrs/0001-jobs-replace-services.md Outdated
@linear-code

linear-code Bot commented Jun 10, 2026

Copy link
Copy Markdown

RAI-913

@findolor findolor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved.

@graphite-app

graphite-app Bot commented Jun 10, 2026

Copy link
Copy Markdown

Merge activity

  • Jun 10, 7:44 PM UTC: Graphite rebased this pull request, because this pull request is set to merge when ready.

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

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants