# Option table, spike brief, and ADR Senior judgment is not having the first answer faster. It is exposing credible alternatives, buying evidence where uncertainty matters, and leaving a decision a future teammate can reconstruct. Source: lesson 0046 · Compare Options and Record Decisions. --- ## Blank — option table At least two *genuinely different* designs. "Kafka" versus "managed Kafka" is one mechanism with two suppliers. Include "do nothing", manual, or buy whenever they are credible. | Option | Mechanism | Drivers it satisfies | Drivers it harms | Delivery / operational cost | Reversibility | Largest unknown | Evidence held | | --- | --- | --- | --- | --- | --- | --- | --- | | `[name]` | `[how it actually works, one sentence]` | `[driver numbers]` | `[driver numbers]` | `[build + run]` | `[a PR / a release / a migration / a contract]` | `[the thing that would change the ranking]` | `[measurement / none]` | --- ## Blank — spike brief Use a spike only when evidence could change the decision. | Field | One-line guidance | | --- | --- | | Decision blocked | Which choice cannot be made until this is answered. | | Question | One falsifiable uncertainty, phrased so a number can answer it. | | Timebox | Fixed hours or days, and one named investigator. | | Method | The smallest experiment; production-like only where the question requires it. | | Result format | A number, a trace, a compatibility finding — or an explicit "inconclusive". | | Disposal | Throw the code away, or review it separately before it goes near production. | "Explore the carrier API" is not a spike. It is an unbounded implementation prelude. --- ## Blank — Architecture Decision Record ``` # ADR [nnnn] · [title in the imperative] Status: Proposed | Accepted | Superseded by ADR [nnnn] Date: [YYYY-MM-DD] Decider: [name/role] Advisers: [who was consulted] ## Context [Facts that are true today, and the decision that is required. No solution here.] ## Forces [Named drivers, constraints, risks, and the owners affected.] ## Options [Each credible alternative and, in one line, why it was not chosen.] ## Decision [Active voice. Specific enough to constrain implementation.] ## Consequences [Benefits, the costs accepted, follow-up work, and who operates the result.] ## Dissent and advice [The material concern, who holds it, and what evidence would prove it right.] ## Revisit when [A measurable change in context. Then supersede — never rewrite.] ``` **Scale the apparatus to reversibility.** A class shape is changed in a pull request. A public contract, a data-ownership boundary, an expensive vendor, or a privacy model deserves broader advice and a written record. **The ADR compression test.** A teammate arriving in six months should be able to answer: what was true, what mattered, which alternatives were real, who decided, what was chosen, what price was accepted, and which changed fact should reopen it. --- ## Completed — what to do when the carrier is slow or down The team's provisional design in week 1 called the carrier inside the request: it was the least code and gave the customer a label immediately. It was written up as **ADR 0002 on 1 September**. The A3 spike killed it eleven days later. ### Option table | | A · Synchronous call | B · Persist intent, drain from a worker | C · Accept, then have an agent buy the label | | --- | --- | --- | --- | | Mechanism | The request calls the carrier and returns the label with the decision | The request commits the return, the decision, and a label intent in one transaction; a worker drains intents and calls the carrier idempotently | The request commits the return and the decision; the label appears in the agent queue and an agent buys it in the carrier portal, as today | | Drivers satisfied | Simplest operationally; one state fewer for the customer to understand, which helps A2 | D1 (accepts during an outage), D2 (the decision path excludes the vendor), D5 (the vendor is separately observable and separately disableable) | D1 and D2 as well, with the smallest possible amount of new code | | Drivers harmed | **D1 and D2 both fail.** At carrier p99 = 11.4 s the 800 ms budget is impossible, and a 30-minute outage refuses every request | A2 is harmed: the customer sees "accepted, label coming" rather than a label. A new process is on-call surface | The 50/day manual-queue guardrail. One manual label purchase per accepted return is roughly 10/day at 10% exposure and roughly 96/day at full exposure, on top of the 34/day of referrals the queue already carries | | Delivery cost | Lowest — about 2 days | Worker, outbox table, retry policy, backlog telemetry, disable flag — about 6 days | Near zero engineering; an agent runbook | | Operational cost | Nothing new, but every carrier incident becomes a customer-facing outage | A queue to watch, a backlog to age out, a flag to own and delete | Permanent human cost that grows linearly with success — the worst shape of cost for a pilot that intends to scale | | Reversibility | High — but reversing after launch changes what the API's `201` promises, which is a contract change | Medium: the pending state is visible to customers and to any consumer, so removing it later is a contract change too | High — it is the current process; turning it off costs nothing | | Largest unknown | Resolved by A3 | Whether customers understand a two-step flow (A2 — still inconclusive at n = 5) | Whether the manual queue survives the exposure ramp. The volume model says it does not | | Verdict | **Rejected on measured evidence** | **Chosen** | **Rejected as the primary mechanism; retained as the fallback** for a carrier outage longer than four hours | Option C deserves a moment. It was not a straw man and it was not fully discarded: it lost as the everyday mechanism because its cost scales with success, and it won as the emergency mechanism because it needs no new code and no vendor. Keeping a rejected option alive in a narrower role is often the most useful outcome of an option table. A fourth candidate — routing label requests through the company message broker — was raised and dropped before it reached the table, because no driver required it and one database transaction already gives all-or-nothing across return, decision, and intent. It is recorded as a rejected preference in the driver table rather than pretended away. ### Spike record | Field | Entry | | --- | --- | | Decision blocked | Whether label creation may sit inside the request that accepts a return. | | Question (falsifiable) | At the pilot's request rate, is the carrier's label-creation p95 under 500 ms, and does a retry after a timeout create a second label? | | Timebox | Four hours, one engineer (Dana). Publish the numbers whatever they are. | | Method | Carrier sandbox: 200 sequential creates recording p50/p95/p99; then 20 creates with the client socket timeout forced to 1 s, each retried once — ten with a client reference and ten without. | | Result | **p50 640 ms, p95 1.9 s, p99 11.4 s.** With a client reference: 0 duplicates in 10. Without: **2 duplicates in 10.** The answer to the question is no, and yes. | | Disposal | The harness was deleted. The one thing kept was the assertion "exactly one carrier label per intent", which became a production reconciliation check rather than a script on a laptop. | | Caveat recorded | The sandbox is not production. The p99 figure is directionally trusted, not contractually. This is why D1's evidence mechanism is a recurring drill rather than a one-off number. | Four hours removed the option the team had walked into the room preferring, and it had already been written up as an accepted decision. That is the entire economic case for spikes. ### ADR ``` # ADR 0003 · Persist label intent before calling the carrier Status: Accepted Date: 12 September Decider: Dana (tech lead) Advisers: Priya, Marco, Nils, Lea, Sam Supersedes: ADR 0002 ("Create the carrier label inside the request", accepted 1 September). ADR 0002 stays in the log, unedited, marked superseded — it is a historical fact about what the team believed on 1 September. ## Context The pilot must accept eligible returns and communicate a decision within a 4-hour median, at up to 40 requests/second at seasonal peak, with a p95 decision path of 800 ms. The carrier is a fixed contractual dependency. The A3 spike measured it at p50 640 ms, p95 1.9 s, p99 11.4 s, and observed duplicate labels on 2 of 10 timeout-retries made without a client reference. The team is seven people with one deployable and a fixed 30 September date. ## Forces D1 availability: accept during a 30-minute carrier outage, and create 99% of labels within 15 minutes of recovery. D2 performance: 800 ms p95 decision, excluding label creation. D5 operability: disable label attempts within 10 minutes. The guardrail of 50 manual-review requests a day. Assumption A2 (customer comprehension of a two-step flow) is unsettled and is the main cost of this decision. Operational simplicity for a team of seven. ## Options A. Synchronous carrier call — rejected: the measured p99 exceeds the decision budget by fourteen times, and a carrier outage would refuse eligible requests, failing D1 outright. C. An agent buys the label — rejected as the everyday mechanism: roughly 40 extra manual actions a day at pilot volume, breaching the 50/day guardrail somewhere around 30% exposure. Retained as the outage fallback. B. Persist intent and drain from a worker — chosen. ## Decision The API commits the return, the eligibility decision, and a label_intent row in a single transaction, and returns a decision with label_state = pending. A separate worker process in the same deployable drains intents and calls the carrier, passing the intent id as the carrier's client reference so retries are idempotent. A unique constraint on label_intent.return_id makes a second label unstorable. Label attempts sit behind a flag that on-call can disable in under ten minutes, checked inside the worker loop as well as at request time. ## Consequences Benefits: request acceptance survives a carrier outage; the 800 ms budget is achievable and measurable because the vendor is no longer inside it; carrier health becomes separately observable and separately disableable, which is what makes D5 reachable by a human at 02:00. Accepted costs: (1) the customer sees two steps — "accepted", then "label ready" — and A2 says two of five test participants misread the first as a refusal; we pay that down with wording work and a retest rather than by reversing the decision. (2) A worker process, a backlog, and an aging metric now exist and must be watched: new on-call surface for a team of seven. (3) A pending state is visible in the API, so removing it later is a contract change, not a refactor. (4) The label flag is temporary machinery with a deletion owner and a date. Follow-up: a reversal rehearsal of the disable path before each exposure stage (Nils); daily reconciliation against the carrier's label list (Lea); the A2 retest at n = 12 (Marco, 15 September). Operational ownership: worker health, backlog age, and the disable flag belong to on-call from the staff-accounts stage onward, and transfer at closeout. ## Dissent Recorded, from Sam (engineer): "For a pilot of about 50 requests a day we are buying a queue, a backlog metric, a flag and an on-call surface to solve a problem that option C solves with a runbook. We should ship C and revisit at 10% exposure." Not resolved by argument. What would prove it right: if by the 10% stage the label worker has produced no incident that the manual path would also have produced, and the manual queue has stayed under 20/day, then the worker was premature and we say so at closeout. ## Revisit when Any one of: sustained volume exceeds the worker's measured drain rate of 12 labels/minute; the carrier publishes a durable asynchronous label API; the label queue misses its 15-minute recovery target twice in one quarter; or the A2 retest shows the two-step flow costs more contacts than the outage risk it avoids. ``` ### The decision log, so ADR 0003 has a context | ADR | Decision | Status | Accepted cost | | --- | --- | --- | --- | | 0001 | Build the returns capability as modules inside the existing storefront deployable, not as a service | Accepted, 28 Aug | No independent scaling or release of eligibility. Revisit when a driver — not a preference — requires one | | 0002 | Create the carrier label inside the request that accepts the return | **Superseded by 0003**, 12 Sep | — | | 0003 | Persist label intent; drain from a worker; idempotent on the intent id | Accepted, 12 Sep | Above, in full | | 0004 | Referral is a first-class decision outcome, not a failure to decide | Accepted, 12 Sep | A third value in the API's closed state enum, and a 4-hour human SLA the team must staff | | 0005 | Warehouse receipt stays a manual confirmation for the whole pilot | Accepted, 15 Sep | About 40 clerk actions a day, with the removal trigger stated in the target workflow | **How the dissent ended.** At closeout, referrals alone reached 41/day at peak, so option C's extra label purchases would have breached the 50/day guardrail. Sam's main point was recorded, tested, and not upheld — but his second point, that the flag carries ongoing cost, was upheld and put on the removal list with a date. A dissent is not a vote to be defeated; it is a prediction to be checked. **Traces to:** drivers D1, D2, and D5 in `architecture-driver-table.md`; assumption A3, which supplied the evidence, and A2, which is the accepted cost, in `assumption-map.md`; the worker process in `c4-review-checklist.md`; the disable flag that `rollout-contract.md` rehearses.