Skip to content

Plan transactional outbox/retry strategy for save-publish-dispatch consistency (deferred) #76

@bartul

Description

@bartul

Parent: #66

type = decision
status = approved
implementation status = deferred

Problem

Current save -> publish -> dispatch flow can leave state and integration effects out of sync on partial failure.

The current Rondel consistency boundary is Rondel.materialize, which sequences:

  1. Save state.
  2. Publish domain events.
  3. Dispatch outbound commands to other bounded contexts.

If save succeeds and a later publish/dispatch step fails, state has already moved forward. For paid rondel movement, that can leave a pending movement in Rondel state without a durable Accounting command, or leave Accounting/Rondel reconciliation dependent on non-durable bus delivery.

Current code nuance

The terminal sandbox is intentionally in-memory and fire-and-forget:

  • RondelHost.Execute and AccountingHost.Execute return enqueue acknowledgment only.
  • The terminal bus suppresses subscriber exceptions, so a successful Publish call means the event was offered to current subscribers, not that every subscriber handled it successfully.
  • Accounting is currently stateless and auto-approves charges.

This means terminal behavior is acceptable for the current sandbox, but it is not a durable/web consistency guarantee.

Failure matrix

Step Failure mode Current behavior Risk
Save fails Store returns Error Handler fails before publish/dispatch Clean failure; no later effects should occur
Save succeeds, publish delivery/handling fails Bus subscriber throws or delivery is not durable Terminal bus suppresses subscriber exceptions State/event divergence; publish success does not prove handling
Save succeeds, dispatch fails Dispatch returns Error or target is unavailable Handler fails after state save State saved but outbound command missing
Dispatch enqueue succeeds, Accounting processing fails Accounting mailbox later fails Caller already received enqueue acknowledgment; notification may be published Rondel has pending movement, Accounting may not process charge
Accounting publishes return event, Rondel handling fails Return event delivery/handling is non-durable Event can be missed or swallowed in terminal bus path Accounting has outcome, Rondel remains pending
Relay redelivers a message in future durable flow Relay crashes after delivery before acknowledgment Duplicate delivery is expected under at-least-once delivery Consumers must be idempotent

Decision

Adopt transactional outbox plus idempotent inbox for durable/web deployments.

The durable commit must atomically persist domain state changes and outbound messages. A relay then delivers pending messages with at-least-once semantics. Consumers must be idempotent through an inbox/processed-message mechanism or equivalent business-level idempotency.

This is the selected strategy because:

  • outbox prevents state committed, message lost;
  • inbox/idempotent consumers handle duplicate delivery, which outbox relay cannot avoid;
  • the model aligns with future HTTP 202 Accepted/eventual consistency semantics;
  • it keeps the terminal sandbox simple while preserving a clear path to durable behavior.

Non-decision for current terminal sandbox

Do not implement an in-memory outbox for the current terminal sandbox.

An in-memory outbox would not survive process failure and would not provide the durability guarantee this decision is about. The current supervised mailbox and in-memory store remain acceptable for local sandbox work.

Implementation direction

When implementation starts, prefer an explicit effect commit boundary over transparent middleware capture.

Target shape:

  • introduce RondelEffects as data for state, domain events, and outbound commands;
  • introduce CommitRondelEffects as the boundary where infrastructure chooses direct commit or durable outbox commit;
  • preserve current terminal direct-commit behavior while making the consistency boundary explicit and testable.

Transparent middleware capture may be used as a tactical bridge if needed, but it is not the target architecture. It gives Save, Publish, and Dispatch two meanings depending on context, which is too implicit for the permanent consistency boundary.

Follow-up work

Acceptance criteria

  • Failure matrix documented.
  • Strategy selected for later implementation.
  • Decision explicitly records outbox plus inbox, not outbox alone.
  • Terminal sandbox non-goal is documented.
  • Implementation target is clarified as explicit effect commit boundary.
  • Follow-up implementation issues are linked.

Consequences

Durable/web readiness requires more than replacing the bus with retries. The architecture needs a named commit boundary, durable outbox storage and relay, and idempotent consumers. Implementation remains deferred until the durable persistence/web phase, but future work should align with this decision.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions