Capstone · Strong packet
The Returns-Resolution Packet, Worked
One initiative, fourteen artifacts, one line of reasoning running through all of them. This is what a lead-level packet looks like when every page exists to serve a decision.
Mission tie-in: this packet is the course's proof of judgment. It joins the Phase IV initiative artifacts (lessons 0039–0050) to the Phase V system-design artifacts (lessons 0051–0060) into a single, coherent whole.
How to read this packet
The initiative is the one used throughout Phase IV and Phase V: a retailer wants self-serve returns. The brief that arrived was “Build a self-serve returns portal by Q3. It needs automated approvals, carrier labels, and status notifications.” What follows is what a competent tech lead, working with a product manager and a product designer, actually produced over fourteen weeks — including the parts that turned out to be wrong.
Three reading habits will get the most out of it.
- Follow the “Traces to” line under each artifact. Every artifact here earns its place by serving an outcome, an assumption, a driver, a risk, or a rollout decision. If you cannot say which, the artifact is decoration.
- Read the connecting prose, not only the tables. The tables are the output. The paragraphs between them are the reasoning, and the reasoning is the part that transfers to your own initiative.
- Look for the costs. Three options are compared and two are rejected in writing; two decisions changed under evidence; one dissent is recorded and never resolved in the dissenter's favour; the outcome is met on one measure and missed on another. A packet with no visible cost is a packet with a hidden one.
The numbers in this packet belong to the case. They are internally consistent and each carries a source line, because a source line is the habit being taught — but they are a teaching fixture, not industry data. Do not cite them outside this course. Names identify roles inside the case; they are fictional.
| # | Artifact | The decision it exists to serve |
|---|---|---|
| 1 | Initiative frame (v0.3, with history) | Is this worth doing, for whom, and what counts as done? |
| 2 | Assumption map | What might be false, and what will we spend to find out? |
| 3 | Current and target workflow | Where does the time actually go, and which waits can leave? |
| 4 | Story map and release slices | What is the smallest end-to-end path that teaches us something? |
| 5 | Acceptance examples | What exactly must be true — including when things fail? |
| 6 | Architecture drivers | Which qualities are allowed to shape the structure? |
| 7 | C4 context, container, dynamic views | What is ours, what runs, and what happens in time? |
| 8 | State machine and contract review | Who may change which fact, and what does a retry mean? |
| 9 | Option comparison and ADR 0003 | Which mechanism, at what accepted cost, revisited when? |
| 10 | Walking skeleton and delivery map | In what order do we retire risk, and what can slip? |
| 11 | Team charter and decision rights | Who decides, who advises, and what reopens a decision? |
| 12 | Test, telemetry and rollout plan | How do we learn in production without betting every customer? |
| 13 | Week-3 initiative review | What did the evidence change? |
| 14 | Closeout | Did it work, who owns it now, and what did we remove? |
Compare against the weak packet once you have read this one, then read the reviewer notes for the side-by-side judgment and the rubric dimension each difference touches.
1 · Initiative frame
The brief named a solution (“a portal”), a deadline (“Q3”), and three features. It named no user, no measure, and no baseline. The frame's job is to convert that into something a stranger can argue with. Everything downstream in this packet is scored against the outcome and guardrails below; if you change nothing else about how you work, change this.
- Why now
- Returns are the second-largest source of support contact volume, and returns volume rises about 40% from mid-November. No support headcount is approved for the season. Doing nothing means the contact queue grows with the returns queue and agents spend the season on status chasing instead of exceptions.
- Problem
- For signed-in domestic customers returning an unopened item within 30 days, a request waits a median of 72 hours for an agent decision, and 41% of those requests produce at least one support contact. Baseline source: returns queue export joined to the contact log, 1 June – 31 July, n = 4,812 requests; query owned by Priya, re-runnable.
- Outcome
- Move median time from request submitted to decision communicated for the eligible domestic cohort from 72 hours to under 4 hours, and the support-contact rate for that cohort from 41% to 31% or lower (a 25% relative reduction), with the pilot open to real customers by 30 September.
- Guardrails
- Fraud loss per 1,000 eligible returns at or below the 2.4 baseline (owner: Risk). Manual review queue at or below 50 requests/day (owner: Nils, support operations). WCAG 2.2 AA on the four critical flows (owner: Marco, design). Carrier cost per resolved return at or below the $3.42 baseline (owner: Priya). Personal data retained no longer than 30 days (owner: Ola, privacy).
- First boundary
- Signed-in domestic customer, unopened item, delivered 30 days ago or less, refund to the original payment method. Later expansion to opened/damaged items and to guest orders must remain possible without changing the eligibility contract — see contract review.
- Non-goals
- Exchanges. International shipping. Guest checkout. Redesigning warehouse intake. Replacing the agent tool. Each is here because someone plausibly assumed it was in scope.
- Constraints
- The order platform stays authoritative for order truth. The carrier contract is fixed for the pilot term. No payment card data enters the returns module. Personal data is retained for 30 days. The team is seven people and does not grow. The 30 September pilot date is fixed by the season, not by preference.
- Assumptions
- Seven named beliefs with impact, evidence strength, test and owner — see the assumption map. Two have since been falsified and one remains inconclusive.
- Risks
- R1 automatic decisions raise fraud loss (high × medium; early signal: fraud rate in the exposed cohort versus control; mitigation: hold rule plus staged exposure; owner: Risk; review weekly). R2 the referred cohort reads a referral as a refusal and contacts support anyway, harming the very measure (high × medium; signal: contact rate within the referred segment; owner: Marco). R3 the carrier cannot meet the label latency budget (medium × high before the spike, resolved — see ADR 0003). R4 guest orders are 29% of demand and stay outside the pilot, so the headline outcome overstates the business effect (medium × high; owner: Priya; decision date 15 November).
- Decision rights
- Outcome, scope and cohort: Priya. Experience: Marco. Architecture and technical risk: Dana. Rollout advance and reversal: Dana with Nils, stop authority to any on-call engineer. Full table in the team charter.
- Next evidence
- Comprehension retest at n = 12 (Marco, 15 September). Fraud rate in the 1% cohort versus matched control after 120 exposed requests (Risk, from 29 September). Guest-order opportunity sizing (Priya, by 15 November).
A frame that never changes is a frame nobody consulted. This one changed twice, and both changes are visible rather than quietly rewritten — which is what lets a newcomer in week 10 reconstruct why the outcome is worded the way it is.
| Version | What changed | Why — the evidence that forced it |
|---|---|---|
| v0.1 25 August |
First frame written from the brief in a 90-minute session with product and design. Outcome stated as “resolve eligible returns in under 4 hours and generate fewer support contacts.” Contact reduction had no number and no denominator. Success was described as “automatic approval for at least 90% of eligible requests.” | Nothing yet — this is the honest starting position. It is recorded rather than deleted so the team can see how far the framing moved on evidence rather than on opinion. |
| v0.2 4 September |
Three changes. (a) The contact target gained a number and a denominator: 41% → 31% for the eligible domestic cohort. (b) The outcome event changed from “automatic approval” to “decision communicated” — a referral to an agent within four hours now counts as success. (c) Added the non-goal “replacing the agent tool” and the guardrail “carrier cost per resolved return.” | The 200-case policy sample (assumption A1) showed 15% of eligible requests are genuine judgement calls and 22% need a rule the pilot will not build. An outcome defined on automation would have scored the correct product behaviour — routing a judgement call to a human quickly — as failure, and would have pushed the team to automate decisions it should not automate. |
| v0.3 2 October |
Two changes. (a) Guest orders moved from “non-goal” to “deferred opportunity, sized, decision date 15 November,” with the 29%-of-demand figure written into the frame. (b) Mobile comprehension work moved ahead of the evidence-photo rule in the slice order. | Guest orders were re-argued in three consecutive weekly reviews because “non-goal” read as “refused forever.” Naming the deferral with a date and an owner ended the recurring debate at a cost of one line. The mobile reorder followed the week-1 exposure reading of an 11-point completion gap on mobile. |
Traces to Nothing — this is the root. Every other artifact in this packet traces here. The test of the frame is the reverse direction: name any artifact below and you should be able to reach this page in one hop. Method: lesson 0039; template in the initiative playbook.
2 · Assumption map
The frame contains beliefs the team cannot yet defend. Writing them down is easy; the leadership work is deciding which ones are worth buying evidence for, and choosing a test proportionate to what the answer is worth. Impact-if-false runs vertically, evidence strength horizontally; the top-left corner — high impact, weak evidence — gets the budget.
EVIDENCE STRENGTH
weak strong
+-------------------------+-------------------------+
high | A1 policy codifiable | A8 customers prefer |
| A2 customers trust it | self-service to |
IMPACT | A3 carrier is fast | calling |
IF FALSE | A6 fraud stays flat | |
+-------------------------+-------------------------+
medium | A5 agents accept the | A4 receipt events are |
| refusal reasons | reliable |
| | A7 policy changes often|
+-------------------------+-------------------------+
tested first: A1, A2, A3 no test bought
| Id | Assumption | Risk type | Impact if false | Evidence before | Test chosen (cost) | Owner | Result |
|---|---|---|---|---|---|---|---|
| A1 | At least 90% of eligible requests match a codifiable policy rule. | Feasibility | High — the manual review queue guardrail (50/day) breaks, and the whole automation premise weakens. | Weak: one estimate from the policy owner (Anya), no sample. | Classify 200 closed agent decisions from June–July into codifiable / needs one new rule / judgement call. 2 analyst-days. | Priya | Falsified. 63% codifiable today, 22% need the opened-item photo rule, 15% are judgement calls. Automatic-decision coverage is 63% in the pilot's scope, 85% with the photo rule. |
| A2 | Customers trust an automatic decision enough not to contact support about it. | Value | High — the contact-rate half of the outcome fails even if the latency half succeeds. | Weak: inference from the 41% contact rate, no direct test. | Five-participant moderated comprehension test on a clickable prototype of the accept, refuse and refer states. 3 designer-days. | Marco | Inconclusive. 2 of 5 read “your label is being prepared” as a refusal. n = 5 cannot separate signal from participant. Retest at n = 12 scheduled 15 September; the wording change shipped anyway because it is cheap and reversible. |
| A3 | The carrier creates a label fast enough to sit inside the request that accepts the return. | Feasibility | High — it decides whether label creation is synchronous, which decides the failure model of the whole flow. | Weak: a latency figure in the vendor's own documentation, never measured by us. | Four-hour timeboxed spike against the carrier sandbox: 200 sequential creates measuring p50/p95/p99, then 20 induced timeouts retried with and without a client reference. Throw the harness away. | Dana | Falsified. p50 640 ms, p95 1.9 s, p99 11.4 s. 2 of 20 timeout-retries without a client reference produced a second label. Changed the decision — see ADR 0003. |
| A4 | Warehouse receipt events are reliable enough to trigger a refund automatically. | Feasibility | Medium — a missed event strands money; a duplicate event double-refunds. | Moderate: the event stream has six months of history and an existing consumer. | Replay 30 days of receipt events against order ids; count missing, duplicate and late. 1 engineer-day. | Lea | Confirmed with a caveat. No missing or duplicate events; 0.4% arrive more than 24 hours after the physical scan. The refund SLA therefore starts at the event, not at the scan — written into the customer wording. |
| A5 | Support agents will accept the system's refusal reasons rather than override them. | Viability | Medium — silent overriding means the policy model is wrong and nobody finds out. | Weak: agent opinion in the discovery workshop. | Shadow ten refusals with two agents; record every override and its stated reason. 1 day. | Nils | Partial. 3 of 10 overridden — all three for gift orders, which no rule covers. Produced one new rule, deferred to slice 3 rather than smuggled into the pilot. |
| A6 | Fraud loss does not rise when the decision becomes automatic. | Viability | High — a guardrail breach stops the initiative regardless of the outcome measures. | Weak, and untestable before exposure: fraud responds to the presence of the new path, not to a prototype. | No pre-test bought. Converted into a rollout guardrail instead: exposed cohort versus matched control, stop threshold, named authority. See the rollout contract. | Risk | Deferred to rollout by design. Held at every stage; see closeout for the number and for what the sample size can and cannot detect. |
| A7 | Policy rules change often enough to justify a separate, independently releasable policy module. | Feasibility | Medium — if false, the modifiability driver is imaginary and the module seam is unearned cost. | Moderate: an existing policy change log nobody had read. | Read the change log. 2 hours. | Dana | Confirmed. Nine policy changes in twelve months, four of them mid-quarter. Justifies driver D4 and the eligibility module seam. |
| A8 | Customers would rather self-serve than call, for this class of request. | Value | High | Strong: 71% of the contacts in the baseline join are status chasing on an already-submitted request, not requests for help deciding. | None bought — deliberately. Buying evidence for a belief that is already well evidenced is a way of looking busy. | — | Held as a stated premise, re-checked in the closeout. |
Two habits in that table are worth naming. First, the test is chosen to fit the decision, not the assumption: A3 got a four-hour spike because a number changed a structural choice, while A7 got two hours of reading because the log already existed. Second, “no test” is a legitimate, recorded choice — A6 could not be answered before exposure, so it became a rollout guardrail with a stop threshold, and A8 was already evidenced. A map where every row has a test is usually a map nobody costed.
Traces to Outcome (A1, A2, A8) · guardrails (A5, A6) · driver D1 and D4 (A3, A7) · rollout stop thresholds (A6). A1 and A3 each changed an artifact downstream; the change is visible in frame v0.2 and ADR 0003. Method: lesson 0040.
3 · Current-state and target workflow
The team mapped one real, recently completed case with a support agent, the policy owner, a warehouse clerk and two engineers in the room — events in past tense first, then the commands that caused them, then the policies, then the waits. Mapping the current state first is not ceremony: three of the five hotspots below were invisible to everyone who had only ever seen the target-state slide.
| Domain event (past tense) | Command | Actor | Policy that fired | External system | Elapsed |
|---|---|---|---|---|---|
| Return requested | Email support | Customer | — | Email / helpdesk | 0:00 |
| Order verified | Look up order | Agent | — | Order platform | +2:10 (queue wait) |
| Evidence requested | Ask for photo | Agent | “If the box is open, ask for a photo” | +2:20 | |
| Evidence received | Reply with photo | Customer | — | +19:40 (wait) | |
| Policy interpreted | Read policy page | Agent | Window, condition, payment-method rules | Wiki | +51:05 (queue wait) |
| Eligibility decided | Approve | Agent | “Within 30 days and unopened → approve” | Helpdesk macro | +51:20 |
| Label created | Buy label | Agent | — | Carrier portal (manual) | +51:35 |
| Customer notified | Send label | Agent | — | +51:40 | |
| Parcel received | Scan parcel | Warehouse clerk | “Match item to the return note” | Warehouse system | +6 days |
| Refund authorised | Approve refund | Finance clerk | “Refund after inspection passes” | Spreadsheet → payments | +6 days, twice-daily batch |
| Refund settled | — | Payments (external) | Card scheme timing | Payments | +3–5 banking days |
Two facts fall out of that table immediately. 68 of the 72 baseline hours sit in two queue waits — order verification and policy interpretation — and neither is work; both are waiting for a human to reach the request. And the evidence round-trip (19 hours) only exists for opened items, which the pilot scope excludes. That is why the pilot's narrow scope is not timidity: it removes the second-largest wait by construction.
| Hotspot | Example / counterexample | Who knows | Open question | Next evidence |
|---|---|---|---|---|
| “Approved” means three different things | Support: policy eligibility passed. Warehouse: inspection passed. Finance: money settled. A single global status would collapse all three. | Anya, Nils, finance clerk | Are these three states of one thing, or three things? | Modelled as three owners with separate facts — see state and contracts. |
| Policy interpretation varies by agent | Three agents given the same 31-day case: two refused, one approved “because the customer was polite about it.” | Anya | How much of the policy is actually written down? | Assumption A1 sample — 63/22/15 split. |
| Gift orders have no rule and no owner | The purchaser is not the recipient; refunds to the original payment method surprise the recipient. | Nils | Who decides gift-order policy? | Owner assigned (Anya); rule deferred to slice 3 (A5). |
| Warehouse-to-refund handoff is a spreadsheet | The clerk copies order ids into a shared sheet; finance reads it twice a day. One id was transposed in the sampled month. | Warehouse clerk | Is the receipt event trustworthy enough to replace the sheet? | Assumption A4 replay — reliable, 0.4% late. |
| Nobody owns “the request that went quiet” | A request with no reply after the evidence email simply ages. No queue, no alert, no age limit. | Nobody — that is the finding | What is the maximum age of an open return? | Became invariant candidate I4 and the 21-day expiry transition. |
| Term | Precise local meaning | Owned by | Phrases to avoid |
|---|---|---|---|
| Eligibility decision | Accepted, refused with a reason code, or referred to an agent. Exactly one per return. | Eligibility | “approved”, “status 2” |
| Referral | A decision that a human must make this call, made within the same latency budget as an acceptance. | Eligibility | “pending”, “stuck”, “rejected” |
| Label intent | A recorded promise to obtain exactly one carrier label for one accepted return. Exists before the carrier is called. | Return journey | “the label” (before it exists) |
| Receipt | The warehouse's fact that the parcel matching this return arrived. Not an inspection result. | Warehouse (external) | “approved”, “received OK” |
| Settlement | Money has left us, as asserted by Payments. Only Payments may assert it. | Payments (external) | “refunded” used loosely |
The target workflow is not the current one with automation bolted on. Two waits are deleted, one manual step is deliberately kept, and one new state (referral) is added because the evidence said 15% of cases need a human and pretending otherwise would have produced bad automatic refusals.
| Event | Command | Actor | Policy | Budget | Change from current |
|---|---|---|---|---|---|
| Return requested | SubmitReturn (idempotent) | Customer | One open return per order line | — | Structured, not email |
| Order verified | — | Returns module | Order platform is authoritative | p95 200 ms | Wait removed (was 2:10) |
| Eligibility decided | AssessEligibility | Policy engine | Window, condition, payment method, fraud hold | p95 800 ms | Wait removed (was 49h) |
| ↳ accepted | RecordLabelIntent | Return journey | Exactly one intent per return | same transaction | New — see ADR 0003 |
| ↳ referred | QueueForAgent | Return journey | Agent answers within 4 h | 4 h SLA | New state — 15% of cases |
| ↳ refused | — | Eligibility | Reason code + human text + appeal path | — | Reason is now structured |
| Label created | CreateLabel (async, idempotent) | Label worker → carrier | One carrier label per intent | 99% within 15 min of recovery | Off the request path |
| Customer notified | NotifyCustomer | Return journey | One notification per state change | — | Proactive, not on request |
| Parcel received | RecordReceipt | Warehouse clerk (manual for pilot) | Item must match the return | — | Kept manual on purpose |
| Refund requested | RequestRefund | Return journey | Only after receipt; never sets settlement | within 24 h of receipt | Spreadsheet removed |
| Refund settled | — | Payments (external) | Refund ≤ captured amount — enforced by Payments | 3–5 banking days | Unchanged, and deliberately so |
The manual seam, stated with its removal trigger. Warehouse receipt stays a human confirmation in the existing tool for the whole pilot, even though assumption A4 says the event stream is reliable. Reason: A4's replay is retrospective, and a wrong automatic refund is money we cannot claw back cheaply. Removal trigger: 14 consecutive days in which fewer than 0.1% of receipt events arrive late and zero mismatches are recorded. Owner: Lea. Capacity: about 40 confirmations/day, inside the clerk's existing workload.
Finally, the workflow yields candidate model boundaries — not deployables. Language, change cadence, invariants and ownership all point the same way, which is the only kind of boundary evidence worth acting on.
| Boundary | Language evidence | Change evidence | Invariant evidence | Ownership evidence |
|---|---|---|---|---|
| Eligibility | “Decision”, “reason”, “override” exist nowhere else | 9 policy changes/yr (A7) | Exactly one open decision per return | Anya is the single named policy owner |
| Return journey | “Intent”, “progress”, “notification” | Changes with wording and channel, on design's beat | One label intent per accepted return | Marco owns the customer-visible steps |
| Carrier (external) | Vendor vocabulary: shipments, references | Vendor's release cadence, not ours | One carrier label per intent | Contractual; not ours to change |
| Payments (external) | “Settled”, “captured” | Payment rail cadence | Refund ≤ captured amount | Ravi's team; financial truth |
Traces to Outcome (the two deleted waits are the 68 of 72 hours) · assumption A1, A4, A5 (hotspots that became tests) · driver D2 and D4 (the boundary evidence) · risk R2 (the referral state). Method: lessons 0041, 0052, 0053.
4 · Story map and release slices
The backbone is the customer's journey, left to right. The depth beneath each activity is variation, not tasks. The release line is drawn across the map so that each slice is a complete journey for a narrower population — never a horizontal layer.
| Start request | Check eligibility | Choose resolution | Return parcel | Track outcome |
|---|---|---|---|---|
| signed-in order | domestic / unopened / ≤30 days | refund to original method | carrier label, drop-off | status page + email |
| gift order | referred to agent (judgement) | refund with restocking fee | QR drop-off (no printer) | SMS |
| guest order | opened / damaged, photo evidence | exchange | home collection | agent contact / appeal |
| agent-created on behalf | international | store credit | in-store drop-off | proactive delay notice |
| Slice | Scope | What it tests | Exit evidence | Target |
|---|---|---|---|---|
| Pilot slice (row 1) | Signed-in, domestic, unopened, ≤30 days, refund to original method, carrier label, status page and email. Warehouse receipt confirmed manually. | Assumption A2 (do customers trust an automatic decision?) and both halves of the outcome, on 1% of eligible demand. | Median resolution and contact rate for the exposed cohort against a matched control, at n ≥ 120 requests; fraud and queue guardrails held. | Open 29 September |
| Slice 2 | Referral made first-class: agent decision view, four-hour referral SLA, appeal path after a refusal, referral-specific customer wording. | Assumption A5 (will agents accept the reasons?) and risk R2 (does a referral read as a refusal?). | Override rate below 15% of referrals; contact rate inside the referred segment no worse than the accepted segment plus 10 points. | Week 3 of exposure |
| Slice 3 | Opened and damaged items with structured photo evidence; the gift-order rule found by A5. | The remaining 22% of A1 — does the photo rule actually make those cases codifiable? | Automatic-decision coverage rises from 63% to at least 80% of in-scope requests without a fraud-guardrail move. | After the pilot's continue decision |
| Deferred | Guest checkout (29% of demand; needs an identity design and a separate fraud model — decision date 15 November). Exchanges. International. Store credit. Home collection. In-store drop-off. | Nothing yet. Each carries the reason it waits, so the deferral is a decision rather than an oversight. | Guest orders: sizing and fraud-model sketch from Priya and Risk by 15 November. | — |
Note what the pilot slice is not. It is not “the eligibility service”, and it is not “the returns database”. Both would be finishable without a single customer being served. The slice is narrow in variation — one cohort, one condition, one resolution — and complete across the journey, which is the only shape that can produce the outcome measure the frame is scored on.
Traces to Outcome (the pilot slice is the smallest thing that can move both measures) · assumptions A2, A5, A1 (one slice per open question) · risk R2 and R4 (slice 2 and the deferral) · rollout stage 3 (the 1% cohort receives exactly this slice). Method: lesson 0042.
5 · Acceptance examples
These were written before implementation, by product, design, engineering and the support-operations lead together. They are not a test plan; they are the specification of the rules the team argued about. Each rule carries a happy path, a boundary, a failure, and the telemetry that proves it in production — because a rule you cannot observe after release is a rule you only believe in.
- Rule
- An unopened item bought by the signed-in customer and delivered 30 days ago or less is accepted automatically. The window is evaluated on the delivery date in the store's timezone, not on the submission timestamp.
- Happy
- Given an unopened domestic item delivered 12 days ago and a signed-in purchaser, when they submit a return, then the decision is
accepted, exactly one label intent is recorded in the same transaction, and the decision is visible to the customer within 800 ms at p95. - Boundary
- Given the same item delivered 31 days ago, then the decision is
refusedwith reason codeoutside_return_window, the refusal states the delivery date it used, and an appeal path is offered. - Boundary
- Given delivery exactly 30 days ago at 23:50 store time and submission at 00:10 store time the following day, then the decision is
accepted: 30 days is inclusive and measured in whole days from the delivery date. This example exists because the first implementation used UTC timestamps and refused it. - Guardrail
- A keyboard-only customer can complete the happy path; every refusal reason is announced by a screen reader and never conveyed by colour alone.
- Telemetry
eligibility_decision_total{outcome, reason_code}, andeligibility_decision_secondsas a histogram. A shift in the reason-code mix is the earliest signal that a policy change did something unintended.- Traces to
- Outcome (latency half) · driver D2 · assumption A1.
- Rule
- A customer with three or more returns marked “not received” in the trailing 90 days is referred, never auto-refused, and no label is created until an agent releases the hold. The customer is never shown the fraud reason.
- Happy (from the guardrail's point of view)
- Given a customer with three such returns in the last 90 days, when they submit an eligible return, then the decision is
referredwith internal reasonfraud_review, the customer sees “a specialist is checking this — you'll hear within 4 hours”, and no label intent is recorded. - Boundary
- Given a customer whose third such return was 91 days ago, then no hold is applied and the normal decision runs.
- Failure
- Given a held return that is still unanswered after 4 hours, then an alert routes to Nils and the ageing hold appears on the operations dashboard. An unanswered hold is a customer waiting, not a queue statistic.
- Guardrail
- No customer-visible text, email, or API response distinguishes a fraud hold from any other referral. A negative test asserts that
fraud_reviewnever appears in any customer-facing payload. - Telemetry
fraud_hold_rateby cohort and by exposure stage;fraud_hold_age_secondsmax. Both feed the rollout guardrail, not just a dashboard.- Traces to
- Guardrail “fraud loss ≤ 2.4/1,000” · risk R1 · assumption A6 · rollout stop threshold.
- Rule
- The customer's return is accepted independently of the carrier. Exactly one carrier label exists per accepted return, however many times we retry.
- Failure
- Given an accepted return whose label intent is recorded, when the carrier call times out after 5 seconds, then the return stays in
awaiting_parcelwithlabel_state = pending, the customer sees “your label is being prepared”, and the worker retries with the same client reference. - Failure
- Given that retry, when the carrier had in fact created a label on the first attempt, then the original label is returned and no second label is created. Asserted directly: exactly one
carrier_label_idper return, checked as a database constraint and as a daily reconciliation against the carrier's own list. - Failure
- Given the carrier is unavailable for 30 minutes at peak, then eligible requests are still accepted, and 99% of pending labels are created within 15 minutes of recovery.
- Operability
- Given label failures exceed 2% for five minutes, then an alert fires with the carrier's error breakdown, and on-call can disable label attempts within ten minutes. Disabling label attempts must not refuse new returns, and the worker must stop retrying the existing backlog — see the reversal rehearsal, which is where we discovered it did not.
- Telemetry
carrier_call_seconds,carrier_error_total{code},label_pending_age_secondsmax,duplicate_label_total(must stay at zero).- Traces to
- Drivers D1 and D5 · assumption A3 · ADR 0003 · rollout stop threshold.
- Rule
- The returns module requests refunds; it never asserts settlement, and it never reverses one. A settled refund can only be offset by a new fact created by Payments.
- Failure
- Given a refund authorised on a receipt event, when the warehouse later reverses that receipt (wrong item scanned), then the return moves to
settlement_disputed, no compensating charge is issued automatically, and a case is opened with Payments carrying the original correlation id. Rationale: reversing money is Payments' decision and often a human's; a returns module that could do it silently would be a fraud vector. - Boundary
- Given an accepted return whose parcel has not arrived within 21 days, then the return moves to
expiredwith reasonparcel_not_received, the customer is notified exactly once, and no refund is requested. - Failure
- Given a refund request that Payments rejects because the captured amount is lower than the requested amount, then the return moves to
settlement_disputedand is queued for an agent. The returns module does not attempt a smaller refund on its own: the invariant “refund ≤ captured” belongs to Payments and is enforced there. - Telemetry
settlement_disputed_total{cause}; the age of any accepted return with no terminal state (feeds the I4 reconciliation sweep).- Traces to
- Invariants I3 and I4 · the “settlement” vocabulary entry · driver D3 (nothing crosses a trust boundary silently).
- Rule
- One intent creates at most one return. A repeated intent is the same return; a genuinely new intent for an already-open line is refused with a link, not silently merged.
- Failure
- Given a customer double-taps submit, so the same
Idempotency-Keyarrives twice, when the second request is processed, then the same return id, decision and label are returned with 200, and exactly one return exists. - Boundary
- Given the same
Idempotency-Keybut a different request body, then the response is 409 with codeidempotency_key_reuse. Silently returning the first result would hide a client bug; silently processing the second would create two returns. - Boundary
- Given a new intent key for an order line that already has an open return, then the request is refused with reason
return_already_openand a link to the existing return. - Guardrail
- Given a customer edits the order id in the URL to another customer's order, then the response is 404, an authorization event is logged with the actor and the attempted id, and no field of the other order appears in any response, log line or error message.
- Telemetry
idempotent_replay_total,idempotency_conflict_total,authz_denied_total{reason}.- Traces to
- Driver D3 · invariant I1 · the idempotency section of the contract review.
Traces to Every example above names its own trace. Collectively: outcome (E1) · guardrails (E2, E5 guardrail) · drivers D1, D2, D3, D5 (E1, E3, E5) · invariants I1–I4 (E4, E5) · rollout thresholds (E2, E3). Method: lessons 0042 and 0049.
6 · Prioritized architecture drivers
Five drivers, not fifty. Each is written as a full quality-attribute scenario — source, stimulus, environment, artifact, response, measure — because a quality without a situation and a number cannot distinguish two designs, and a driver that cannot distinguish two designs is context, not a driver.
| # | Quality | Scenario (source / stimulus / environment / artifact / response / measure) | Why it is ranked here | Evidence mechanism · owner |
|---|---|---|---|---|
| D1 | Availability | The carrier becomes unavailable for 30 minutes during peak returns traffic. The returns module and label worker must continue to accept and decide eligible requests, and create 99% of the queued labels within 15 minutes of carrier recovery. | Highest reach: it decides whether the carrier call sits inside the request. Uncertainty was highest too, until the A3 spike measured it. | Recovery drill in staging with the carrier sandbox forced to 100% timeouts, run before every exposure stage · Dana |
| D2 | Performance | Customers submit returns at 40 requests/second at seasonal peak. The eligibility decision path must return a decision within 800 ms at p95, excluding label creation. | Directly carries the outcome's latency half, and it is the driver that forces label creation off the request path. Conflicts with D1 — see below. | Load test at 40 rps against a production-shaped dataset in the pipeline; production histogram with a p95 alert · Sam |
| D3 | Security & privacy | A signed-in customer substitutes another customer's order identifier, in any environment including staging. The returns API must disclose no field of the other customer's order or return, in responses, errors, or logs, and record the attempt. | Impact is unbounded and non-negotiable; reach is moderate but it constrains every read path and every log line. | Negative authorization tests in the contract suite; a log-scanning fitness function that fails the build on an unredacted order id · Ola |
| D4 | Modifiability | The policy owner adds one eligibility rule, mid-quarter, nine times a year. One engineer must release it within one working day, changing no carrier adapter, no UI component, and no API contract. | Evidenced by A7 (9 changes/yr, 4 mid-quarter). It is the reason eligibility is a module with its own seam rather than rules scattered through the journey. | Import-rule fitness function in the build (below); measured lead time for the last three policy changes · Dana |
| D5 | Operability | The carrier starts failing: label errors exceed 2% for five minutes during any exposure stage. On-call must identify the carrier as the cause and disable label attempts within ten minutes, without refusing new returns and without leaving the worker retrying. | Lower reach than D1 but it is what makes D1's response actually reachable by a human at 02:00. Its rehearsal found a real defect. | Alert with a runbook link; the disable path exercised in the reversal rehearsal before each stage · Nils |
The conflicts, written down before an engineer discovers them mid-sprint. D1 and D2 pull against a synchronous carrier call: D2 wants the decision back in 800 ms, and the A3 spike measured the carrier at p99 11.4 s, so a live label inside the request cannot satisfy both. The team's resolution is to return the eligibility decision quickly and make label creation a separately observable state — which is exactly the decision recorded in ADR 0003, and which costs the customer a two-step mental model (“accepted” then “label ready”), the cost that assumption A2's comprehension test exists to measure. D3 and D5 also conflict in miniature: on-call wants richer failure payloads in logs, and D3 forbids identifiers in them. Resolution: correlation ids in logs, identifiers only behind an access-controlled lookup.
| Proposed | Why it is not a driver here |
|---|---|
| “The system must scale.” | No load, operation or horizon. Once written out, the real load is 40 rps at seasonal peak (D2), which a single deployable handles. No credible design in the option table responds differently to “scale”, so it distinguishes nothing. |
| “Must handle 10× current volume.” | 10× is beyond the twelve-month horizon in the demand forecast, and the pilot is 1% of one cohort. Designing for it now would buy an architecture we cannot test and would not be able to justify in ADR 0001. |
| “Must be cloud-native / event-driven.” | These are mechanisms, not qualities. They belong in the option table as candidate answers, and both appear there — where they can be compared and rejected with a reason. |
| “Must be maintainable.” | Replaced by D4, which names the change (one policy rule), the person (one engineer), the elapsed time (one day) and the blast radius (no adapter or contract change). That version can fail a check; “maintainable” cannot. |
| “99.99% uptime.” | A number without an operation. The pilot's real availability requirement is D1: accepting requests must survive a carrier outage. Label creation may degrade; request acceptance may not. One number for the whole system would have hidden that distinction. |
Traces to Outcome (D2 carries the latency half) · guardrails (D3 privacy, D5 the queue's operability) · assumptions A3 (D1), A7 (D4), A6 (D5's disable path) · risk R1 and R3. D1 and D2's conflict is the direct input to ADR 0003; D4 is the direct input to the module seam in the container view. Method: lessons 0043 and 0059.
7 · Architecture views
Four views, each with a stated audience and a question it answers. Anything that helps neither is deleted. The rule the team enforced in review: every comment must point at a driver, a workflow step, or a risk — otherwise it is decoration and gets no diagram time.
7.1 System context
| Element | Kind | Relationship to the returns system | Protocol | Trust boundary | Personal data crossing |
|---|---|---|---|---|---|
| Customer | Person | Submits a return, reads status, appeals a refusal | HTTPS, session-authenticated | Public internet | Name, address, order history (theirs only) |
| Support agent | Person | Decides referred returns, releases fraud holds, overrides | HTTPS, staff SSO + role | Corporate | Full return detail, audited on read |
| Returns system | System in scope | Decides eligibility, records intent, tracks the journey, requests refunds | — | — | Retains name, address, reason for 30 days |
| Order platform | External, internal team | Authoritative for order, delivery date, captured amount. Read-only to us. | Internal HTTP, synchronous | Internal, same estate | We read; we do not copy order history |
| Carrier | External, third party | Creates labels; we send the minimum needed to print one | HTTPS + API key, outbound only | Vendor | Recipient name and address only — never the return reason |
| Payments | External, internal team | Authoritative for settled money. We request; it decides. | Internal HTTP + async settlement event | Internal, PCI scope | No card data enters returns (constraint) |
| Warehouse system | External, internal team | Publishes receipt facts; a clerk confirms them manually during the pilot | Event stream, at-least-once | Internal | Order id and item only |
CONTEXT · returns resolution (v3, 12 Sep)
legend: [ ] person ( ) system in scope { } external system ==> trust boundary
[Customer] [Support agent]
| submit / track (HTTPS) | decide referrals (SSO)
v v
+---------------------------------------------+
| ( Returns system ) |
| decides eligibility, records label intent, |
| tracks the journey, requests refunds |
+---------------------------------------------+
| read order | create | request ^ receipt
| (sync) | label | refund | events
v v ==vendor==> v |
{Order platform} {Carrier} {Payments} {Warehouse system}
authoritative: one label authoritative: authoritative:
order truth per intent settled money parcel received
The three “authoritative:” labels are the most load-bearing text on the diagram. They say that the returns system holds no independent opinion about order truth, settled money, or whether a parcel arrived — and that is what makes the state machine in section 8 enforceable rather than aspirational.
7.2 Containers
| Container | Technology | Responsibility | Data it owns | Owner | Driver that justifies it existing separately |
|---|---|---|---|---|---|
| Existing web app | Server-rendered pages in the existing storefront | Customer-facing return flow and status page | None | Marco / Sam | None needed — it already exists; adding a new frontend would be a boundary with no driver |
| Returns module | Python package inside the existing storefront deployable | Eligibility decisions, return journey state, API, notifications | return, eligibility_decision, label_intent, return_event | Dana | None — deliberately not a separate deployable. See ADR 0001. |
| Label worker | Same artifact, separate process, same database | Drains label intents, calls the carrier idempotently, retries with backoff | None — it reads and updates label_intent | Lea | D1: the carrier call must survive a 30-minute outage without blocking request acceptance. A separate process, not a separate service. |
| Existing database | The storefront's PostgreSQL, new schema returns | Durable state and the outbox table | The four tables above; no other module may write them | Dana | None — a second store would add operational surface with no driver. Ownership is enforced by schema grants, not by convention. |
| Reconciliation job | Same artifact, scheduled daily | Sweeps for I4 breaches and for carrier labels we do not know about | None | Lea | D5: the reconciliation condition needs a detector with an owner, or it is a wish. |
CONTAINERS · one deployable, two processes, one schema (v3, 12 Sep)
legend: [==] process (--) datastore --> in-process call ~~> async
+----------------------- one deployable artifact -----------------------+
| |
| [== web process ==] [== label worker ==] |
| storefront pages drains label_intent |
| | | ^ |
| v | | retry w/ |
| +--------------------------+ | | same ref |
| | returns module | v | |
| | +--------------------+ | ==vendor boundary==> |
| | | eligibility | | {Carrier} |
| | | (no I/O, no | | |
| | | vendor types) | | [== reconciliation job ==] |
| | +--------------------+ | daily I4 + label sweep |
| | | return journey | | |
| | +--------------------+ | |
| | | adapters: order, | | |
| | | carrier, payments | | |
| | +--------------------+ | |
| +--------------------------+ |
| | ^ |
| v | |
| (-- returns schema --) outbox: label_intent, return_event |
+-----------------------------------------------------------------------+
| | ^
v v | at-least-once
{Order platform} {Payments} {Warehouse events}
The container view's job here is mostly to say what is not there. There is no returns service, no message broker, and no second database. Each absence is an accepted cost recorded in ADR 0001: the team gives up independent scaling and independent release of eligibility, and gains one deployment, one transaction, and no distributed failure modes for a seven-person team with a fixed pilot date. The label worker is a separate process — the smallest boundary that satisfies D1 — precisely so that no network hop is introduced where a transaction will do.
7.3 Module and dependency model
Inside the deployable, the boundaries come straight from the workflow evidence: eligibility owns policy interpretation, the return journey owns customer-visible progress, and adapters own vendor vocabulary. The dependency direction is one-way and enforced in the build rather than in a README, which is what turns D4 from a hope into a checkable property.
| Module | May import | May not import | Why |
|---|---|---|---|
eligibility | nothing in the module tree | return_journey, any adapter, any vendor SDK | D4: a policy change must not require touching an adapter. A pure decision function is also the cheapest thing to test at the 200-case scale A1 produced. |
return_journey | eligibility, adapter ports | adapter implementations, vendor SDKs | Depends on the vocabulary, not the vendor. Lesson 0023's rule applied. |
adapters | ports, vendor SDKs | eligibility | Translation lives at the edge, one direction only. |
api | return_journey, ports | eligibility directly | One entry point per capability; the API cannot bypass the journey's guards. |
ALLOWED = {
"eligibility": set(),
"return_journey": {"eligibility", "ports"},
"adapters": {"ports"},
"api": {"return_journey", "ports"},
"ports": set(),
}
def import_violations(graph: dict[str, set[str]]) -> list[str]:
"""Fitness function for driver D4: the boundary map, enforced in the build."""
violations = []
for module, imports in sorted(graph.items()):
for target in sorted(imports - ALLOWED[module]):
violations.append(f"{module} must not import {target}")
return violations
assert import_violations({"eligibility": {"adapters"}}) == [
"eligibility must not import adapters"
]
assert import_violations({"return_journey": {"eligibility"}}) == []
7.4 Dynamic view · happy path
| # | Interaction | Sync / async | Failure semantics |
|---|---|---|---|
| 1 | Customer submits the return with an Idempotency-Key | sync | A replay returns the same return (E5) |
| 2 | API reads order, delivery date and captured amount from the order platform | sync, 200 ms budget | Timeout ⇒ 503 with retry_after; nothing is written. The customer's request has not been accepted, and we say so. |
| 3 | return_journey calls eligibility.assess(request) | in-process | Cannot fail on I/O — it performs none. That is the point of the seam. |
| 4 | One transaction: insert return, insert eligibility_decision, insert label_intent, insert return_event (outbox) | sync | All four commit or none do. This is the consistency boundary — the reason no message broker appears in the container view. |
| 5 | Response to the customer: accepted, label pending | sync | p95 800 ms (D2) — measured from 1 to 5, excluding everything after |
| 6 | Label worker claims the intent and calls the carrier with the intent id as the client reference | async | At-least-once; the client reference makes the carrier call idempotent (E3) |
| 7 | label_intent updated with carrier_label_id; LabelAttached appended | async | Unique constraint on (return_id) makes a second label impossible to persist even if one is created |
| 8 | Notification sent to the customer | async | Sent from the event, after commit — never inside the transaction (lesson 0025) |
| 9 | Warehouse receipt event consumed; refund requested from Payments | async | Deduplicated on the receipt event id; the refund request carries the return id as its idempotency key |
| 10 | Payments emits settlement; the return moves to refunded | async | Only this event may move the return to refunded (I5) |
7.5 Dynamic view · carrier label timeout
| # | Interaction | State after | Customer sees | Operator sees |
|---|---|---|---|---|
| 1 | Steps 1–5 of the happy path complete normally | awaiting_parcel, label_state=pending | “Accepted. Your label is being prepared.” | — |
| 2 | Worker calls the carrier; the call times out at 5 s | unchanged | unchanged | carrier_call_seconds spikes |
| 3 | Worker retries at 2 s, 8 s, 32 s, then every 2 min, always with the same client reference | unchanged | unchanged | label_pending_age_seconds rises |
| 4 | Carrier error rate passes 2% for 5 minutes | unchanged | unchanged | Alert with error breakdown and runbook link (D5) |
| 5 | The carrier had actually created a label on attempt 1; the retry returns it | label_state=ready, one carrier_label_id | Label available; email sent | duplicate_label_total stays at 0 |
| 5′ | Alternative: the outage lasts 30 minutes. On-call disables label attempts. | intents stay queued; new returns are still accepted and decided | “Accepted. Labels are delayed — we'll email yours.” | Worker stops retrying within one loop iteration (the defect the rehearsal found) |
| 6′ | Carrier recovers; the flag is re-enabled | 99% of queued intents get labels within 15 minutes (D1) | Label email arrives | Backlog drains; alert clears |
| 7′ | Worst case: the outage exceeds 4 hours | Returns older than 4 h with a pending label are referred to agents, who issue labels from the carrier portal manually | Agent contact within the SLA | Manual queue rises — watched against the 50/day guardrail |
Row 7′ is the artifact that most often goes missing. The fallback for a long carrier outage is the old manual process, kept alive on purpose during the pilot, with its own capacity number and its own guardrail. An architecture that has no answer for “the vendor is down for four hours” has quietly promised that this never happens.
Traces to Drivers D1 (worker process, view B), D2 (steps 1–5 budget), D3 (context trust boundaries and data minimisation), D4 (module import rules), D5 (view B rows 4–6′) · constraints (order platform and Payments authoritative) · risk R3 · rollout reversal rehearsal. Method: lessons 0044, 0056, 0059.
8 · State machine and contract review
“Return status” looked like a field in the brief. It is a machine with eleven states, six actors and four rules that must never be violated. Designing it before the schema is what stops return.status = "approved" from being assignable from anywhere in the codebase.
| From | Command | To | Actor | Precondition / invariant | Authoritative owner | Emitted fact | Recovery if wrong |
|---|---|---|---|---|---|---|---|
| — | SubmitReturn | requested | Customer | No other open return for this order line (I1); order line exists and belongs to the actor | Return journey | ReturnRequested | Cancel (below) |
requested | AssessEligibility | accepted | Policy engine | Exactly one decision per return; window and condition rules pass; no fraud hold | Eligibility | ReturnAccepted, LabelIntentRecorded | Reversal is legal only before a label exists |
requested | AssessEligibility | refused | Policy engine | A reason code and human text are mandatory; no refusal without both | Eligibility | ReturnRefused | Appeal (forward transition, not a reversal) |
requested | AssessEligibility | referred | Policy engine | Judgement case or fraud hold; 4-hour agent SLA starts now | Eligibility | ReturnReferred | — |
referred | AgentDecides | accepted / refused | Support agent | Agent identity recorded; if it contradicts a policy rule, an override reason is mandatory | Eligibility | ReturnAccepted / ReturnRefused + PolicyOverridden | — |
refused | Appeal | under_review | Customer via support | One appeal per refusal; new evidence required | Eligibility | AppealOpened | — |
under_review | Uphold / Overturn | refused / accepted | Policy owner | Who upheld or overturned, and why, is recorded | Eligibility | AppealDecided | — |
accepted | AttachLabel | awaiting_parcel | Label worker | Exactly one carrier label per return (I2) — a unique constraint, not a code check | Return journey | LabelAttached | Void the label with the carrier; the return returns to accepted |
awaiting_parcel | RecordReceipt | received | Warehouse clerk (manual, pilot) | The scanned item must match the return; receipt event id deduplicated | Warehouse (external) | ParcelReceived | Receipt reversal ⇒ settlement_disputed (E4) |
awaiting_parcel | Expire | expired | Reconciliation job (time) | 21 days since acceptance with no receipt; customer notified exactly once | Return journey | ReturnExpired | An agent may re-open within 7 days |
received | RequestRefund | refund_requested | Return journey | Never sets settlement; carries the return id as its idempotency key | Return journey | RefundRequested | Retry is safe by construction |
refund_requested | — | refunded | Payments only | I5: no other actor may write this state. Refund ≤ captured amount is enforced by Payments, not by us (I3) | Payments (external) | RefundSettled (consumed) | None. Money has moved; only a new Payments fact can offset it |
refund_requested / refunded | DisputeSettlement | settlement_disputed | Reconciliation job or agent | Receipt reversed, or Payments rejected the request | Return journey | SettlementDisputed | Compensation, decided by a human in Payments |
requested / accepted | Cancel | cancelled | Customer | Only before a parcel is received; any attached label is voided with the carrier | Return journey | ReturnCancelled | Submit a new return |
Two rows in that table carry most of the design. Only Payments may write refunded: the returns module requests and observes, so the invariant “a refund never exceeds the captured amount” lives where the captured amount lives, and our module physically cannot break it. And the refund row has no recovery, which is written down rather than hidden behind the word “rollback”. A code deploy can be reverted; a settled refund cannot. That single admission is what produced the settlement_disputed state and E4's rule that we never auto-compensate.
| Id | Rule | Kind | Where it is enforced | What happens when it fails |
|---|---|---|---|---|
| I1 | An order line has at most one open return. | Hard invariant | Partial unique index on (order_line_id) where state is not terminal, plus a domain guard | Refuse with return_already_open and a link (E5) |
| I2 | A return in awaiting_parcel has exactly one carrier label. | Hard invariant | Unique constraint on label_intent.return_id; the carrier call is idempotent on the intent id | Refuse the second write; alert; daily reconciliation against the carrier's own label list |
| I3 | A refund never exceeds the captured amount. | Hard invariant owned by another system | Payments. We cannot enforce it and do not pretend to. | Payments rejects; we move to settlement_disputed (E4) |
| I4 | Every accepted return reaches a terminal state within 21 days. | Reconciliation condition, not an invariant — it is temporarily false by design | Daily sweep; detection within 24 hours; owner Nils; escalation to Dana after two consecutive days | Referred to an agent with the ageing reason; counted against the manual-queue guardrail |
| I5 | Only Payments may assert settlement. | Ownership rule | No code path in the returns module writes refunded; a fitness function greps for one and fails the build | Build failure, before it can be a production defect |
| — | Only the assignee or an agent with the returns role may decide a referral. | Authorization precondition (not an invariant) | API edge plus a domain guard, deliberately separate so error codes tell the caller which edge they hit | 403 with not_permitted; logged with actor |
8.1 Internal contracts
The seam between eligibility and the return journey is an in-process contract, and it is the one the modifiability driver depends on. It speaks the domain's language, performs no I/O, and returns a value that is complete enough that the caller never has to ask a follow-up question — the definition of a deep module.
- Command
assess(request: ReturnRequest) -> EligibilityDecision- Promise on success
- Exactly one decision, complete: outcome (accepted / refused / referred), a reason code and human text whenever the outcome is not
accepted, thepolicy_versionused, the timestamp, and the deciding actor. Deterministic given the same request and policy version. - Forbidden
- No network or database I/O. No vendor types in the signature or the result. No knowledge of how the decision will be displayed or stored. Enforced by the import rule in 7.3.
- Failure
- There is no failure mode. Missing input is a programming error and raises; there is nothing to retry, because there is nothing to fail. This is the payoff of keeping I/O out.
- Events emitted after commit
ReturnAccepted,ReturnRefused,ReturnReferred— each carrying return id, decision, reason code, policy version, correlation id. Published from the outbox, never inside the transaction.- Proof
- The 200 sampled agent decisions from A1 run as a table-driven test suite against
assess. A policy change that alters an existing sampled outcome fails the build and must be signed off by Anya.
from dataclasses import dataclass
from datetime import date, timedelta
from enum import Enum
class Outcome(Enum):
ACCEPTED = "accepted"
REFUSED = "refused"
REFERRED = "referred"
@dataclass(frozen=True)
class EligibilityDecision:
outcome: Outcome
policy_version: str
reason_code: str | None = None
reason_text: str | None = None
def __post_init__(self) -> None:
# A refusal or referral without a reason is not a decision the
# customer, the agent, or the telemetry can act on.
if self.outcome is not Outcome.ACCEPTED and not self.reason_code:
raise ValueError("a non-acceptance must carry a reason code")
if self.reason_code and not self.reason_text:
raise ValueError("every reason code ships with human text")
def assess(request, policy, holds) -> EligibilityDecision:
"""No I/O: policy and holds are already-loaded values, not clients."""
if holds.fraud_hold_for(request.customer_id):
return EligibilityDecision(
Outcome.REFERRED, policy.version,
"fraud_review", "A specialist is checking this.")
age = request.submitted_on - request.delivered_on
if age > timedelta(days=policy.window_days):
return EligibilityDecision(
Outcome.REFUSED, policy.version,
"outside_return_window",
f"Delivered on {request.delivered_on}, outside the "
f"{policy.window_days}-day window.")
if request.item_condition != "unopened":
return EligibilityDecision(
Outcome.REFERRED, policy.version,
"condition_needs_review", "An agent will review this item.")
return EligibilityDecision(Outcome.ACCEPTED, policy.version)
8.2 External contract
| Question | Designed answer |
|---|---|
| What is the resource? | A return. The eligibility decision is a child resource of it, not a mutable field on it. |
| What names identify it? | POST /v1/returns to create; GET /v1/returns/{id}; POST /v1/returns/{id}:appeal as a custom method that reads like the domain verb. |
| What does success promise? | 201 means: the return exists durably, a decision has been made, and if accepted, exactly one label intent is recorded. It does not promise a label exists — that is label_state, and the distinction is the whole point of ADR 0003. |
| How are retries deduplicated? | A mandatory Idempotency-Key header on every unsafe method. See the key table below. |
| What does a failure mean? | Every error carries code (a stable string), message (human text), and retryable (boolean). 409 means “your intent conflicts with a stored one — do not retry blindly”. 503 carries retry_after. A timeout means unknown: the caller must retry with the same key, never submit afresh. |
| Which edge rejected me? | 400 = schema; 403 = authorization; 422 = a valid request the domain refuses. Collapsing these into 400 is what makes consumers guess. |
| What data crosses? | Request: order line id, item condition, reason. Response: decision, reason text, label state, label URL when ready. The carrier receives recipient name and address only — never the return reason (D3, data minimisation). |
| Change | Breaking? | Rule |
|---|---|---|
| Adding an optional response field | No | Free. Consumers must ignore unknown fields; asserted by a consumer contract test that sends an unknown field and expects success. |
Adding a new reason_code value | No, by construction | reason_code is an open enum. Every code ships with reason_text, and consumers must render the text for codes they do not recognise. This is why the internal contract refuses to construct a code without text. |
Adding a new value to state | Yes | state is a closed enum: consumers branch on it. A new state requires a version bump and a migration window. referred was added in v1 before any external consumer existed — after that, it would not have been free. |
| Making an optional request field required | Yes | New version. Never quietly enforced. |
| Changing what an existing field means | Yes — and the most dangerous kind | New field, new name, expand/contract. Redefining label_state from “we have a label” to “we have asked for one” would pass every schema check and break every consumer. |
| Removing a field | Yes | Expand, migrate readers, verify with consumer contract tests, then contract. Minimum 90-day deprecation window; migration owner named in the deprecation notice. |
| Operation | Key | Scope | TTL | Same key, different body |
|---|---|---|---|---|
POST /v1/returns | Client-supplied Idempotency-Key; the UI derives it from the order line id plus the form session | Per customer | 24 hours | 409 idempotency_key_reuse (E5) |
| Carrier label creation | label_intent.id sent as the carrier's client reference | Per carrier account | Carrier-defined, 7 days | Impossible — the worker never changes an intent |
| Refund request to Payments | return_id | Per merchant | Permanent | Payments rejects; we move to settlement_disputed |
| Warehouse receipt consumption | receipt_event_id, stored in a processed-events table | Per stream | 90 days | Second delivery is a no-op |
Traces to Constraints (order platform and Payments authoritative; no card data) · driver D3 (edges, data minimisation) and D4 (the internal seam) · assumptions A3 and A4 (idempotency and receipt dedup) · risk R1 (fraud hold as a decision, not a hidden refusal) · examples E3, E4, E5 · the workflow's “approved means three things” hotspot. Method: lessons 0045, 0054, 0055, 0057.
9 · Options, spike, and ADR 0003
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. The A3 spike killed it. What follows is the comparison as it was actually written — three genuinely different mechanisms, not one mechanism with three vendors — and the record that replaced the earlier decision.
| Option A · synchronous call | Option B · persist intent, drain from a worker | Option 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; an agent buys it in the carrier portal, as today. |
| Drivers satisfied | Simplest operationally; one state fewer for the customer to understand (helps A2). | D1 (accepts during an outage), D2 (decision path excludes the vendor), D5 (the vendor is separately observable and separately disableable). | D1 and D2 as well; 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 now sees “accepted, label coming” rather than a label. A new process is on-call surface (D5 cost). | Guardrail: one manual label purchase per accepted return — about 10/day at 10% exposure, about 96/day at full exposure — on top of the 34/day of referrals the queue already carries at full exposure. The 50/day manual-queue guardrail breaks somewhere between the 10% and 50% stages. |
| 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 it after launch means changing what the API's 201 promises, which is a contract change. | Medium: the pending state becomes 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: carrier p95/p99 and duplicate behaviour (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 (dynamic view B, row 7′). |
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.
- 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 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, 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. Answer to the question: no, and yes.
- Disposal
- Harness 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, not a one-off number.
- Status
- Accepted · 12 September · decider: Dana (tech lead) · advice from Priya, Marco, Nils, Lea, Sam
- Supersedes
- ADR 0002 (“Create the carrier label inside the request”, accepted 1 September). ADR 0002 remains in the log, unedited, marked superseded — it is a historical fact about what the team believed on 1 September.
- Superseded by
- None. See “Revisit when”.
- Context
- The pilot must accept eligible returns and communicate a decision within 4 hours median, at up to 40 rps at seasonal peak, with a p95 decision path of 800 ms. The carrier is a fixed contractual dependency. The A3 spike measured the carrier 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; 99% of labels within 15 minutes of recovery). D2 performance (800 ms p95 decision, excluding label). D5 operability (disable label attempts within 10 minutes). Guardrail: manual review queue ≤ 50/day. Assumption A2 (customer comprehension of a two-step flow) is not yet settled and is the main cost of this decision. Operational simplicity for a seven-person team.
- Options considered
- A. Synchronous carrier call — rejected: measured p99 exceeds the decision budget by 14×, and a carrier outage would refuse eligible requests, failing D1 outright. C. Agent buys the label — rejected as the everyday mechanism: it adds ~40 manual actions/day at pilot volume and breaks the 50/day guardrail at roughly 30% exposure; retained as the outage fallback. B. Persist intent + worker — chosen.
- Decision
- The API commits the return, the eligibility decision and a
label_intentrow in a single transaction, and returns a decision withlabel_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 onlabel_intent.return_idmakes 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.
- Consequences · 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 accept this cost and pay down the risk with wording work and a retest, not by reversing the decision. (2) A worker process, a backlog, and an ageing metric now exist and must be watched; that is new on-call surface for a team of seven. (3) A pending state is now 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 date (see closeout).
- Consequences · follow-up work
- Reversal rehearsal of the disable path before each exposure stage (Nils). Daily reconciliation against the carrier's label list (Lea). 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; transferred to the steady-state owner 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.” The concern is legitimate and was 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 should say so in the closeout. Outcome at closeout: referrals alone reached 41/day at peak, so option C's extra label purchases would have breached the 50/day guardrail — the dissent was recorded, tested, and not upheld. Sam's second point, that the flag is carrying cost, was upheld: it is on the removal list with a date.
- 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.
| 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 state 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/day; removal trigger stated in the target workflow. |
Traces to Drivers D1, D2, D5 (the forces) · guardrail “manual queue ≤ 50/day” (why C lost) · assumptions A3 (the evidence that changed it) and A2 (the accepted cost) · risk R3 (closed by this decision) · the container view's worker process · rollout stage entry conditions. Method: lessons 0046 and 0059.
10 · Walking skeleton and delivery map
The plan is ordered by what it retires, not by what it builds. The first thing the team produced was not a feature: it was one request travelling the entire real path with almost no behaviour in it, so that every integration surprise arrived in week 2 instead of week 9.
- The sentence
- From the main branch, deploy one internal-user return request through the production pipeline; persist its intent, cross the carrier port with a fake adapter, render pending label on the real status page, and find the complete request by correlation id in the real logging stack.
- Real
- The build, the pipeline, the deployable, the database and its migration, the module boundaries and their import check, the outbox, the worker process, the correlation id, the telemetry pipeline, the status page.
- Fake or hard-coded — stated, not implied
- Eligibility always returns
accepted(no policy at all). The carrier adapter returns a fixed label id after 100 ms. Order lookup returns one fixture order. No notifications. No authorization beyond “is signed in”. Warehouse receipt is a button in an internal page. - What it proved in week 2
- Three things nobody had predicted: the storefront's session cookie was not available to the worker process (so correlation ids broke across the boundary), the migration needed a two-step deploy to avoid locking a hot table, and the platform's log retention was 3 days, not 30 — which the privacy review had assumed. All three were cheap in week 2 and would have been expensive in week 9.
| Milestone | Evidence produced | Exit condition | Retires | Owner · target |
|---|---|---|---|---|
| Path walks | One request deploys, persists, crosses the port, and is traceable end to end from a clean build | Repeatable from a clean checkout; a forced failure is visible in the logging stack within one minute | Integration and deployment risk | Dana · week 2 |
| Policy works | The 200 sampled agent decisions run as tests against assess | Anya accepts the decisions and the refusal reasons on the sample; disagreements are logged as rules, not bugs | Assumption A1 | Sam · week 4 |
| Team uses it | Internal users complete the accessible happy path and both refusal paths on their own devices | No severity-one usability, accessibility or authorization defect open | Assumption A2 (partly), driver D3 | Marco · week 5 |
| Pilot learns | 1% of eligible demand exposed against a matched control; outcome and guardrail measures readable | The pre-agreed continue / change / stop thresholds can actually be evaluated at n ≥ 120 | Assumption A6, risk R1, the outcome itself | Priya · week 6 (29 Sep) |
| Scale earns | Load at 40 rps, carrier recovery drill, support runbook exercised, steady-state owner named | Every rollout gate passes; the disable path has been exercised twice | Drivers D1, D2, D5; operational ownership | Nils · week 9 |
| Dependency | External owner | Exact deliverable | Needed by | Readiness evidence | Fallback | Escalation trigger → who |
|---|---|---|---|---|---|---|
| Carrier sandbox credentials and rate limit | Asha (platform) | Sandbox key with 20 rps, plus the production key held in the vault | Week 2 | A test call succeeded on 26 Aug | Fake adapter behind the real port — the skeleton is designed to proceed without it | Not delivered by Wednesday → platform lead |
| Payments refund API scope | Ravi (payments) | Written confirmation that partial refunds are out of scope for the pilot and that return_id is accepted as the idempotency key | Week 3 | Confirmed in writing, 8 Sep | Refund requests queue for an agent; the pilot still measures the decision outcome | No answer by end of week 3 → engineering director |
| Warehouse receipt event access | Warehouse platform | Read access to the receipt stream plus 30 days of history for the A4 replay | Week 2 | Replay completed 3 Sep | Manual confirmation (which the pilot uses anyway, per ADR 0005) | Access refused → escalate as a scope trade, not a delay |
| Privacy and security review | Ola | Sign-off on data minimisation to the carrier, 30-day retention, and the log-redaction fitness function | Before staff-accounts stage | Draft review 5 Sep; log retention finding raised by the skeleton | None. This is a gate, not a risk. Exposure does not start without it. | Not signed 5 days before the stage → CISO |
| Support staffing for referrals | Nils | Two agents rostered for the 4-hour referral SLA during exposure hours | Week 6 | Roster published 22 Sep | Reduce exposure percentage rather than break the SLA | Roster gap → head of support, same day |
- Fixed
- The 30 September pilot-open date (seasonal). The team of seven. The outcome and its guardrails.
- Movable
- Scope. With a fixed date and a fixed team, scope is the only control surface, and saying so in week 1 is what makes the week-6 trade a decision rather than a crisis.
- Forecast
- Pilot open to 1% between 26 September and 3 October, ~70% confidence, based on four completed slices at a measured 4–7 days each. Not “30 September”: a single date at this range is a claim the evidence does not support.
- Smallest scope trade that preserves the outcome
- Drop the in-product appeal flow to slice 2 and keep “refused, with a reason and a link to contact an agent”. This costs a worse experience for the 11% of requests that are refused and saves about 5 days. It does not touch either outcome measure, which is the test a scope trade has to pass.
- What would not be traded
- The reversal rehearsal, the authorization negative tests, and the matched control cohort. Trading those buys days by removing the ability to know whether the pilot worked.
- Next reforecast
- At the “Team uses it” milestone, or immediately if any dependency's escalation trigger fires — not only when the date becomes impossible.
Traces to Every milestone names the assumption, driver or risk it retires. Outcome (the “Pilot learns” milestone is the first point at which the outcome is measurable) · assumptions A1, A2, A6 · drivers D1, D2, D3, D5 · risk R1 · rollout stage entry conditions. Method: lessons 0047 and 0060.
11 · Team charter and decision rights
Seven people, one outcome, and a fixed date. The charter took 45 minutes to write and settled the arguments that otherwise get re-run every week under time pressure. Its most valuable section is not the cadence — it is the column headed “must seek advice from”, which is what lets a decision be made quickly and be informed.
- Purpose
- Move eligible domestic return resolution from 72 hours to under 4 hours and cut support contacts by a quarter, for signed-in customers with unopened items, with the pilot open by 30 September.
- People and accountabilities
- Priya — product (outcome, scope, cohort). Marco — product design (experience, research, accessibility). Dana — tech lead (technical integrity, architecture, risk). Sam, Lea, Tomas, Ije — engineers (implementation within agreed boundaries; Lea owns the worker, Sam owns eligibility, Tomas owns the API and adapters, Ije owns telemetry and the pipeline). Consulted, not embedded: Anya (policy), Nils (support operations), Ola (privacy and security), Ravi (payments), Asha (platform).
- Missing expertise, named
- Nobody on the team has run a fraud guardrail before. Consultation path: Risk, fortnightly, and Risk holds the stop authority on the fraud guardrail rather than the team.
- Flow
- WIP limit: two slices in progress, full stop. Swarm rule: if a slice has not moved in two days, everyone not on the other slice joins it. Review: every change reviewed, but the tech lead is not a required reviewer — guardrails are published as the import fitness function and the contract tests, so review is distributed rather than queued behind one person.
- Sources of truth — one place each
- Why: this frame. Scope: the story map. Current flow: the delivery board. Decisions: the ADR log. Uncertainty: the risk and dependency map. Evidence: the initiative dashboard. Nothing is duplicated into slides; a status that exists in two places will disagree within a fortnight.
- Cadence
- Daily 10 min (flow and blockers, not status). Twice weekly 30 min (shape the next slice with examples, product + design + engineering together). Weekly 30 min (initiative review — see section 13). Weekly 45 min (demo integrated behaviour). As needed, timeboxed (one architecture decision with the required advisers). Fortnightly 45 min (team health; one owned experiment). Any of these is cancelled for the week if it has no decision to make.
- Escalation triggers — objective, not emotional
- (1) A dependency passes its needed-by date without readiness evidence. (2) A driver cannot be met inside the agreed scope. (3) A decision crosses another team's ownership. (4) Forecast confidence falls below 60%. Each escalation states the decision or resource requested, the options with consequences, and the date by which an answer is needed.
- Health
- No planned weekend work; the fixed date is protected by scope, not by hours. Every module has a second person who has changed it. One team experiment per fortnight; this initiative's was “no ticket may be assigned to the person who wrote it”, kept after two cycles.
| Decision | Final call | Must seek advice from | Recorded in | What reopens it |
|---|---|---|---|---|
| Outcome, scope, cohort, scope trades | Priya | Dana, Marco, Anya, Risk | The frame, versioned | Outcome evidence, or a guardrail that cannot be protected |
| Experience, wording, research method | Marco | Priya, Nils, customers, accessibility | The story map and examples | Comprehension or accessibility evidence |
| Architecture and technical risk | Dana | Engineers, Ola, Asha, Ravi | ADR log | The revisit trigger written into the ADR itself |
| Implementation inside agreed boundaries | The engineer doing it | Peers, and any affected owner | Code and its tests | A fitness function failing |
| Quality bar and definition of done | Whole team | Nils, Ola | The charter and the rollout contract | An escaped defect class, reviewed fortnightly |
| Rollout advance to the next stage | Dana with Nils | Priya, Risk, Ola | The rollout contract | Any stop threshold |
| Rollout stop / reversal | Any on-call engineer, unilaterally | Nobody — advice happens after | Incident log | Deliberately asymmetric: advancing needs agreement, stopping does not |
| Fraud guardrail breach | Risk | Priya, Nils | The rollout contract | — |
| Privacy and security acceptance | Ola | Dana | Review sign-off | A new data flow or a new retention period |
| Steady-state ownership acceptance | Kim (platform services) | Dana, Nils | Closeout | — |
The asymmetry in row 7 is the single most useful line in the table. Advancing exposure requires two named people to agree; stopping it requires one person to act and explain afterwards. A team that needs permission to stop will not stop in time.
Traces to Every decision in this packet has an owner in this table, and the frame's “decision rights” entry points here. Risk R1 (Risk holds the fraud stop authority, not the team) · driver D4 (distributed review is what makes a one-day policy release possible) · rollout gates. Method: lesson 0048.
12 · Test, telemetry and staged rollout
Tests are chosen by risk, not by a coverage quota. Telemetry is paired: a system-health measure that is green proves nothing about whether returns resolve faster. And exposure is staged with a control cohort held back for the whole pilot, because “it improved after we launched” is not a finding.
| Risk or driver | Test | What it proves | Where it runs | Owner |
|---|---|---|---|---|
| A1 — policy correctness | 200 sampled agent decisions as table-driven tests against assess | Every rule and refusal reason matches what an expert would have decided | Unit, every build | Sam |
| D2 — 800 ms p95 | Load test at 40 rps against a production-shaped dataset | The decision path holds its budget under peak load | Pipeline, nightly | Sam |
| D1 — carrier outage | Recovery drill: carrier sandbox forced to 100% timeouts for 30 minutes, then released | Requests are still accepted; 99% of the backlog drains within 15 minutes | Staging, before every exposure stage | Lea |
| E3 — duplicate labels | Contract test with an induced timeout and retry, plus a daily production reconciliation against the carrier's label list | Exactly one carrier label per return, in test and in production | Contract + production job | Lea |
| D3 — cross-customer disclosure | Negative authorization tests on every read path; a log-scanning fitness function that fails the build on an unredacted order id | No identifier leaks through a response, an error, or a log line | Contract + build | Ola |
| D4 — one-day policy release | Import-rule fitness function (7.3); measured lead time on the last three policy changes | The seam is real, and the claim about it is measured rather than asserted | Build + monthly report | Dana |
| Guardrail — accessibility | Automated axe checks in the pipeline, plus manual keyboard and screen-reader passes on the four critical flows, plus one external audit | WCAG 2.2 AA on the flows that matter. Automation alone would have missed two of the three findings. | Pipeline + manual + external | Marco |
| Unknown unknowns | Two exploratory charters per slice: “try to get a label you should not have” and “try to make the system contradict itself about a return” | Behaviour the examples did not imagine. The second charter found the receipt-reversal case that became E4. | Timeboxed, per slice | Whole team |
| D5 — reversal | Reversal rehearsal (below) | The disable path works when a human uses it under time pressure | Staging, before every stage | Nils |
| Measure | Kind | Segmented by | Owner | Decision it can change |
|---|---|---|---|---|
| Median and p90 time from submission to decision communicated | Outcome | Exposed vs control; device; decision outcome | Priya | Continue / change / stop |
| Support-contact rate within 7 days of submission | Outcome | Exposed vs control; accepted / referred / refused | Priya | Continue / change / stop; slice order |
| Fraud loss per 1,000 eligible returns | Guardrail | Exposed vs control | Risk | Stop (Risk's unilateral authority) |
| Manual review queue depth and daily arrivals | Guardrail | Referral cause | Nils | Pause the exposure ramp |
| Referral age; oldest unanswered referral | Guardrail | Cause | Nils | Roster change; pause the ramp |
| Decision-outcome mix (accepted / referred / refused by reason code) | Business state | Reason code; policy version | Anya | Roll back a policy change |
| Eligibility decision p95 | Health, tied to D2 | — | Sam | Block the next stage |
| Carrier call latency and error rate; label backlog age | Health, tied to D1 and D5 | Carrier error code | Lea | Disable label attempts |
| Age of the oldest accepted return with no terminal state | Data integrity, tied to I4 | State | Lea | Escalate to Dana after two days |
| Records older than 30 days in the returns schema | Privacy | — | Ola | Halt exposure; it is a compliance breach, not a bug |
| Accessibility check failures on critical flows | Guardrail | Flow | Marco | Block the release |
Baselines used by the measures above: eligible domestic demand 130 requests/day, rising to about 160/day from mid-November (source: order platform returns extract, rolling 12 months). Decision mix from the A1 sample and the first exposed week: 63% accepted, 26% referred, 11% refused.
| Stage | Cohort | Entry | Observe (minimum) | Advance when | Pause / reverse when · authority |
|---|---|---|---|---|---|
| 1 | Team only | All tests pass; the support path exists; the runbook has a first draft | All flows, 2 days | No critical security or accessibility defect open | Any critical defect · anyone |
| 2 | Staff accounts | Runbook exercised; alerts route to a human; privacy sign-off; reversal rehearsal passed | Errors, retries, comprehension; 50 real cases | Zero duplicate labels; decision p95 within 800 ms | Any duplicate label · Dana |
| 3 | 1% of eligible demand, selected by a stable hash of the customer id, with a 10% holdback control that stays unexposed for the whole pilot | Support briefed; reversal rehearsed again; control cohort instrumented | Outcome and guardrails; 3 days and at least 120 exposed requests | Median resolution improved against control; fraud not worse; queue under 50/day | Fraud loss above control by more than 1.0 per 1,000 · Risk. Contact rate worse than control · Priya. |
| 4 | 10%, then 50% | Previous gate signed off by Dana and Nils | Capacity, referral age, manual queue, segments (device, decision outcome); 3 days each | All stage-3 thresholds still hold at the larger n | Referral SLA missed for 4 hours · Nils. Queue above 45/day · Nils. |
| 5 | 100% of eligible demand, minus the 10% holdback, which is released only after the outcome has held for two consecutive weeks | Steady-state owner named and has accepted the runbook | Continuous; weekly review | Outcome and guardrails hold for two consecutive weeks | Any guardrail breach · the guardrail's owner |
Data and side effects at each reversal: disabling exposure stops new returns entering the new path; returns already in flight continue, because abandoning them would strand customers mid-journey. Labels already bought are not voided. Refunds already requested are not recalled — see E4. Reversal is therefore “stop the intake”, never “undo the week”, and the contract says so rather than leaving the team to discover it.
- Scenario
- The carrier starts returning timeouts at 09:40 on a weekday. Label errors cross 2% within four minutes. The on-call engineer is someone who did not build the worker.
- What the team had to demonstrate
- Detect (alert fires, names the carrier), diagnose (runbook link resolves the carrier's error code), limit (disable label attempts), recover (re-enable, backlog drains), explain (a support message the agents can send).
- Finding, first rehearsal (18 September)
- The disable flag did not do what everyone believed. It was checked when a request created an intent, so it correctly stopped new label attempts — but the worker read the flag once at start-up and kept retrying the 63 intents already in its backlog against a failing carrier. The rehearsal's stopwatch said “disabled in 3 minutes”; the carrier's dashboard said we were still calling it eleven minutes later.
- Fix
- The flag is read inside the worker loop, once per intent. A test asserts that the worker stops within one iteration of the flag flipping. Added to the operability driver's evidence line, and to E3 as an explicit expectation.
- Finding, second rehearsal (22 September)
- Disable worked in 90 seconds. The gap was the support message: nobody could say what a customer with a pending label should be told. Written, put in the runbook, and now part of the stage-2 entry condition.
- Why it is rehearsed every stage, not once
- The disable path is code, and code changes. A rehearsal that happened in September proves nothing about the flag in November.
Traces to Outcome (the exposed-vs-control measures are how the outcome is read at all) · every guardrail (each has a stop threshold and a named authority) · drivers D1, D2, D3, D4, D5 (one evidence mechanism each) · assumption A6 (converted into a guardrail with a control cohort) · risk R1 · ADR 0003 (the disable flag it created is the thing being rehearsed). Method: lessons 0049 and 0060.
13 · Weekly initiative review · week 3
Week 3 is the week the evidence arrived. Three results landed within four days of each other: one changed an architecture decision, one changed the slice order, and one refused to say anything at all. This is the review as it was written — one page, built from live sources, produced in thirty minutes.
Returns resolution · week 3 · 11 September · attending: Priya, Marco, Dana, Sam, Lea, Tomas, Ije · 30 min
- Outcome
- Not yet measurable — no customer is exposed. First reading due at the 1% stage, forecast 26 September to 3 October. Reporting “on track” here would be a claim with no source.
- Guardrails
- One moved: the manual-queue projection is now 34 referrals/day at full exposure (68% of the 50/day cap) against a week-1 estimate of “about 12”. Others unchanged and unmeasured.
- Learned
- (1) The carrier is far slower and less safe under retry than its documentation says (A3, falsified). (2) The referral volume implied by the A1 sample is nearly three times the week-1 estimate. (3) Customer comprehension is still unknown; the n = 5 test was inconclusive, not positive.
- Delivered
- The walking skeleton runs end to end from a clean build, including the worker process and correlation ids across it. The 200-case policy suite runs and 186 of 200 pass; the 14 failures are all gift orders, which is a missing rule, not a defect. Demonstrable this Friday.
- Flow
- Two slices in progress, at the WIP limit. The appeal flow has not moved in three days because Marco is on the comprehension retest — this is the item the swarm rule applies to, and it is the item the scope trade below removes.
- Risk and dependency
- R3 (carrier latency) closes today, replaced by a smaller operational risk: worker backlog age. Payments confirmed
return_idas the refund idempotency key in writing (8 Sep). Privacy review has one open finding — log retention is 3 days, not the 30 the review assumed; Ola is deciding whether that is acceptable for the audit trail. - Decisions
- Two made today (below). One waiting: whether the gift-order rule enters slice 3 or is refused for the pilot — Priya, by 18 September, needs the volume from Anya.
- Forecast
- Pilot open to 1% between 26 September and 3 October, ~70% confidence, down from ~80% last week because the worker is six days of work that was not in last week's plan. Scope trade taken today restores about five of those days.
- Ask
- One decision from Ola by Monday on log retention, because the audit trail's design depends on it. No staffing change requested.
The evidence, and what each piece did
- Result
- p50 640 ms, p95 1.9 s, p99 11.4 s. Two duplicate labels in ten timeout-retries made without a client reference.
- What it falsified
- Assumption A3, and with it ADR 0002, which had said the carrier call belongs inside the request. The 800 ms decision budget (D2) is unreachable at p99 11.4 s, and a 30-minute outage would have refused every eligible request, failing D1.
- Changed decision
- ADR 0002 is superseded by ADR 0003: persist the label intent in the accepting transaction, drain it from a worker, make the carrier call idempotent on the intent id. Signed the following day.
- Dissent, recorded
- Sam: at pilot volume, option C — an agent buys the label from the carrier portal — solves this with a runbook rather than a queue, a backlog metric, a flag and new on-call surface. Not resolved by argument; recorded in the ADR with the condition that would prove it right, and revisited at closeout.
- Artifacts updated the same day
- ADR log (0002 marked superseded, 0003 added). Container view (worker process). Dynamic view B (added). E3 (rewritten around idempotent retry). Delivery map (six days added). Rollout contract (the disable flag and its rehearsal). Risk R3 closed.
- Result
- Applying the A1 sample's 63 / 26 / 11 decision mix to the demand extract gives 34 referrals/day at full exposure, rising to about 41/day in the seasonal ramp. Week-1 planning had assumed “about 12”.
- What it did not falsify
- The guardrail still holds — 41 is under 50. The finding is that the headroom is nine requests a day, not thirty-eight, and that agents will be doing this work from the old helpdesk unless something changes.
- Changed decision
- Slice 2 becomes the agent decision view and the four-hour referral SLA. The in-product appeal flow moves behind it. A refused customer still gets a reason and a link to contact an agent; they do not get a self-service appeal until slice 3. This is exactly the scope trade the forecast pre-identified in week 1, taken now on evidence rather than under deadline pressure.
- Dissent, recorded
- Marco (design). “Refusal is where the contact rate lives. Sending a refused customer to a support agent to appeal is the exact behaviour the outcome measures. We are protecting the referral experience by damaging the refused one.” Decider on scope is Priya, and Priya took the trade. What would prove Marco right: if the contact rate within the refused segment exceeds 55% during the 1% stage, the appeal flow returns to the front of the queue and the exposure ramp pauses until it ships. Instrumented before the stage opens.
- Artifacts updated
- Story map (slice order). Delivery map (five days recovered). Rollout telemetry (contact rate now segmented by decision outcome — a measure added specifically to adjudicate a dissent). Frame unchanged: no outcome or guardrail moved.
- Result
- Two of five participants read “your label is being prepared” as a refusal.
- Honest reading
- Inconclusive. Two of five is a strong-sounding fraction of a sample too small to separate a wording problem from two participants. It is not evidence that the two-step flow fails, and it is not evidence that it works.
- What was done anyway
- The wording changed — “Accepted. We're preparing your label; we'll email it within the hour” — because it is a one-line, fully reversible change that costs nothing if the finding was noise. A retest at n = 12 was commissioned for 15 September. The distinction is worth naming: changing something cheap and reversible on weak evidence is fine; changing something expensive and irreversible on weak evidence is not.
- Why it is in the review at all
- Because an inconclusive result that goes unreported becomes a silently confirmed assumption. A2 remains the largest open value risk in the packet and is on the frame's “next evidence” line.
Three results, three different responses: one decision reversed, one decision re-ordered, one decision deliberately not made. A review in which every piece of evidence conveniently supports the existing plan is a review that is being performed rather than run.
Traces to Assumptions A1, A2, A3 (the three results) · ADR 0002 and 0003 · drivers D1, D2 (the forces that made the spike decisive) · guardrail “manual queue ≤ 50/day” (the volume model) · risk R2 (Marco's dissent is R2 restated with a threshold) · story map slice order · forecast. Method: lesson 0050.
14 · Closeout
The pilot ran from 29 September to 24 November. One outcome measure was met and one was missed; both are reported with the query that produced them. Then the unglamorous part: handing the thing over, removing the machinery that was only ever meant to be temporary, and writing down what the next lead should know.
| Measure | Baseline | Target | Actual | Source | Verdict |
|---|---|---|---|---|---|
| Median time from submission to decision communicated (eligible domestic) | 72 h | under 4 h | 3.1 h exposed; 69 h in the holdback control | Initiative dashboard, resolution_seconds p50, 29 Sep – 24 Nov; exposed n = 6,480, control n = 694. Query in the dashboard repository, re-runnable. |
Met |
| Support-contact rate within 7 days | 41% | 31% or lower (−25% relative) | 31.6% exposed; 40.2% control. A 23% relative reduction. | Same window; contact log joined on return id. Same join as the baseline query, deliberately, so the two numbers are comparable. | Missed by 0.6 points. Reported as a miss, not rounded into a win. |
| Fraud loss per 1,000 eligible returns | 2.4 | 2.4 or lower | 2.2 exposed (14 events in 6,480). Control 2.5 — but see the note. | Risk ledger, 29 Sep – 24 Nov, reconciled 1 December. | Held on the agreed measure, with a caveat we are naming rather than hiding. |
| Manual review queue | —(new) | 50/day or fewer | Mean 26/day; peak 41/day on 11 November | Referral queue arrivals, daily. | Held, with 9/day of headroom at peak |
| WCAG 2.2 AA on the four critical flows | — | No open AA issue | 3 AA issues found at the staff stage, all fixed before the 1% stage; 0 open at the final audit | External audit report, 12 November. Two of the three were found by manual keyboard testing, not by the automated check. | Held |
| Carrier cost per resolved return | $3.42 | $3.42 or lower | $3.39 | Carrier invoice, October and November, divided by resolved returns. Lower because the manual process bought and voided about 3% of its labels; the idempotent path bought none twice. | Held |
| Personal data retained no longer than 30 days | — | Zero records over 30 days | 0 records over 30 days on 24 November | Deletion job audit count, plus an independent query run by Ola on 24 November. | Held |
| Duplicate carrier labels | — | Zero | 0 in 6,480 returns | Daily reconciliation against the carrier's own label list. | Held — the invariant I2 evidence |
The trajectory matters as much as the endpoint. At pilot week 4 the reading was 3.2 hours median and an 18% relative contact reduction; the remaining five points came from two rounds of wording work on the referral and refusal states, not from further speed. That is the finding recorded in the learning review below.
The weakest number in this packet, named. The fraud guardrail's control cohort contains 694 requests. At a base rate near 2.4 per 1,000 that is under two expected events, so the control cannot detect a fraud change of any plausible size — the “2.5 in control” figure is one or two incidents and should not be read as a comparison. What can honestly be said is the absolute exposed figure (2.2 per 1,000 over 6,480 requests) against a baseline computed over 4,812. The guardrail held on the measure the team agreed in advance, and the measure itself was under-powered. That is a design defect in the evidence, not in the system, and it is the first thing the steady-state owner should fix.
| Segment or cost | What happened | Decision |
|---|---|---|
| Guest orders — 29% of eligible demand | Untouched. The headline result covers 71% of the demand it appears to describe, and the packet says so everywhere the outcome is quoted. | Sized; decision due 15 November, taken 18 November: build in Q1 with a separate fraud model. Not folded into this initiative. |
| Refused customers — 11% of requests | Contact rate in the refused segment was 47%, above the 41% overall baseline. Being refused quickly is still being refused. | Marco's week-3 dissent threshold was 55%; 47% is below it, so the appeal flow was not pulled forward — but this is the segment slice 3 exists for, and the number is now on its exit condition. |
| Referred customers — 26% of requests | Contact rate 38%, and median resolution 3.7 h — inside the 4-hour SLA but the segment carrying almost all of the variance. | Kept. The referral state is what made the automatic path safe enough to expose. |
| Mobile customers | Completion was 11 points below desktop in the first exposed week; 4 points below by week 8 after two rounds of wording and layout work. | Closed as a gap, retained as a monitored segment. |
| Support operations | Front-line contact volume fell; specialist referral work rose to 26/day mean. Work moved rather than disappearing, and Nils' roster carries it. | Two agents rostered permanently for referrals; agreed with head of support at handover. |
| Asset | From | To | Evidence of readiness | Accepted |
|---|---|---|---|---|
| Service, on-call, alerts, dashboards | Dana | Kim (platform services) | Kim's team ran the carrier-outage drill unaided on 20 November and disabled labels in 80 seconds | 21 Nov |
| Referral queue, SLA, support runbook and scripts | Nils (initiative) | Nils (steady state), two rostered agents | Roster published; SLA met on 97% of referrals in the final fortnight | 24 Nov |
| Eligibility policy content and the 200-case regression suite | Sam | Anya, with Kim's team executing changes | Anya released one policy change unaided on 17 November; elapsed time 4 hours (driver D4's claim, measured) | 18 Nov |
| Carrier vendor relationship and the sandbox key | Dana | Asha (platform) | Key rotated into the platform vault; rotation runbook tested | 21 Nov |
| Outcome dashboard and the holdback experiment | Priya | Analytics, with Priya as reader | Queries in the dashboard repository; the holdback released 24 November | 24 Nov |
| Privacy: retention job, log redaction fitness function | Ola | Kim, with Ola auditing quarterly | Independent retention query run at handover | 24 Nov |
| Item | Disposition | Owner · date |
|---|---|---|
Release flag returns_new_path | Deleted, with its fallback branch and both configurations' tests | Tomas · 26 Nov |
| Exposure percentage and holdback machinery | Deleted. A holdback nobody is reading is a cohort of customers receiving a worse product for no reason. | Ije · 26 Nov |
| Label-attempt disable flag | Kept — it is an operational control, not release machinery. Moved into the runbook, its off-state tested quarterly. | Kim · standing |
| Elevated database access for three engineers | Revoked | Asha · 25 Nov |
| Pilot dashboards and the staff-account test cohort | Deleted; the four steady-state panels moved to Kim's dashboard | Ije · 28 Nov |
| Manual warehouse receipt confirmation (ADR 0005) | Debt, recorded. The removal trigger — 14 consecutive days under 0.1% late receipt events — was met on 9 November, but automating it was not done. Interest: about 40 clerk actions/day, roughly 1.5 hours of a person's day, indefinitely. Trigger to pay it down: the next returns initiative, or the first week the clerk's queue delays a refund past 24 hours. Owner: Kim. Reviewed quarterly. | Kim · reviewed Feb |
| The old email-based return path | Kept deliberately as the carrier-outage fallback (dynamic view B, row 7′). Documented as a fallback rather than left running as a forgotten alternative. | Nils · standing |
- ADR 0002 remains superseded, not deleted. A newcomer can still read what the team believed on 1 September and why it stopped being true.
- ADR 0005 (manual receipt) is not superseded, because the decision still stands — the debt entry above records that its removal trigger fired and was not acted on. Superseding it would have hidden the debt.
- Context and container views updated to v4 (worker moved to Kim's ownership; the fake adapter removed from the legend).
- All four fitness functions remain in the build after handover: import rules, log redaction, the “nothing writes
refunded” check, and the 200-case policy suite. They are the part of this packet that keeps working when everyone who wrote it has moved on. - The compatibility policy for the returns API is now published to the two internal consumers that appeared during the pilot. It was written in week 4, before there were any consumers, which is the only time it is cheap to write.
| Expected | Observed | Surprised |
|---|---|---|
| Speed would drive the contact reduction. | It did not, alone. The contact drop tracked proactive status more than raw speed — the referred segment resolves in 3.7 h and still contacts at 38%. | Telling people what is happening is worth more than shaving hours. Would have changed the slice order in week 1. |
| Policy would be about 90% codifiable. | 63% in pilot scope. The gap was not exceptions but undocumented rules held by two people. | The sample cost two analyst-days and reshaped the outcome statement. It should have been the first thing done, not the third. |
| The carrier would be the easy integration. | It was the decision that reshaped the architecture. | Vendor documentation was wrong by an order of magnitude at p99, and unsafe under retry. Measure vendors before designing around them. |
| The disable flag would work because it existed. | It half-worked, and only a rehearsal revealed it. | An untested control is a belief. The rehearsal cost 30 minutes and found a defect that would have been discovered at 02:00 during a real outage. |
Two improvements owned, not twelve. (1) Every future initiative buys a domain sample before writing an outcome statement — owner Priya, applied to the guest-order initiative in Q1. (2) Every guardrail gets a power check at design time: if the agreed measure cannot detect the change it is meant to catch, it is not a guardrail — owner Risk, due before the Q1 initiative's first exposure.
- Decision
- Hand over and continue in a new initiative. The returns capability moves to steady-state ownership as of 24 November. Slice 3 (opened and damaged items) and guest orders become a separate Q1 initiative with its own frame, because they need a different fraud model and a different cohort — not because they are “phase two”.
- Why not simply continue
- The opportunity that remains is a different opportunity. Continuing under this frame would have let the outcome statement drift to cover work it was never evidence for, which is how initiatives become permanent programmes with unfalsifiable goals.
- Why not stop
- Both measures moved substantially against a held-back control, and every guardrail held. The contact-rate miss is 0.6 points on a measure that moved 9.4 points; it is a miss worth reporting, not a reason to reverse.
- What the next lead is told
- Read ADR 0003 before touching the label path. The fraud guardrail is under-powered — fix the measure before you widen the cohort. The manual receipt debt has a trigger that has already fired. And the outcome statement covers 71% of demand: quote it with that clause attached or you will mislead someone.
Traces to The frame's outcome and every guardrail, each reported against its own baseline and source · assumptions A1, A2, A3, A6 (what each turned out to be worth) · ADR 0003's accepted costs and Sam's dissent, both revisited · the week-3 dissent from Marco, adjudicated against its own threshold · the rollout contract's flags and cohorts, now removed. Method: lesson 0050.
The traceability line
The test of the packet is not its length. It is whether one line runs through all fourteen artifacts without breaking. Here is that line, read end to end for a single thread:
Outcome “median under 4 hours” → assumption A3 “the carrier is fast enough to sit inside the request” → target workflow puts the label after the decision → pilot slice includes a label the customer can actually use → example E3 pins what a timeout means → drivers D1 and D2 give it a number and a failure condition → container view gains a worker process and dynamic view B → state table makes “exactly one label per return” a constraint, not a convention → ADR 0003 chooses the mechanism, records Sam's dissent and its accepted costs → delivery map puts the carrier port in the walking skeleton in week 2 → decision rights say Dana decides it and any on-call engineer may stop it → rollout rehearses the disable path and finds it broken → week-3 review is where the evidence arrived and the decision changed → closeout reports zero duplicate labels in 6,480 returns, hands the worker to Kim, and revisits the dissent.
Now try it on your own packet. Pick your riskiest assumption and walk it forward. The first place the line breaks is either work nobody needed or a risk nobody is carrying.
- Which artifact in this packet would you have skipped, and which decision would you then have made by instinct?
- Two decisions changed under evidence and two dissents were recorded. In your last initiative, where is that written down?
- The contact-rate target was missed by 0.6 points and reported as a miss. What would have happened to that number in your organisation?
Next: the same initiative, done weakly — realistic, plausible, and approved in a great many companies. Then the reviewer notes, which put the two side by side and name the rubric dimension each difference touches.