# Reference patch — 02-dead-letter-office

The answer key for how the reference absorbs this change, and the thing that makes `budget.json`
reproducible. **Never shipped to the candidate.**

1. **Overlay** `contract-delta/NotificationServiceApi.java` onto `contract/` by filename. It is the
   base file character for character with one method and its javadoc appended:
   `List<DeliveryReceipt> deadLetters();`. Contract files are given, not owned, so this is not
   charged.
2. **Add** `DeadLetterOffice.java` (in this directory) to `reference/src/`. New file, so
   `measureChange` charges nothing for it. It is a `DeliveryListener` that keeps the `FAILED`
   receipts in a `CopyOnWriteArrayList` and hands back `List.copyOf`.
3. **Add six lines** to `reference/src/NotificationService.java` — three separate one-to-three-line
   insertions, all pure additions, nothing removed (mechanised in `edits.json`):

   ```diff
        private final List<DeliveryListener> listeners = new CopyOnWriteArrayList<>();
   +    private final DeadLetterOffice deadLetterOffice = new DeadLetterOffice();
   ```
   ```diff
            this.delivery = new RetryingDelivery(gateway, Objects.requireNonNull(retryPolicy, "retryPolicy"));
   +        addListener(deadLetterOffice);      // remembering the failures is an observation, not a step
        }
   ```
   ```diff
   +    @Override
   +    public List<DeliveryReceipt> deadLetters() {
   +        return deadLetterOffice.received();
   +    }
   +
        @Override
        public Optional<DeliveryReceipt> receiptFor(String idempotencyKey) {
   ```

Measured: `reference_diff = 6` (6 added, 0 removed, one file touched), so
`diff_budget = max(3, 6 + ceil(6/2)) = 9`.

## Why six, and why six is the floor rather than a smell

**Four of the six are the contract method itself.** `@Override`, the signature, one delegating
`return`, the closing brace. The interface grew a method; no arrangement of seams makes that free.
The other two are one field and one `addListener` call.

**Zero lines of behaviour changed.** `send` is untouched. The retry loop is untouched. The delivery
log, the channel selection, the recipient book and the receipt factories are untouched. Nothing was
*removed* at all, which is the shape a genuinely additive change has.

## What the listener seam paid for

Three of the interviewer's five rules cost nothing, because `DeliveryListener` already guarantees
them and the reference already honours them:

| Rule | Why it was free |
|---|---|
| one entry per notification, never per call | listeners fire on the terminal outcome of the submission that did the work; a resubmission of a spent key fires nothing at all |
| in the order they failed | listeners are called after the outcome is recorded, on the thread that did the work, so appending *is* the ordering |
| reading it while things fail must not blow up | the queue is a `CopyOnWriteArrayList` and the accessor returns `List.copyOf`, and even a broken listener cannot fail a delivery because the service swallows listener exceptions |

The one genuinely new business rule — **`SUPPRESSED` is not a failure** — is one comparison, and it
lives in the new file rather than in the service.

## What it costs a design without that seam

A design that never built an observer point writes this as a branch in `send`:

```java
claim.settle(receipt);
if (receipt.status() == DeliveryStatus.FAILED) {      // ← the fourth unrelated job in this method
    deadLetters.add(receipt);
}
```

That version is *four* lines and passes the first three tests, so it looks cheaper. Then:

- Put the branch **inside the retry loop** instead of after settlement — a natural mistake, since that
  is where "it failed" is actually discovered — and every exhausted channel adds an entry, so one
  notification appears three times.
- Put it **before the claim check**, or forget that a duplicate returns early, and every upstream
  resubmission adds a copy. That is the rule the interviewer said outright would make the list
  useless, and it is the one `resubmitting a failed notification does not add a second dead letter`
  exists to catch.
- Reach for a plain `ArrayList` and the operator reading the queue gets a
  `ConcurrentModificationException` from somebody else's failing notification.
- Return the list itself and `theListIsASnapshot` fails — and in production support's tooling silently
  mutates the service's state.

More importantly the cost is not the six lines, it is the precedent: `send` has now acquired one
"and also record this" branch, and the next request of this shape adds another. The version that
registers an observer can absorb "and page the on-call engineer", "and count failures per channel"
and "and republish to Kafka" for **zero** further lines in `send`, which is the actual claim the seam
is making.

**Both suites are green after the patch** — the curveball suite (5 tests) and the base suite (22
tests). The base suite is unaffected because it asserts outcomes and gateway calls, and this change
alters neither; the revised interface is a superset, so the base suite still compiles against it
untouched.
