Sagas
1 · Why a saga rather than a distributed transaction?
1 · Why a saga rather than a distributed transaction?
liability.in_transit) rather than pretending they do not exist.2 · What makes a compensation safe?
2 · What makes a compensation safe?
3 · Why compensate in reverse order if reversals commute?
3 · Why compensate in reverse order if reversals commute?
4 · What happens when a compensation itself fails?
4 · What happens when a compensation itself fails?
compensation_failed and stops, rather than retrying blindly into a partially-unwound state.That is a distinct terminal state from compensated precisely so it can be alerted on: it is the one outcome that needs a human.Follow-up: what should happen next? An operational case with the completed and failed compensation steps attached, so someone can finish the unwind manually with full context. In Arc that case management is Phase 8 and does not exist yet, which is a known gap, not a solved problem.Messaging
5 · What problem does a transactional outbox solve?
5 · What problem does a transactional outbox solve?
6 · Distinguish at-least-once delivery from effectively-once processing.
6 · Distinguish at-least-once delivery from effectively-once processing.
7 · What do you do with a poison event?
7 · What do you do with a poison event?
Idempotency
8 · A payout request times out. Retry or not?
8 · A payout request times out. Retry or not?
9 · Why not deduplicate on amount plus beneficiary?
9 · Why not deduplicate on amount plus beneficiary?
10 · Why is retryability a field on the error rather than a decision at the call site?
10 · Why is retryability a field on the error rather than a decision at the call site?
account_closed just fails again, more slowly, while the customer waits.The alternative, a regex on the error message at the call site, is duplicated logic that drifts, in the code path that runs when things are already going wrong.Follow-up: what if you get it wrong? Marking a rejection retryable wastes time and delays the unwind. Marking a timeout non-retryable strands the transfer. The second is worse, so the default when genuinely unsure should be retryable, which is only safe because the key exists.Boundaries
11 · The saga needs a ledger reservation synchronously. How, without importing the ledger?
11 · The saga needs a ledger reservation synchronously. How, without importing the ledger?
LedgerPort as an interface it owns; the ledger implements it; the adapter lives at the composition root outside every context’s src.Movement imports its own port type, never the ledger. The boundary check stays green while the call stays synchronous.Follow-up: why not use an event? Because the saga needs the answer before the next step runs. A reservation must be accepted or rejected before proceeding, or the saga has no idea what to compensate. Events are the default; ports exist for exactly the cases where the caller cannot wait.12 · Your architecture rule is enforced in CI and green. Is it working?
12 · Your architecture rule is enforced in CI and green. Is it working?
dependency-cruiser rule caught relative imports and silently skipped package-name imports: @arc/ledger resolves through node_modules to a dist path that does not exist until after a build, so the edge was never added to the graph and the rule never evaluated.Not a failure. A rule that did not apply, which is worse, because a rule that errors gets fixed and a rule that quietly does nothing gets trusted.Follow-up: what is the fix? A second layer with a different failure mode: ESLint no-restricted-imports, which matches on the specifier string and resolves nothing, so build order is irrelevant to it. Both probes verified to fire. The scenario.Questions to ask them back
- What is your delivery guarantee, and where is the idempotency enforced?
- What happens to an event whose handler keeps failing?
- Is there a state in your system where money is neither sent nor refunded? What is it called, and can you query how much is in it right now?
- Have you ever deliberately broken your CI rules to check they fire?