# Reference patch — 01-async-appenders

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/LoggerApi.java` onto `contract/LoggerApi.java` — it adds
   `addAsyncAppender` and `flush()` and otherwise restates the base contract unchanged.
2. **Replace** `reference/src/Logger.java` with the file in this directory. `Registration.java`
   and `Demo.java` are untouched.

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

Both suites are green after the overlay: the curveball suite (4 tests) and the base suite
(14 tests) — this change is purely additive, so nothing that worked before stops working.

## Why this one costs more than the other two curveballs

Curveballs 2 and 3 change *how* an existing capability behaves. This one adds a capability that
did not exist at all: a caller can now ask for a destination that never makes it wait, plus a way
to know when that destination has genuinely caught up. That requires state no single call to
`log` can hold on its own — a background thread, and a record of what is still in flight — and
that state has exactly one owner: `Logger`, because it is the only thing that knows about every
registered destination. There was no seam to land this behind for free, and inventing one for a
single curveball would be exactly the kind of speculative interface C5 argues against.

**What stayed cheap regardless.** `Registration` did not need a new field to distinguish sync
from async — a second `List<Registration>`, one per registry, says the same thing without
teaching `Registration` that asynchrony exists. `offer` — the method that formats, writes, and
swallows a destination's exception — did not change by a single character; it has no idea
whether it is running on the caller's thread or `asyncExecutor`'s, which is exactly why rule 3
("a destination that throws is skipped, not fatal") did not need restating for the async case in
the curveball tests — it is the same code path.

## The one real decision: one background thread, not a pool

A single-threaded `ExecutorService` serves every asynchronous destination. The alternative — one
executor per destination, or a shared pool — would need its own bookkeeping to keep one
destination's writes in submission order, which `LoggerApi` promises. One thread gets that
ordering for free, at the cost of asynchronous destinations sharing a single lane; that trade-off
is stated in the requirement ("we are not asking for a way to wait on one destination at a
time") and is the reason a slow asynchronous destination could, in principle, delay a fast one —
worth saying out loud in a defence rather than discovering it under a follow-up question.

## Why 36 lines is honest, and why the budget is 42 rather than 54

This is one of only three curveballs in the corpus above 20 lines. The corpus median is 4, and five
curveballs absorb their requirement in literally zero lines, so 36 deserves an explanation rather than
a shrug.

The authoring plan says a cost above 20 means one of two things, and the answer has to be written down:

**Not this one:** *the reference has the wrong seams.* Splitwise's multi-currency curveball first
measured 30, its author found the cause — validation inlined in `addExpense` that the new overload had
to copy — extracted a helper into the base reference, re-verified the base suite green, and the cost
dropped to 15. That is the usual and correct response.

**This one:** *the requirement adds a new dimension rather than a new value within an existing one.* The
`Appender`/`Formatter`/threshold triple is a genuinely good seam, and both other curveballs prove it —
`03-rolling-appender` costs **0** and `02-collapse-repeats` costs **10**, neither touching `Logger.java`.
Asynchrony is not another value on any of those axes. It is a new axis: a queue, a worker, an ordering
guarantee, and a way to wait for it. No arrangement of appenders and formatters makes that free, which
is exactly why every one of the 36 lines is *additive* — new fields and two new methods, with no
existing body rewritten.

**What changed after this was first measured** is the budget policy, not the code. Slack used to scale
with cost (`d + ceil(d/2)`), which gave this curveball 18 lines of headroom — enough that a candidate
who reorganised half of `Logger.java` and one who added the same 36 lines cleanly would both score
level 2. Slack is now capped at six lines absolute. The measurement is unchanged and still reproducible
from this patch; only the tolerance around it tightened, and it now discriminates.

**What a defence should say.** If asked why this cost more than the other two, the answer is not "async
is hard" — it is that the existing seams vary *what* is written and *where*, and this requirement varies
*when*. Naming which axis a requirement moves along is the skill being tested.
