# ADR-S3: Event, Audit, Idempotency, and Transaction Boundaries

**Status**: Adopted (agent-recorded design decision) — pending named
architecture ratification **Date**: 2026-07-24 **Authors**: V1 Domain
Workbenches extraction audit (Phase S)

> Records the event/audit/idempotency and failure semantics for the shared
> workbench mutation path (`V1_DOMAIN_WORKBENCHES_TODOS_2026-07-23.md` §S0.8).
> Built on the committed facts from §S0.8.a–c: **42 mutations → 34 local-ACID +
> 8 transactional-outbox, 0 sagas** (`tara-transaction-boundaries.json`) and the
> identity contract (command / event / external-operation / correlation ids).
> The machine-checked model is
> `evidence/v1-workbenches/event-audit-boundaries.json`.

## Context

§S0.8.b chose the **transactional outbox** for every mutation that crosses an
external boundary (8 of 42) and a **local ACID transaction** (persistence +
audit in one commit) for the rest. **No mutation requires a saga** — none
demands that an external effect be atomic with the local write. This ADR fixes
the delivery, audit, and failure semantics that follow.

## S0.8.d — Delivery semantics (the 8 outbox mutations)

| Dimension         | Rule                                                                                                                          |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **ordering**      | per-aggregate (per target id) order via the outbox row sequence; no global cross-aggregate order                              |
| **delivery**      | at-least-once — the dispatcher retries a row until the external effect acks                                                   |
| **duplication**   | consumers are idempotent, deduplicating by `externalOperationId` (the outbox row id, §S0.8.c)                                 |
| **replay**        | safe from the durable outbox log by `externalOperationId`; dedup makes re-dispatch a no-op                                    |
| **compatibility** | events are versioned; additive changes are backward-compatible; a breaking change ships a new version, never a silent reshape |
| **poison**        | after a bounded retry budget a row moves to a dead-letter queue (no infinite retry); it alerts and awaits manual replay       |

## S0.8.e — Audit semantics (all 42 mutations)

| Dimension                   | Rule                                                                                                                                                       |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **atomicity**               | the audit row commits in the **same** local transaction as the persistence write (§S0.8.b local-ACID = persistence + audit); no mutation without its audit |
| **refusal/failure capture** | authorization refusals and validation failures are audited too, not only successes                                                                         |
| **immutability**            | audit rows are append-only; no UPDATE/DELETE on the ledger                                                                                                 |
| **redaction**               | PII/secret fields are redacted at write time; the ledger never stores raw secrets                                                                          |
| **retention**               | governed by a `RetentionPolicy` (per audit class); expiry is a scheduled purge, not an ad-hoc delete                                                       |
| **evidence linkage**        | each audit event links to its target id + command id (§S0.8.c) — the chain actor → command → effect                                                        |

## S0.8.f — Failure scenario diagrams

The seven scenarios and the guarantee each relies on. Notation: `W`=workbench
BFF, `DB`=durable store (persistence + audit + outbox, one tx), `D`=outbox
dispatcher, `X`=external effect (provider/publish/blob/notification).

### 1. Failure before commit — atomic rollback

```mermaid
sequenceDiagram
  participant W as Workbench
  participant DB as Store (tx)
  W->>DB: BEGIN; write + audit + outbox
  DB--xW: tx fails / rolls back
  Note over DB: nothing persisted, no outbox row, no success audit
  W->>DB: append FAILURE audit (refusal capture, S0.8.e)
```

### 2. After commit / before dispatch — durable outbox closes the window

```mermaid
sequenceDiagram
  participant W as Workbench
  participant DB as Store (tx)
  participant D as Dispatcher
  W->>DB: COMMIT write + audit + outbox row
  Note over W: process crashes before dispatch
  D->>DB: on recovery, poll pending outbox rows
  DB-->>D: row (externalOperationId)
  D->>D: dispatch → at-least-once delivery
```

### 3. Duplicate delivery — idempotent consumer

```mermaid
sequenceDiagram
  participant D as Dispatcher
  participant X as External
  D->>X: send (externalOperationId=k)
  X-->>D: (ack lost)
  D->>X: retry (externalOperationId=k)
  X->>X: dedup by k → effect applied ONCE
```

### 4. Worker loss — pending rows resume

```mermaid
sequenceDiagram
  participant D1 as Dispatcher A
  participant DB as Outbox
  participant D2 as Dispatcher B
  D1->>DB: claim batch
  Note over D1: worker A dies mid-batch
  D2->>DB: claim un-acked (still pending) rows
  DB-->>D2: same rows → resumed, none lost
```

### 5. External success / local timeout — safe retry

```mermaid
sequenceDiagram
  participant D as Dispatcher
  participant X as External
  D->>X: send (k)
  X->>X: effect succeeds
  X--xD: ack times out
  Note over D: row still looks pending
  D->>X: retry (k)
  X->>X: dedup by k → no double effect
```

### 6. Compensation failure — NOT APPLICABLE (0 sagas)

```mermaid
sequenceDiagram
  participant W as Workbench
  participant DB as Store (tx)
  participant X as External
  W->>DB: COMMIT (local truth)
  DB->>X: eventual dispatch (outbox)
  Note over W,X: no mutation demands cross-boundary atomicity → sagaRequired = 0
  Note over W,X: there is NO compensating action, so none can fail
```

### 7. Replay — idempotent by construction

```mermaid
sequenceDiagram
  participant Op as Operator
  participant DB as Outbox log
  participant X as External
  Op->>DB: replay rows [k1..kn]
  DB->>X: re-dispatch (k1..kn)
  X->>X: dedup by externalOperationId → no double effect
```

## Consequences

- The **only** durability seam is the after-commit / before-dispatch window
  (scenario 2), which the durable outbox closes. Everything downstream is made
  safe by **dedup on `externalOperationId`** (scenarios 3, 5, 7).
- Because `sagaRequired = 0`, there is deliberately **no distributed-rollback
  code path** — a simpler, safer design than compensating transactions.
- Named architecture ratification remains a human step; the delivery/audit model
  and the scenario set are coherence-gated in CI against the §S0.8.b/c evidence.

## References

- `V1_DOMAIN_WORKBENCHES_TODOS_2026-07-23.md` §S0.8
- `evidence/v1-workbenches/tara-transaction-boundaries.json` (§S0.8.b/c),
  `tara-mutation-effect-matrix.json` (§S0.8.a)
- `evidence/v1-workbenches/event-audit-boundaries.json` (this model,
  machine-checked)
- `docs/adr/ADR-S1-*.md`, `docs/adr/ADR-S2-*.md`
