<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Barking Iguana</title>
  <link href="http://barkingiguana.com/atom.xml" rel="self"/>
  <link href="http://barkingiguana.com/"/>
  <updated>2026-06-09T08:16:22+08:00</updated>
  <id>http://barkingiguana.com/</id>
  <author>
    <name>Craig R Webster</name>
    <email>craig@barkingiguana.com</email>
  </author>
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  <entry>
    <title>Domain-Driven Design: Drawing the Boundaries</title>
    <link href="/writing/domain-driven-design-drawing-the-boundaries/"/>
    <updated>2026-06-09T06:00:00+08:00</updated>
    <id>/writing/domain-driven-design-drawing-the-boundaries/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/drawing-the-lines/&quot;&gt;Drawing the Lines&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Greenbox delivers weekly produce boxes from local farms. With 2,500 subscribers and a team growing from five to twelve, the startup is expanding to Melbourne, and the codebase that was built fast for a small team is starting to crack under the weight.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maya is chopping sweet potato when her phone rings. She’s at home in Fremantle, Saturday evening, Nadia’s Spotify playlist filling the kitchen. Nadia is making a dressing at the counter. The number on the screen is Dave Morrison’s.&lt;/p&gt;

&lt;p&gt;Dave doesn’t call on weekends. He doesn’t call much at all, he’s a text message man, and even those are sparse. Three words. “Zucchini looks short.” Maya puts down the knife and answers.&lt;/p&gt;

&lt;p&gt;“Maya. That Freshly mob rang me today.”&lt;/p&gt;

&lt;p&gt;He says it the way he says everything: flat, unhurried, like he’s reporting rainfall. Maya leans against the counter. Nadia glances over.&lt;/p&gt;

&lt;p&gt;“They’re offering guaranteed volume. A hundred crates a week. That’s more than you take from me in a month.”&lt;/p&gt;

&lt;p&gt;Maya’s mouth goes dry. “Are you going to switch?”&lt;/p&gt;

&lt;p&gt;A pause. Dave doesn’t rush pauses. “I didn’t say that. I said they called. Thought you should know.”&lt;/p&gt;

&lt;p&gt;They talk for another two minutes. Dave mentions that Rachel got the same call. He says “goodnight, Maya” and hangs up.&lt;/p&gt;

&lt;p&gt;Maya puts the phone face-down on the counter. Nadia has stopped whisking.&lt;/p&gt;

&lt;p&gt;“What happened?”&lt;/p&gt;

&lt;p&gt;“The thing I was afraid of.”&lt;/p&gt;

&lt;p&gt;She tells Nadia about Freshly — the $12 million in funding, the guaranteed volume they’re dangling in front of the farms Greenbox depends on. A phone call to Dave is different from a competitor entering a market. That’s someone reaching for the thing she built.&lt;/p&gt;

&lt;p&gt;The sweet potato burns slightly while they talk. They eat it anyway.&lt;/p&gt;

&lt;h3 id=&quot;the-47-file-pr&quot;&gt;The 47-file PR&lt;/h3&gt;

&lt;p&gt;Greenbox has two thousand five hundred subscribers. The team is growing from five to twelve. They’re opening operations in Melbourne. And the codebase that Tom and Priya built for 200 subscribers is groaning under the weight.&lt;/p&gt;

&lt;p&gt;Charlotte is now the team’s scaling coach. She’s spent fifteen years with subscription businesses and she’s seen what happens when a startup codebase meets rapid growth. Lee is still around for discovery foundations. But the problems now are different — not “we don’t understand the domain” but “the architecture can’t keep up.”&lt;/p&gt;

&lt;h3 id=&quot;the-pull-request-that-said-everything&quot;&gt;The pull request that said everything&lt;/h3&gt;

&lt;p&gt;Kai joins the team on a Monday. He’s twenty-eight, from Sydney, five years at a fintech company where he built payment systems handling half a billion dollars a year. Solid Go skills, comfortable with LLMs, and he carries the quiet confidence of someone who has never worked on a codebase he couldn’t master in a week.&lt;/p&gt;

&lt;p&gt;With Kai joining, Tom realises the deploy script doesn’t scale — two developers deploying simultaneously caused a conflict last week. Tom sets up a basic CI/CD pipeline: tests run automatically, deploys go through a single pipeline instead of individual laptops. Priya’s GitHub Action from the BDD work evolves into a real pipeline.&lt;/p&gt;

&lt;p&gt;Kai reads the codebase over two days. On Wednesday, he opens his &lt;label for=&quot;sn-writing-domain-driven-design-drawing-the-boundaries-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-domain-driven-design-drawing-the-boundaries-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-domain-driven-design-drawing-the-boundaries-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-domain-driven-design-drawing-the-boundaries-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; and prompts: “Add a gift subscription feature to this codebase. A customer should be able to buy a subscription as a gift for someone else.”&lt;/p&gt;

&lt;p&gt;The LLM generates code. Kai reviews it, tweaks a few things, writes tests, and opens a pull request on Thursday afternoon.&lt;/p&gt;

&lt;p&gt;The PR touches 47 files.&lt;/p&gt;

&lt;p&gt;Across every part of the system. The subscription model, the payment processing, the delivery scheduling, the farm matching algorithm, the email templates, the customer portal. The gift subscription feature reaches into every corner because the codebase has no corners. It’s one big room.&lt;/p&gt;

&lt;p&gt;One of the changes modifies the farm matching algorithm — it assumes supply is reliable enough to serve gift recipients on the same schedule as regular subscribers. Dave Morrison, whose zucchini yield over-promises by twenty percent every spring, would have something to say about that. But Dave isn’t in the code review.&lt;/p&gt;

&lt;p&gt;Tom reviews the PR and his heart sinks. Not because the code is bad — some of the function signatures are cleaner than his own. But every change is tangled with everything else. Changing gift billing requires touching the same files as regular billing. The delivery changes affect all subscribers. The farm matching modifications could break Maya’s substitution logic.&lt;/p&gt;

&lt;p&gt;“I can’t review this,” Tom tells Kai honestly. “Not because it’s wrong. Because I can’t tell what it’ll break.”&lt;/p&gt;

&lt;p&gt;Charlotte pulls up the PR. “This is a symptom, not a bug.”&lt;/p&gt;

&lt;h3 id=&quot;what-charlotte-sees&quot;&gt;What Charlotte sees&lt;/h3&gt;

&lt;p&gt;She asks the team: “When you say ‘subscription,’ what do you mean?”&lt;/p&gt;

&lt;p&gt;Tom: “The record that tracks what box someone gets and when they’re billed.”&lt;/p&gt;

&lt;p&gt;Priya: “The relationship between a customer and their delivery schedule.”&lt;/p&gt;

&lt;p&gt;Sam: “The thing a customer signs up for and can pause or cancel.”&lt;/p&gt;

&lt;p&gt;Maya: “The commitment to receive a box every week.”&lt;/p&gt;

&lt;p&gt;Four people. Four definitions. None wrong. All different.&lt;/p&gt;

&lt;p&gt;“That’s your problem. Not four definitions — four definitions living in one codebase with no boundaries. When Kai asked the LLM to add gift subscriptions, the LLM did what the codebase told it to: spread the feature across everything, because everything is connected to everything.”&lt;/p&gt;

&lt;h3 id=&quot;bounded-contexts&quot;&gt;Bounded Contexts&lt;/h3&gt;

&lt;p&gt;Charlotte introduces Domain-Driven Design — specifically, Eric Evans’ concept of Bounded Contexts. Complex systems should be divided into distinct areas, each with its own clear language and boundaries.&lt;/p&gt;

&lt;p&gt;“Subscription” in the billing context means “a recurring charge.” In the fulfilment context, it means “a delivery schedule.” In the customer context, it means “a thing I signed up for.” These aren’t contradictions. They’re different perspectives that belong in different parts of the code.&lt;/p&gt;

&lt;p&gt;Charlotte pulls up the Event Storm photographs from months ago — Maya had them laminated, which Charlotte says is one of the smartest things she’s seen a founder do. “We’re going to run the next level up from what you did with Lee,” she says. “Lee got you a Process Level model — the flow, the events, the commands, the actors. Today we’re going to Event Storm an Architecture on top of it. Same wall, same sticky notes, but we’re looking for the code boundaries instead of the business logic.”&lt;/p&gt;

&lt;p&gt;She copies the domain events onto the whiteboard and asks the team to help her find the boundaries.&lt;/p&gt;

&lt;p&gt;“Look for three things,” she says. “First: where the language changes. When ‘subscription’ stops meaning the same thing to different people, that’s a boundary. Second: where the people change. The person who cares about supply matching is not the same person who cares about billing. Different stakeholders, different contexts. Third: where the rate of change differs. Billing changes when Stripe changes their API. Fulfilment changes when you add a city. If two areas change for different reasons, they probably belong in different contexts.”&lt;/p&gt;

&lt;p&gt;The team clusters the events on the whiteboard. Tom moves “Payment Charged” next to “Invoice Generated”, they’re both about money. Priya groups “Farm Availability Submitted” with “Substitution Applied”, they’re both about what goes in the box. Sam pulls “Box Packed” and “Delivery Confirmed” together, those are her world.&lt;/p&gt;

&lt;p&gt;Charlotte watches and asks questions. “Who cares when a payment fails?” Sam says billing. “Who cares when a box is packed?” Sam says logistics. “Who cares when a subscription is paused?” Everyone hesitates, it affects billing AND delivery. Charlotte marks it with a pink note: “Pause is a boundary event. It starts in one context and triggers work in others.”&lt;/p&gt;

&lt;p&gt;After twenty minutes, four clusters have emerged. Not because Charlotte drew them, because the team found them by looking at who cares about what and when the language shifts:&lt;/p&gt;

&lt;div style=&quot;display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-md); margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;background: rgba(33, 150, 243, 0.06); border: 2px solid var(--color-rule); border-radius: 6px; padding: var(--space-md);&quot;&gt;
    &lt;div style=&quot;font-weight: 600; margin-bottom: var(--space-sm); color: var(--color-accent);&quot;&gt;Subscription Context&lt;/div&gt;
    &lt;ul style=&quot;margin: 0; padding-left: 1.2em; color: var(--color-ink-secondary); font-size: 0.9em;&quot;&gt;
      &lt;li&gt;Customer Subscribed&lt;/li&gt;
      &lt;li&gt;Subscription Paused&lt;/li&gt;
      &lt;li&gt;Subscription Cancelled&lt;/li&gt;
      &lt;li&gt;Gift Subscription Created&lt;/li&gt;
      &lt;li&gt;Box Size Changed&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background: rgba(255, 152, 0, 0.06); border: 2px solid var(--color-rule); border-radius: 6px; padding: var(--space-md);&quot;&gt;
    &lt;div style=&quot;font-weight: 600; margin-bottom: var(--space-sm); color: var(--color-accent);&quot;&gt;Billing Context&lt;/div&gt;
    &lt;ul style=&quot;margin: 0; padding-left: 1.2em; color: var(--color-ink-secondary); font-size: 0.9em;&quot;&gt;
      &lt;li&gt;Payment Charged&lt;/li&gt;
      &lt;li&gt;Payment Failed&lt;/li&gt;
      &lt;li&gt;Invoice Generated&lt;/li&gt;
      &lt;li&gt;Refund Issued&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background: rgba(76, 175, 80, 0.06); border: 2px solid var(--color-rule); border-radius: 6px; padding: var(--space-md);&quot;&gt;
    &lt;div style=&quot;font-weight: 600; margin-bottom: var(--space-sm); color: var(--color-accent);&quot;&gt;Supply Matching Context&lt;/div&gt;
    &lt;ul style=&quot;margin: 0; padding-left: 1.2em; color: var(--color-ink-secondary); font-size: 0.9em;&quot;&gt;
      &lt;li&gt;Farm Availability Submitted&lt;/li&gt;
      &lt;li&gt;Supply Matched to Demand&lt;/li&gt;
      &lt;li&gt;Substitution Applied&lt;/li&gt;
      &lt;li&gt;Shortfall Detected&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background: rgba(156, 39, 176, 0.06); border: 2px solid var(--color-rule); border-radius: 6px; padding: var(--space-md);&quot;&gt;
    &lt;div style=&quot;font-weight: 600; margin-bottom: var(--space-sm); color: var(--color-accent);&quot;&gt;Fulfilment Context&lt;/div&gt;
    &lt;ul style=&quot;margin: 0; padding-left: 1.2em; color: var(--color-ink-secondary); font-size: 0.9em;&quot;&gt;
      &lt;li&gt;Box Packed&lt;/li&gt;
      &lt;li&gt;Box Dispatched&lt;/li&gt;
      &lt;li&gt;Delivery Confirmed&lt;/li&gt;
      &lt;li&gt;Delivery Failed&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Four bounded contexts. Each talks to the others through events and clearly defined interfaces. Inside each context, the code is self-contained. You can change billing without touching fulfilment.&lt;/p&gt;

&lt;h3 id=&quot;the-reprompt&quot;&gt;The reprompt&lt;/h3&gt;

&lt;p&gt;Charlotte has Kai try again. This time with a bounded &lt;label for=&quot;sn-writing-domain-driven-design-drawing-the-boundaries-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-domain-driven-design-drawing-the-boundaries-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-domain-driven-design-drawing-the-boundaries-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-domain-driven-design-drawing-the-boundaries-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Add a gift subscription to the Subscription context. A gift subscription is created by a purchasing customer for a recipient. It has a status (pending, activated, active, paused, cancelled), a box size, a purchaser reference, and a recipient email. When created, publish a GiftSubscriptionCreated event. When activated, publish GiftSubscriptionActivated. The Subscription context does not handle billing, delivery, or supply matching.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The LLM generates code. The PR touches 8 files. The codebase is still one big room, nothing has been carved into packages yet, but the changes cluster: the subscription model, its status transitions, the gift fields, the two new events. Nothing reaches into billing, delivery, or farm matching.&lt;/p&gt;

&lt;p&gt;Eight instead of forty-seven. Tom can review it in twenty minutes.&lt;/p&gt;

&lt;p&gt;“The boundary didn’t just organise the code,” Charlotte says. “It organised the conversation with the LLM. We haven’t moved a single line yet, we just told it where the line is going to be.”&lt;/p&gt;

&lt;h3 id=&quot;the-context-map&quot;&gt;The Context Map&lt;/h3&gt;

&lt;p&gt;The bounded contexts communicate through events. Charlotte draws a Context Map showing what flows between them:&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 6px; padding: var(--space-md); margin: var(--space-md) 0; overflow-x: auto;&quot;&gt;
  &lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 0.9em;&quot;&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align: left; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); color: var(--color-ink-tertiary);&quot;&gt;From&lt;/th&gt;
        &lt;th style=&quot;text-align: left; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); color: var(--color-ink-tertiary);&quot;&gt;To&lt;/th&gt;
        &lt;th style=&quot;text-align: left; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); color: var(--color-ink-tertiary);&quot;&gt;Events&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;&lt;strong&gt;Subscription&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Billing&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;SubscriptionCreated, SubscriptionPaused, SubscriptionCancelled&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;&lt;strong&gt;Subscription&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Supply Matching&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;SubscriptionCreated, SubscriptionCancelled&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;&lt;strong&gt;Supply Matching&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Fulfilment&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;BoxAllocated, SubstitutionApplied&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm);&quot;&gt;&lt;strong&gt;Billing&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm);&quot;&gt;Fulfilment&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm);&quot;&gt;PaymentConfirmed&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;This loose coupling is what the team is aiming for. Once the contexts are real in the code, Kai can build gift subscriptions while Priya works on Melbourne delivery zones, and their changes won’t collide.&lt;/p&gt;

&lt;h3 id=&quot;toms-resistance&quot;&gt;Tom’s resistance&lt;/h3&gt;

&lt;p&gt;Tom pushes back. “This feels like Java-enterprise-architect nonsense. We’re a startup. We have twelve people, not twelve hundred.”&lt;/p&gt;

&lt;p&gt;Charlotte doesn’t dismiss him. “You’re right about the ceremony. DDD has a reputation for being over-engineered. But look at Kai’s PR. Could you review it?”&lt;/p&gt;

&lt;p&gt;“No.”&lt;/p&gt;

&lt;p&gt;“Could you be confident it wouldn’t break billing?”&lt;/p&gt;

&lt;p&gt;“No.”&lt;/p&gt;

&lt;p&gt;“That’s the problem DDD solves at your scale. Not coordinating a thousand developers. Being able to change one thing without breaking everything else.”&lt;/p&gt;

&lt;p&gt;She shows him the numbers from the teams she’s coached through this. Average PR size before boundaries: 23 files. After: 9 files. Review time dropped by more than half.&lt;/p&gt;

&lt;p&gt;Tom looks at the data. “Fine. But if I ever have to write a UML diagram, I’m quitting.”&lt;/p&gt;

&lt;p&gt;“Deal.”&lt;/p&gt;

&lt;p&gt;That evening, Tom sits in his home office after the kids are asleep. Three monitors, the framed print of his first merged PR, LEGOs on the floor. Sarah comes in with tea.&lt;/p&gt;

&lt;p&gt;“You’re quiet tonight.”&lt;/p&gt;

&lt;p&gt;“Charlotte wants to carve up the codebase. Draw boundaries.”&lt;/p&gt;

&lt;p&gt;“Is she right?”&lt;/p&gt;

&lt;p&gt;Tom looks at Kai’s 47-file diff, still open on his centre monitor. “Yeah. Probably. It’s just –” He picks up a LEGO brick. “I built this. All of it. And now someone’s telling me it needs walls.”&lt;/p&gt;

&lt;p&gt;Sarah leans against the door frame. “You love making things. But you hate letting anyone help you make them. You’re like your dad.”&lt;/p&gt;

&lt;p&gt;Tom’s jaw tightens. His father runs a construction company. Marco, Tom’s brother, works there. Every family dinner, Marco talks about the business and Tom’s dad listens like it matters.&lt;/p&gt;

&lt;p&gt;“That’s not fair,” Tom says.&lt;/p&gt;

&lt;p&gt;“It’s not a criticism. The codebase isn’t yours any more, Tom. It’s theirs. That’s what growing means.”&lt;/p&gt;

&lt;p&gt;She leaves the tea and goes to bed. Tom stares at the monitor for another hour.&lt;/p&gt;

&lt;h3 id=&quot;the-boundaries-that-dont-stick&quot;&gt;The boundaries that don’t stick&lt;/h3&gt;

&lt;p&gt;Two weeks later, Kai opens another PR for the gift activation flow — what happens when a recipient clicks the link, creates an account, starts receiving boxes. The PR touches three bounded contexts.&lt;/p&gt;

&lt;p&gt;Tom says, to nobody in particular: “I told you it wasn’t that simple.”&lt;/p&gt;

&lt;p&gt;Charlotte doesn’t defend the diagram. She studies the event flows. The gift activation genuinely requires coordination between subscriptions, billing, and fulfilment. The feature isn’t violating the boundaries — the boundaries were drawn in the wrong place.&lt;/p&gt;

&lt;p&gt;“You’re right,” she says to Tom. “The boundaries I drew were a first hypothesis. Let’s redraw them.”&lt;/p&gt;

&lt;p&gt;The Subscription and Billing contexts share too many events. They merge into a single “Commercial” context — subscriptions, billing, gifts, pausing. Supply Matching and Fulfilment stay separate.&lt;/p&gt;

&lt;p&gt;Tom watches Charlotte erase her own lines and draw new ones. He’d expected her to defend the original design.&lt;/p&gt;

&lt;p&gt;“DDD is iterative,” Charlotte says. “The first set of boundaries is always wrong. You find out where by building against them. Kai’s PR told us something about the domain that the workshop didn’t.”&lt;/p&gt;

&lt;p&gt;Kai looks at the new map. “So the 47-file PR was useful after all.”&lt;/p&gt;

&lt;p&gt;Charlotte smiles. “The most expensive domain discovery session Greenbox ever ran. But yes.”&lt;/p&gt;

&lt;h3 id=&quot;making-it-real&quot;&gt;Making it real&lt;/h3&gt;

&lt;p&gt;The team refactors incrementally — Charlotte is adamant about no big-bang rewrites. They start with Billing (already somewhat isolated because of the Stripe API), then Supply Matching, then Fulfilment. Three weeks. Not perfect — some leaky abstractions remain — but the major boundaries are drawn.&lt;/p&gt;

&lt;p&gt;New joiners can now be pointed at a single context: “You’re working on Supply Matching. Here’s the package. Here are the events. You don’t need to understand Billing to be productive.” Onboarding drops from two weeks to days.&lt;/p&gt;

&lt;p&gt;A month later, when Maya announces a corporate catering service — weekly fruit boxes for offices — the bounded contexts prove their worth. Each context changes independently. Nobody’s PR touches 47 files.&lt;/p&gt;

&lt;p&gt;The database needs splitting too. Tom’s first migration takes the site down for twenty minutes on a Sunday. Three subscribers email Sam. Tom resolves to learn zero-downtime migrations. The next migration, weeks later, goes live without anyone noticing. Progress.&lt;/p&gt;

&lt;h3 id=&quot;what-comes-next&quot;&gt;What comes next&lt;/h3&gt;

&lt;p&gt;The boundaries are drawn. The team agrees on the contexts. But the wall is about to be painted over, and new joiners can’t read the sticky notes from three cities away. Next: turning the wall into living diagrams.&lt;/p&gt;

&lt;p style=&quot;margin-top: var(--space-md); font-size: 0.88rem; color: var(--color-ink-tertiary); font-style: italic;&quot;&gt;The next chapter, Drawing the System: From Event Storm to C4, publishes around 16 Jun.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Designing Short-Term and Long-Term Memory for a Bedrock Chat Assistant</title>
    <link href="/writing/designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant/"/>
    <updated>2026-06-08T06:00:00+08:00</updated>
    <id>/writing/designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;Generative AI Developer Professional&lt;/strong&gt; · AIP-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A product team is building an AI support assistant for a mid-sized SaaS company. The assistant handles first-line queries, billing, account access, feature questions, refund requests, and escalates to human agents when it can’t. Measured over six weeks of closed beta:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Average conversation length: 15 turns, ranging from five to past thirty.&lt;/li&gt;
  &lt;li&gt;Return rate: 40% within 30 days. Median return gap eleven days; roughly half reference something from a previous thread, &lt;em&gt;“did the refund you mentioned go through?”&lt;/em&gt;, &lt;em&gt;“I’m still seeing the login error you helped me with last week”&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;label for=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-tool-use&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-tool-use-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Tool use&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-tool-use&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-tool-use-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Tool use&lt;/span&gt;Letting an LLM call structured functions you’ve defined – search, calculator, database query, API call – instead of trying to do everything in text.
&lt;/span&gt;: three or four per conversation. Account lookups, subscription checks, ticket creation.&lt;/li&gt;
  &lt;li&gt;Platform: Bedrock. Nothing self-hosted.&lt;/li&gt;
  &lt;li&gt;Team: two backend engineers, one front-end, no dedicated ML-ops.&lt;/li&gt;
  &lt;li&gt;Compliance: GDPR. Conversation content is personal data; deletion-on-request has to be clean, retention has to be bounded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;“Memory” is two problems, not one. The first is keeping a single conversation coherent: turn fifteen has to know what happened at turn two. The second is recognising a returning user: someone who comes back eleven days later should land on a bot that already knows about their open refund, not one that asks them to retype it. Build both with one mechanism and you usually get one that does neither well, because the two pull in different directions. In-conversation memory has to be right on every turn and fails loudly when it isn’t, which makes it backend plumbing. Cross-visit memory can be approximate, but it has two failure modes that are worse than approximate, which makes it product policy with engineering behind it.&lt;/p&gt;

&lt;p&gt;Those two cross-visit failures are worth naming, because they set the privacy bar. Surfacing someone else’s conversation as if it were this user’s is a wrongful-disclosure incident: a stranger’s refund thread pulled up against this user’s login question. Failing to surface this user’s own open refund when they ask about it is milder, a trust dent rather than a breach, but still a product bug. Avoiding the first means per-user isolation has to be airtight, and it can’t be talked out of place by &lt;label for=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-prompt-injection&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-prompt-injection-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt injection&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-prompt-injection&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-prompt-injection-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt injection&lt;/span&gt;An attack where untrusted text the model is processing tries to override the instructions you actually gave it.
&lt;/span&gt;. Avoiding the second means retrieval has to work on short, fragmented conversation text, which is exactly what document-retrieval tooling is bad at.&lt;/p&gt;

&lt;p&gt;GDPR sets the next bar. When a user asks to be forgotten, every trace of their conversations has to go, cleanly and provably. A design where deletion cascades across four stores is one that eventually fails an audit. Aim instead for one delete call per store, each scoped to an identifier the application already holds. A single opaque per-user key deletes cleanly; per-turn vectors scattered through a shared index behind metadata filters can be made to work, but they’re far harder to stand behind when someone asks you to prove the data is gone.&lt;/p&gt;

&lt;p&gt;Then there’s the team: two backend engineers, no ML-ops. Anything that scales with conversation volume is a liability by year two. A summarisation cron firing an &lt;label for=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; call on every session close brings its own eviction policy, retention TTL, and retry logic, all of it infrastructure to own and operate. A managed option that does the same job behind a config flag buys that attention back for the product. The thing you give up is flexibility, and this product never spends it. One seam is worth leaving open, though: a billing &lt;label for=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-ai-agent&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-ai-agent-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;agent&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-ai-agent&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-ai-agent-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Agent&lt;/span&gt;A system that wraps an LLM with tools, memory, and a loop, so it can take multi-step actions toward a goal rather than just answering one prompt.
&lt;/span&gt; and a support agent may one day need to share what they each know about the same user, so memory keyed to user identity rather than to a single agent instance is the easier thing to grow into.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Five things the design has to deliver.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;In-session coherence. Turn fifteen must be aware of turn two. The agent needs to see the relevant history of this conversation when it generates the next response.&lt;/li&gt;
  &lt;li&gt;Cross-session recall. A user returning eleven days later should land on a bot that can reasonably answer &lt;em&gt;“what was the last thing we talked about?”&lt;/em&gt; without asking them to retype context. Not perfect replay, a usable summary.&lt;/li&gt;
  &lt;li&gt;Orchestration included. Fifteen turns with three tool calls per conversation means the assistant is planning, calling tools, observing results, and deciding what to do next. The memory solution has to live next to the orchestration, not compete with it.&lt;/li&gt;
  &lt;li&gt;Retrieval quality for conversational context. Pulling the correct fact from a past conversation is a different retrieval problem from pulling the correct paragraph from a product manual. Conversation data is short, interleaved, and context-dependent.&lt;/li&gt;
  &lt;li&gt;Operational overhead low enough for two backend engineers. No bespoke orchestration loop, no custom summarisation pipeline, no self-hosted vector database. GDPR delete has to be a button, not a project.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-memory-landscape-on-bedrock&quot;&gt;The memory landscape on Bedrock&lt;/h3&gt;

&lt;p&gt;Four plausible ways to build this.&lt;/p&gt;

&lt;p&gt;Bedrock Agents’ built-in memory. A Bedrock Agent is the managed orchestration primitive: &lt;label for=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; + action groups (tool definitions) + knowledge bases + &lt;label for=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-designing-short-term-and-long-term-memory-for-a-bedrock-chat-assistant-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; templates, all wired together so the platform handles the plan-call-observe loop. Memory comes in two layers. &lt;em&gt;Session state&lt;/em&gt; is automatic: every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeAgent&lt;/code&gt; call within a session sees the full conversation history, pass a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt; and the agent assembles the history itself. &lt;em&gt;Long-term session summaries&lt;/em&gt; are opt-in: enable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryConfiguration&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SESSION_SUMMARY&lt;/code&gt;, set a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; per end user, set a retention window (1 to 365 days). After each session ends, the agent generates a concise summary and stores it keyed to that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt;. Delete is a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeleteAgentMemory&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;DynamoDB-backed session store (build-your-own). Roll the orchestration loop yourself. A Lambda receives the user turn, reads conversation-so-far from DynamoDB (partition key &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt;, sort key turn timestamp), builds the prompt, calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt;, writes the response back, returns it. Cross-session recall is a second table keyed by user ID holding rolled-up state. Summaries come from an LLM call you write and schedule.&lt;/p&gt;

&lt;p&gt;Bedrock Knowledge Bases for long-term recall. Dump transcripts or summaries into S3 and query at runtime for &lt;em&gt;“what’s this user’s history?”&lt;/em&gt;. Chunking strategies assume a prose document; conversations are short, fragmentary, and relevance is keyed to &lt;em&gt;who&lt;/em&gt; spoke and &lt;em&gt;when&lt;/em&gt;. A chunk from someone else’s refund thread retrieved as “relevant” to this user’s login question is a correctness problem with a compliance problem stapled to it.&lt;/p&gt;

&lt;p&gt;Custom vector store with conversation embeddings. Embed each conversation (or turn, or summary) with Titan Embeddings V2, store in OpenSearch Serverless or pgvector with per-user metadata, at session start query for the current user’s top-k most relevant past interactions. Full control of chunking granularity, metadata filtering, ranking. Also a second stateful system to own alongside DynamoDB.&lt;/p&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Option&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;In-session coherence&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Cross-session recall&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Orchestration included&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Retrieval for conversation&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Low ops&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Bedrock Agents memory&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;DynamoDB session store (DIY)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Knowledge Bases for past transcripts&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Custom vector store of conversation embeddings&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;matching-the-layers-to-the-memory&quot;&gt;Matching the layers to the memory&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 640&quot; style=&quot;max-width: 100%; height: auto; font-family: system-ui, -apple-system, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;A user turn carrying sessionId and memoryId enters a Bedrock Agent. The agent reads session memory (full turn-by-turn history for this sessionId) and long-term summary memory (prior-session summaries for this memoryId), then orchestrates tool calls and generates a reply. The turn appends to session memory; on session end, a summary writes to long-term memory keyed by memoryId.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .tmf-bg       { fill: rgba(47, 125, 74, 0.05); stroke: rgba(47, 125, 74, 0.4); stroke-width: 2; }
      .tmf-user     { fill: #fff; stroke: #3a5fb5; stroke-width: 1.8; }
      .tmf-agent    { fill: rgba(183, 138, 42, 0.12); stroke: #b78a2a; stroke-width: 2; }
      .tmf-session  { fill: rgba(47, 125, 74, 0.12); stroke: #2f7d4a; stroke-width: 1.8; }
      .tmf-long     { fill: rgba(168, 74, 42, 0.12); stroke: #a84a2a; stroke-width: 1.8; }
      .tmf-tool     { fill: #f0f0f5; stroke: #5a5a6a; stroke-width: 1.5; }
      .tmf-reply    { fill: #f8f8f8; stroke: #333; stroke-width: 1.5; }
      .tmf-title    { font-size: 14px; font-weight: 700; fill: #111; }
      .tmf-detail   { font-size: 12px; fill: #222; }
      .tmf-tag      { font-size: 11px; fill: #555; font-style: italic; }
      .tmf-phase    { font-size: 11px; fill: #444; font-weight: 600; letter-spacing: 0.4px; }
      .tmf-arrow    { fill: none; stroke: #333; stroke-width: 1.4; }
      .tmf-arrow-read  { fill: none; stroke: #2f7d4a; stroke-width: 1.6; stroke-dasharray: 5 3; }
      .tmf-arrow-write { fill: none; stroke: #a84a2a; stroke-width: 1.6; }
    &lt;/style&gt;
    &lt;marker id=&quot;tmf-head&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#333&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;tmf-head-read&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#2f7d4a&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;tmf-head-write&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#a84a2a&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;1060&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;tmf-bg&quot; /&gt;

  &lt;rect x=&quot;60&quot; y=&quot;60&quot; width=&quot;260&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;tmf-user&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;88&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-title&quot;&gt;User turn&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;110&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-detail&quot;&gt;sessionId + memoryId + text&lt;/text&gt;

  &lt;rect x=&quot;430&quot; y=&quot;60&quot; width=&quot;260&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;tmf-agent&quot; /&gt;
  &lt;text x=&quot;560&quot; y=&quot;88&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-title&quot;&gt;Bedrock Agent&lt;/text&gt;
  &lt;text x=&quot;560&quot; y=&quot;110&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-detail&quot;&gt;InvokeAgent&lt;/text&gt;

  &lt;path d=&quot;M320,95 L430,95&quot; class=&quot;tmf-arrow&quot; marker-end=&quot;url(#tmf-head)&quot; /&gt;

  &lt;rect x=&quot;60&quot; y=&quot;190&quot; width=&quot;320&quot; height=&quot;100&quot; rx=&quot;6&quot; class=&quot;tmf-session&quot; /&gt;
  &lt;text x=&quot;220&quot; y=&quot;218&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-title&quot;&gt;Session memory&lt;/text&gt;
  &lt;text x=&quot;220&quot; y=&quot;240&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-detail&quot;&gt;full turn-by-turn history&lt;/text&gt;
  &lt;text x=&quot;220&quot; y=&quot;258&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-detail&quot;&gt;scoped to sessionId&lt;/text&gt;
  &lt;text x=&quot;220&quot; y=&quot;278&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot;&gt;managed by the agent, idle timeout 30 min&lt;/text&gt;

  &lt;rect x=&quot;740&quot; y=&quot;190&quot; width=&quot;320&quot; height=&quot;100&quot; rx=&quot;6&quot; class=&quot;tmf-long&quot; /&gt;
  &lt;text x=&quot;900&quot; y=&quot;218&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-title&quot;&gt;Long-term summary memory&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;240&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-detail&quot;&gt;prior-session summaries&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;258&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-detail&quot;&gt;scoped to memoryId&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;278&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot;&gt;SESSION_SUMMARY, storageDays 1-365&lt;/text&gt;

  &lt;path d=&quot;M430,120 Q 360 155 320 195&quot; class=&quot;tmf-arrow-read&quot; marker-end=&quot;url(#tmf-head-read)&quot; /&gt;
  &lt;text x=&quot;320&quot; y=&quot;160&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot; fill=&quot;#2f7d4a&quot;&gt;read in-session history&lt;/text&gt;

  &lt;path d=&quot;M690,120 Q 760 155 800 195&quot; class=&quot;tmf-arrow-read&quot; marker-end=&quot;url(#tmf-head-read)&quot; /&gt;
  &lt;text x=&quot;800&quot; y=&quot;160&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot; fill=&quot;#2f7d4a&quot;&gt;read prior-session summaries&lt;/text&gt;

  &lt;rect x=&quot;240&quot; y=&quot;340&quot; width=&quot;180&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;tmf-tool&quot; /&gt;
  &lt;text x=&quot;330&quot; y=&quot;368&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-title&quot;&gt;GetAccount&lt;/text&gt;
  &lt;text x=&quot;330&quot; y=&quot;388&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot;&gt;action group&lt;/text&gt;

  &lt;rect x=&quot;470&quot; y=&quot;340&quot; width=&quot;180&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;tmf-tool&quot; /&gt;
  &lt;text x=&quot;560&quot; y=&quot;368&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-title&quot;&gt;LookupRefund&lt;/text&gt;
  &lt;text x=&quot;560&quot; y=&quot;388&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot;&gt;action group&lt;/text&gt;

  &lt;rect x=&quot;700&quot; y=&quot;340&quot; width=&quot;180&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;tmf-tool&quot; /&gt;
  &lt;text x=&quot;790&quot; y=&quot;368&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-title&quot;&gt;Knowledge Base&lt;/text&gt;
  &lt;text x=&quot;790&quot; y=&quot;388&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot;&gt;product docs (reference)&lt;/text&gt;

  &lt;text x=&quot;560&quot; y=&quot;322&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-phase&quot;&gt;ORCHESTRATION, plan / call / observe&lt;/text&gt;

  &lt;path d=&quot;M490,130 Q 420 250 340 340&quot; class=&quot;tmf-arrow&quot; marker-end=&quot;url(#tmf-head)&quot; /&gt;
  &lt;path d=&quot;M560,130 L560,340&quot; class=&quot;tmf-arrow&quot; marker-end=&quot;url(#tmf-head)&quot; /&gt;
  &lt;path d=&quot;M630,130 Q 720 240 780 340&quot; class=&quot;tmf-arrow&quot; marker-end=&quot;url(#tmf-head)&quot; /&gt;

  &lt;rect x=&quot;430&quot; y=&quot;460&quot; width=&quot;260&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;tmf-reply&quot; /&gt;
  &lt;text x=&quot;560&quot; y=&quot;488&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-title&quot;&gt;Response to user&lt;/text&gt;
  &lt;text x=&quot;560&quot; y=&quot;510&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot;&gt;streamed reply&lt;/text&gt;

  &lt;path d=&quot;M330,410 Q 400 440 470 460&quot; class=&quot;tmf-arrow&quot; marker-end=&quot;url(#tmf-head)&quot; /&gt;
  &lt;path d=&quot;M560,410 L560,460&quot; class=&quot;tmf-arrow&quot; marker-end=&quot;url(#tmf-head)&quot; /&gt;
  &lt;path d=&quot;M790,410 Q 700 440 640 460&quot; class=&quot;tmf-arrow&quot; marker-end=&quot;url(#tmf-head)&quot; /&gt;

  &lt;path d=&quot;M430,490 Q 310 420 240 290&quot; class=&quot;tmf-arrow-write&quot; marker-end=&quot;url(#tmf-head-write)&quot; /&gt;
  &lt;text x=&quot;240&quot; y=&quot;440&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot; fill=&quot;#a84a2a&quot;&gt;append turn to session&lt;/text&gt;

  &lt;path d=&quot;M690,490 Q 820 420 880 290&quot; class=&quot;tmf-arrow-write&quot; marker-end=&quot;url(#tmf-head-write)&quot; /&gt;
  &lt;text x=&quot;880&quot; y=&quot;440&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-tag&quot; fill=&quot;#a84a2a&quot;&gt;on session end, write summary for memoryId&lt;/text&gt;

  &lt;text x=&quot;560&quot; y=&quot;600&quot; text-anchor=&quot;middle&quot; class=&quot;tmf-phase&quot;&gt;SHORT-TERM lives in session memory. LONG-TERM lives in summaries keyed by memoryId&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary); margin-top: 0.5em;&quot;&gt;One `InvokeAgent` call. Green dashed reads pull session history and prior-session summaries; red writes append the new turn and, on session end, write the summary. The developer passes `sessionId` and `memoryId`; the agent owns the plumbing.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;bedrock-agents-memory-in-depth&quot;&gt;Bedrock Agents memory, in depth&lt;/h3&gt;

&lt;p&gt;A Bedrock Agent is more than a model invocation; it’s an orchestration surface. Define &lt;em&gt;action groups&lt;/em&gt; (tool schemas plus implementing Lambdas), optionally attach knowledge bases, write an instruction prompt, call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeAgent&lt;/code&gt; with a user turn and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt;. The runtime handles the ReAct-style loop.&lt;/p&gt;

&lt;p&gt;Session memory is automatic. Every call with the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt; sees every prior turn, including tool calls and tool results. Idle timeout defaults to 30 minutes, configurable up to 24 hours. Turn fifteen sees turns one through fourteen because the agent reads them itself.&lt;/p&gt;

&lt;p&gt;Long-term summary memory is configuration. Set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryConfiguration&lt;/code&gt; on the agent with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enabledMemoryTypes: [SESSION_SUMMARY]&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageDays&lt;/code&gt; retention window. At runtime, pass a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; alongside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt;, typically a hash of the authenticated user ID. When the session ends, the agent generates a summary using a managed (customisable) prompt and stores it keyed to that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt;. Subsequent sessions with the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; have the prior summaries injected into context.&lt;/p&gt;

&lt;p&gt;Retention and deletion. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageDays&lt;/code&gt; sets a TTL; once it lapses, the summary is gone. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeleteAgentMemory&lt;/code&gt; with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; wipes everything for that user on demand. GDPR right-to-be-forgotten in one request.&lt;/p&gt;

&lt;p&gt;Limits worth naming. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; lookup is exact-match, not semantic, no vector-search “find users with similar past experiences” built in. Summaries are bounded in length, so very long histories lose detail over time. Session memory is within an agent instance, moving a user from a support agent to a billing agent needs application-level plumbing to pass state across.&lt;/p&gt;

&lt;h3 id=&quot;when-build-your-own-earns-a-place&quot;&gt;When build-your-own earns a place&lt;/h3&gt;

&lt;p&gt;Two situations flip the decision toward DynamoDB + a hand-rolled loop.&lt;/p&gt;

&lt;p&gt;When you don’t want the orchestration. Bedrock Agents is opinionated about how tools get called: it runs the loop, chooses the action group, writes the reasoning. A team that needs tighter control over prompts, tool ordering, or failure modes sometimes builds its own loop instead. Session state then has to live somewhere, and DynamoDB is the natural home: partition key &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt;, sort key turn timestamp, TTL for auto-expiry.&lt;/p&gt;

&lt;p&gt;When state is richer than turns. Conversations aren’t the only per-session state; a shopping cart, a configured quote, a workflow status are none of them naturally turns. DynamoDB holds that directly, and the tools read and write it.&lt;/p&gt;

&lt;p&gt;Neither flip applies to the two-engineer support bot. Orchestration is standard ReAct-over-tools; state is conversational. Bedrock Agents covers both.&lt;/p&gt;

&lt;p&gt;The hybrid worth knowing. Teams using Bedrock Agents memory often add a small DynamoDB or S3 store for &lt;em&gt;structured&lt;/em&gt; cross-session facts, ticket numbers, subscription plan, last-known issue code, that the agent needs reliably regardless of whether they appear in a generated summary. Summary memory is the prose recall; the DynamoDB table is the structured one. A tool the agent calls to fetch it is the clean seam.&lt;/p&gt;

&lt;h3 id=&quot;why-knowledge-bases-is-the-wrong-shape-for-conversations&quot;&gt;Why Knowledge Bases is the wrong shape for conversations&lt;/h3&gt;

&lt;p&gt;Four reasons.&lt;/p&gt;

&lt;p&gt;Chunking doesn’t match. Knowledge Bases chunk documents, fixed-size (default ~300 tokens), hierarchical, or semantic, assuming nearby text is topically coherent. A conversation transcript has rapid speaker alternation, interleaved tool outputs, and short turns; a 300-token chunk spans three sub-topics and two speakers.&lt;/p&gt;

&lt;p&gt;Retrieval relevance is topic, not speaker. A vector search for &lt;em&gt;“refund”&lt;/em&gt; across a knowledge base of all transcripts will cheerfully return high-similarity chunks from other users’ refund conversations. Compliance problem plus correctness problem. Metadata filtering by user ID helps but has to be attached at ingestion and is less flexible than a native vector store’s.&lt;/p&gt;

&lt;p&gt;Summaries vs transcripts. Storing raw transcripts means retrieving fragments. The correct thing to retrieve is summaries, and generating those is the job Bedrock Agents’ long-term memory already does.&lt;/p&gt;

&lt;p&gt;GDPR is harder. Deleting a user’s data means locating every chunk that contains their content in a service-managed index, then re-ingesting. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeleteAgentMemory&lt;/code&gt; is one call.&lt;/p&gt;

&lt;p&gt;Knowledge Bases are correct for &lt;em&gt;“what does our support policy say about refunds?”&lt;/em&gt;, a reference corpus shared across users. Wrong for &lt;em&gt;“what did this user say yesterday?”&lt;/em&gt;, per-user conversational state.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-design&quot;&gt;A worked design&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Bedrock Agent wrapping Claude Haiku 4.5, latency-sensitive, cost-sensitive, reasoning bar for first-line support is low enough. Action groups for account lookup, subscription status, ticket create/query. One Knowledge Base attached for the product documentation corpus, the &lt;em&gt;policy&lt;/em&gt; memory, not the &lt;em&gt;user&lt;/em&gt; memory.&lt;/li&gt;
  &lt;li&gt;Session memory: on by default. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt; is the chat-widget session, rotated on explicit “new conversation” or 30 minutes idle.&lt;/li&gt;
  &lt;li&gt;Long-term summary memory: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryConfiguration&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SESSION_SUMMARY&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageDays: 90&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sha256(userId)&lt;/code&gt;, stable per authenticated user, doesn’t leak the raw ID. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; both passed on every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeAgent&lt;/code&gt; call.&lt;/li&gt;
  &lt;li&gt;Structured cross-session state: a small DynamoDB table keyed by user ID, holding open ticket IDs, subscription tier, last-issue-code. A &lt;em&gt;GetUserContext&lt;/em&gt; action group lets the agent fetch this at conversation start when relevant.&lt;/li&gt;
  &lt;li&gt;GDPR delete: a Lambda triggered by account closure calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeleteAgentMemory&lt;/code&gt; with the user’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt;, deletes the DynamoDB row, records an audit trail.&lt;/li&gt;
  &lt;li&gt;Retention: summaries lapse after 90 days via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageDays&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Monitoring: CloudWatch on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeAgent&lt;/code&gt; latency and error rate; a weekly anonymised sample of summaries reviewed for quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No dedicated memory database, no custom summarisation cron, no per-user vector index. The memory plumbing comes with the agent.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Short-term and long-term memory are different problems. Turn-level coherence within one conversation is session state; cross-visit recall is summary state. A single solution rarely does both well unless it was designed for both.&lt;/li&gt;
  &lt;li&gt;Bedrock Agents memory covers both layers as managed functionality. Session memory is automatic, pass a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt;. Long-term summary memory is configuration, enable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SESSION_SUMMARY&lt;/code&gt;, pass a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt;, set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageDays&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; scopes long-term memory to a user; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionId&lt;/code&gt; scopes session memory to a conversation. Orthogonal identifiers, both passed on every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeAgent&lt;/code&gt; call when long-term memory is enabled.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeleteAgentMemory&lt;/code&gt; is the GDPR delete button. One API call, scoped to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt;. Retention also lapses automatically via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageDays&lt;/code&gt; (1 to 365).&lt;/li&gt;
  &lt;li&gt;Knowledge Bases are for reference corpora, not conversational state. Chunking, retrieval relevance, and per-user isolation all work against using them for past-transcript recall.&lt;/li&gt;
  &lt;li&gt;DynamoDB fits as structured-state companion to Bedrock Agents memory. Ticket IDs, subscription tier, status flags, things the agent fetches via a tool call, not things the agent summarises in prose. A hybrid is common and clean.&lt;/li&gt;
  &lt;li&gt;A custom vector store over conversation embeddings is flexibility that costs a team. Justified when cross-user semantic similarity is a product feature; overkill when the product just needs &lt;em&gt;“remember this user”&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;Bedrock Agents includes orchestration. Action groups, knowledge bases, and the ReAct-style tool loop come with the agent. Build-your-own means rebuilding that loop, more code to own, no better outcome for standard shapes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answer: use a Bedrock Agent with session memory for in-conversation coherence and long-term summary memory (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SESSION_SUMMARY&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoryId&lt;/code&gt; per user, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageDays&lt;/code&gt; retention) for cross-session recall. Attach a Knowledge Base for product documentation, the reference corpus every user shares. Add a small DynamoDB table of structured per-user state (open tickets, subscription tier) behind a &lt;em&gt;GetUserContext&lt;/em&gt; action group. Wire &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeleteAgentMemory&lt;/code&gt; into the account-closure path for GDPR. The two engineers ship a memory system without operating a memory system.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Search and Planning</title>
    <link href="/writing/search-and-planning/"/>
    <updated>2026-06-06T06:00:00+08:00</updated>
    <id>/writing/search-and-planning/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;You open the map app. You type an address. Forty milliseconds later it shows you a path through 3.4 million road segments, optimal in time, accounting for current traffic. There’s no neural network involved. There’s no learning. There’s an algorithm from the 1960s, running on a graph, doing what it has always done.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the previous five posts we covered AI for problems where the input is text. Classification, retrieval, generation, the lot. This post leaves text behind. Most of what the textbooks call classical AI, the kind in Russell and Norvig’s &lt;em&gt;Artificial Intelligence: A Modern Approach&lt;/em&gt;, isn’t about understanding language. It’s about searching through possibilities.&lt;/p&gt;

&lt;p&gt;Search algorithms run more production AI than transformers do. They route your packets, plan your warehouse robot’s path, schedule your CI build, find the chess move, plan your delivery route. They’ve been quietly working since the 1960s and they’re not getting replaced.&lt;/p&gt;

&lt;p&gt;This post walks the family. Like &lt;a href=&quot;/writing/before-the-transformer/&quot;&gt;Before the Transformer&lt;/a&gt; but for problem-solving instead of language modelling.&lt;/p&gt;

&lt;h3 id=&quot;the-setup-states-actions-goals&quot;&gt;The setup: states, actions, goals&lt;/h3&gt;

&lt;p&gt;Most search problems share a structure:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;States. Configurations of the world. The position of the chess pieces. The location of the delivery truck. The contents of the warehouse robot’s basket.&lt;/li&gt;
  &lt;li&gt;Actions. Things you can do that change one state into another. Move a piece. Drive to the next intersection. Pick up an item.&lt;/li&gt;
  &lt;li&gt;A goal. A state (or set of states) you want to reach. Checkmate. The customer’s address. All packages delivered.&lt;/li&gt;
  &lt;li&gt;A cost. Often there’s a cost on each action, distance, time, fuel, whatever, and you want the cheapest path to the goal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The search problem is: find a sequence of actions that gets you from the starting state to a goal state, ideally cheaply, ideally fast.&lt;/p&gt;

&lt;p&gt;That’s it. That’s the framing. Once a problem is in this shape, decades of algorithms apply.&lt;/p&gt;

&lt;h3 id=&quot;uninformed-search&quot;&gt;Uninformed search&lt;/h3&gt;

&lt;p&gt;These are the algorithms you can write in fifty lines because they don’t know anything about the problem, they just systematically explore.&lt;/p&gt;

&lt;p&gt;Breadth-first search (BFS) explores layer by layer, finding the path with the fewest actions. Use it for: small graphs, “fewest-moves” puzzles, finding the nearest matching node. The classic example: solve the 8-puzzle in the minimum number of slides.&lt;/p&gt;

&lt;p&gt;Depth-first search (DFS) goes as deep as possible before backtracking. Use it for: exploring trees, generating permutations, anything where you need a memory-light traversal. Classic example: enumerate all possible game positions.&lt;/p&gt;

&lt;p&gt;Iterative deepening (IDS) combines both: do DFS to depth 1, then to depth 2, then to depth 3, and so on. Memory of DFS, completeness of BFS. Used in chess engines for depth-limited search.&lt;/p&gt;

&lt;p&gt;Uniform-cost search is BFS with weighted edges, explore in order of cumulative cost rather than in order of depth. Equivalent to Dijkstra’s algorithm, which you’ve probably implemented at some point.&lt;/p&gt;

&lt;p&gt;These are the workhorses. They’re old, they’re simple, and they show up everywhere.&lt;/p&gt;

&lt;h3 id=&quot;informed-search-a-and-friends&quot;&gt;Informed search: A* and friends&lt;/h3&gt;

&lt;p&gt;The big jump happens when you have a heuristic, a function that estimates how far each state is from the goal. With a heuristic, you don’t explore blindly. You explore in order of “most promising next.”&lt;/p&gt;

&lt;p&gt;A* (&lt;a href=&quot;https://ieeexplore.ieee.org/document/4082128&quot;&gt;Hart, Nilsson, and Raphael, 1968&lt;/a&gt;) is the algorithm. It expands states in order of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g(n) + h(n)&lt;/code&gt;, where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g&lt;/code&gt; is the cost to reach the state and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt; is the heuristic estimate of cost to the goal. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt; never overestimates the true cost (an “admissible” heuristic), A* is guaranteed to find the optimal path.&lt;/p&gt;

&lt;p&gt;A* runs:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Your map app, with a heuristic of straight-line distance to the destination.&lt;/li&gt;
  &lt;li&gt;Pathfinding in games, where the units need to walk around walls efficiently.&lt;/li&gt;
  &lt;li&gt;Robot path planning, both in factories and in self-driving cars.&lt;/li&gt;
  &lt;li&gt;Puzzle solving, with heuristics like “number of misplaced tiles” for the 15-puzzle.&lt;/li&gt;
  &lt;li&gt;Build systems, finding the minimum-work path through a dependency graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A* works because a &lt;em&gt;good&lt;/em&gt; heuristic can collapse the search space dramatically, from “explore everything” to “explore only what’s plausible.”&lt;/p&gt;

&lt;p&gt;When A* won’t fit in memory, you have variants: IDA* (iterative-deepening A*), SMA* (memory-bounded A*), D* (dynamic A* for changing environments). These are all in the toolbox for production pathfinding.&lt;/p&gt;

&lt;h3 id=&quot;local-search-and-metaheuristics&quot;&gt;Local search and metaheuristics&lt;/h3&gt;

&lt;p&gt;When the search space is too big to enumerate, you give up on optimality and try to find a &lt;em&gt;good&lt;/em&gt; answer rather than the &lt;em&gt;best&lt;/em&gt; one. This is local search.&lt;/p&gt;

&lt;p&gt;Hill climbing starts from a random state and moves to the best neighbour. Simple, fast, gets stuck in local optima. Good enough for many problems.&lt;/p&gt;

&lt;p&gt;Simulated annealing (&lt;a href=&quot;https://www.science.org/doi/10.1126/science.220.4598.671&quot;&gt;Kirkpatrick, Gelatt, and Vecchi, 1983&lt;/a&gt;) hill-climbs but occasionally accepts a worse move (more often early, less often later). The “annealing” comes from metallurgy, cooling slowly to find a better global structure. Workhorse for layout problems, scheduling, and combinatorial optimisation.&lt;/p&gt;

&lt;p&gt;Genetic algorithms maintain a population of candidate solutions, combine pairs (“crossover”), perturb them (“mutation”), and select the fittest to breed. Used for design-space exploration, hyperparameter tuning before Bayesian methods, and antenna design (NASA has flown a genetic-algorithm-designed antenna).&lt;/p&gt;

&lt;p&gt;Tabu search keeps a list of recently-visited states and refuses to revisit them, forcing the search to explore new territory.&lt;/p&gt;

&lt;p&gt;These are not the prestige algorithms of the field, but they’re the practical answer for “I have a giant combinatorial problem and I need a reasonable solution by Friday.”&lt;/p&gt;

&lt;h3 id=&quot;adversarial-search-games&quot;&gt;Adversarial search: games&lt;/h3&gt;

&lt;p&gt;When you’re playing against an opponent, single-agent search isn’t enough, you need to anticipate what they’ll do. Enter adversarial search.&lt;/p&gt;

&lt;p&gt;Minimax is the basic game-tree search: assume both players play optimally, and pick the move that maximises your worst-case outcome. The tree branches at every move, with you maximising at your turns and the opponent minimising at theirs.&lt;/p&gt;

&lt;p&gt;Alpha-beta pruning is the optimisation that makes minimax practical. By tracking the best score the maximiser is assured of (alpha) and the best the minimiser is assured of (beta), large parts of the search tree can be pruned without affecting the result. A well-tuned alpha-beta search can go many plies deeper than naive minimax in the same time.&lt;/p&gt;

&lt;p&gt;This is the algorithmic core of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Chess engines (Stockfish, the strongest classical chess program, is alpha-beta with extensive engineering).&lt;/li&gt;
  &lt;li&gt;Checkers, Go (pre-AlphaGo), Othello, and most other deterministic two-player games.&lt;/li&gt;
  &lt;li&gt;Monte Carlo tree search (MCTS), which is what AlphaGo and AlphaZero used, a different strategy, but still adversarial search at heart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even the deep-learning-based game systems use search. AlphaGo combined a neural network for evaluation with MCTS for search. Stockfish 16+ has a neural network evaluation but still does alpha-beta search through the tree. The search is the engine; the neural net is the heuristic.&lt;/p&gt;

&lt;h3 id=&quot;constraint-satisfaction-problems&quot;&gt;Constraint satisfaction problems&lt;/h3&gt;

&lt;p&gt;Slightly different shape: you have a set of variables, each with a domain of possible values, and a set of constraints between them. You want an assignment of values to variables that satisfies all constraints.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sudoku. Variables are cells, domains are 1-9, constraints are row/column/box uniqueness.&lt;/li&gt;
  &lt;li&gt;Map colouring. Variables are regions, domains are colours, constraints are “adjacent regions different.”&lt;/li&gt;
  &lt;li&gt;Class scheduling. Variables are courses, domains are time slots, constraints are room/teacher/student conflicts.&lt;/li&gt;
  &lt;li&gt;Configuration. Variables are component choices, domains are products, constraints are compatibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classical algorithm is backtracking with constraint propagation: pick a variable, try a value, propagate the implications, recurse, backtrack on failure. The cleverness is in the propagation, arc consistency, forward checking, unit propagation, which prunes the search space dramatically.&lt;/p&gt;

&lt;p&gt;Industrial CSP solvers (Google OR-Tools, Choco, MiniZinc) handle problems with millions of variables and constraints. They run:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Hospital staff scheduling.&lt;/li&gt;
  &lt;li&gt;Aircraft and crew scheduling.&lt;/li&gt;
  &lt;li&gt;Hardware verification.&lt;/li&gt;
  &lt;li&gt;Network configuration.&lt;/li&gt;
  &lt;li&gt;Supply-chain optimisation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s no learning involved. Just very-well-engineered search through structured spaces.&lt;/p&gt;

&lt;h3 id=&quot;planning&quot;&gt;Planning&lt;/h3&gt;

&lt;p&gt;Planning is the version of search where the action descriptions are more abstract. Instead of a graph of states, you have:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A description of the world in terms of facts (the box is at location A, the gripper is empty, the door is open).&lt;/li&gt;
  &lt;li&gt;A library of actions, each described by preconditions (what must be true to execute) and effects (what becomes true / false after executing).&lt;/li&gt;
  &lt;li&gt;A goal expressed in terms of facts (the box is at location B).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classical algorithm here is STRIPS (Stanford Research Institute Problem Solver, 1971) and its descendants. Modern planners. Fast Downward, LAMA, ENHSP, can handle much richer planning problems with continuous variables, time, and resources.&lt;/p&gt;

&lt;p&gt;Planning runs:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Robot task planning. “Put the cup on the shelf” decomposed into a sequence of low-level actions.&lt;/li&gt;
  &lt;li&gt;Logistics and delivery routing in complex domains.&lt;/li&gt;
  &lt;li&gt;Game AI for non-player-character behaviour, particularly Goal-Oriented Action Planning (GOAP) in commercial game engines.&lt;/li&gt;
  &lt;li&gt;Spacecraft autonomy. NASA’s Remote Agent on Deep Space 1 was a planner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Planning is less famous than minimax or A*, but in domains where you need to reason about long action sequences, it’s the correct tool.&lt;/p&gt;

&lt;h3 id=&quot;a-decision-table&quot;&gt;A decision table&lt;/h3&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;If your task is…&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Reach for…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Shortest route between points on a graph&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Dijkstra (no heuristic) or A* (with one)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Pathfinding for a unit in a 2D/3D world&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A* on a navigation grid or mesh&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A two-player perfect-information game&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Alpha-beta or MCTS, with a learned or hand-crafted evaluation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Solving a puzzle (Sudoku, n-queens, scheduling)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A CSP solver (OR-Tools, MiniZinc)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Optimising a hard combinatorial problem&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Simulated annealing or genetic algorithm if exact methods are infeasible&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Sequencing actions for a robot or workflow&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A planner (PDDL + Fast Downward, or GOAP for games)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Routing many vehicles to many destinations&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A vehicle-routing solver (built on CSP / mixed-integer programming)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Build-system dependency resolution&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Topological sort + Dijkstra or DAG-aware scheduler&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h3 id=&quot;search-vs-ml-when-each-wins&quot;&gt;Search vs ML: when each wins&lt;/h3&gt;

&lt;p&gt;Search and machine learning solve different shapes of problem.&lt;/p&gt;

&lt;p&gt;Search wins when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The state space and action space are well-defined.&lt;/li&gt;
  &lt;li&gt;Optimality (or near-optimality) is the goal, not “good enough.”&lt;/li&gt;
  &lt;li&gt;Problems are deterministic and the rules are knowable.&lt;/li&gt;
  &lt;li&gt;You can write a heuristic that estimates progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ML wins when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The state space is fuzzy or perceptual (images, raw text).&lt;/li&gt;
  &lt;li&gt;“Good enough” is fine and you can’t define optimal.&lt;/li&gt;
  &lt;li&gt;The rules are statistical, not deterministic.&lt;/li&gt;
  &lt;li&gt;You have lots of examples to learn from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most powerful systems combine both. AlphaZero is search guided by a learned heuristic. Self-driving cars use planning over a perception layer that’s deep learning. Modern logistics systems use ML to predict demand and search to plan delivery.&lt;/p&gt;

&lt;p&gt;Most of the AI in the textbooks before deep learning was some flavour of search. Those textbooks weren’t wrong; they were a generation early, and most of the algorithms they covered are still in production. A* still finds the route in the map app. Alpha-beta still drives the strongest classical chess engines, and even AlphaZero is search guided by a learned evaluator rather than a pure neural play. CSP solvers schedule the hospital, the airline, and the supply chain. STRIPS-descended planners sequence robot actions and ran the autonomy on Deep Space 1. When exact search doesn’t fit, simulated annealing and genetic algorithms produce respectable answers by Friday.&lt;/p&gt;

&lt;p&gt;Search and ML solve different shapes of problem. Search wins where the state space is well-defined, the rules are knowable, and you can write a heuristic that estimates progress. ML wins where the input is perceptual, the rules are statistical, and “good enough” is the bar. The best systems combine them: ML for perception and evaluation, search for the sequencing that has to be optimal. The pattern was always going to be both.&lt;/p&gt;

&lt;p style=&quot;margin-top: var(--space-md); font-size: 0.88rem; color: var(--color-ink-tertiary); font-style: italic;&quot;&gt;The next chapter, Knowledge, Logic, and Constraints, publishes around 13 June.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: Business Model Canvas</title>
    <link href="/writing/the-workshop-business-model-canvas/"/>
    <updated>2026-06-05T06:00:00+08:00</updated>
    <id>/writing/the-workshop-business-model-canvas/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Nine boxes on one page. The Business Model Canvas shows where revenue comes from, what it costs to serve a customer, and which assumptions hold the whole thing together: “does this business actually work?” you can read in five minutes. Worked example: &lt;a href=&quot;/writing/business-model-canvas-does-this-actually-work/&quot;&gt;Does This Actually Work?&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;business-model-canvas&quot;&gt;Business Model Canvas&lt;/h3&gt;

&lt;p&gt;The Business Model Canvas (BMC) lays out how a business creates, delivers, and captures value on a single page of nine boxes, filled in customer-first order, so the team can see whether the whole thing holds together and where the most dangerous assumptions live. Invented by Alexander Osterwalder as part of his PhD, published with Yves Pigneur in &lt;em&gt;Business Model Generation&lt;/em&gt; (2010), and now one of the most widely used strategic tools in any discipline. Sometimes confused with a business plan (a long document assuming the model works; the Canvas is a one-page hypothesis about whether it will) or with Ash Maurya’s Lean Canvas (&lt;em&gt;Running Lean&lt;/em&gt;, 2012), which swaps four boxes for Problem, Solution, Key Metrics, and Unfair Advantage; reach for Lean Canvas before product-market fit, BMC once there’s something to articulate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; a facilitator, the founder or business owner, a product person, customer-facing people (sales, support, marketing), one or two developers, and operations. Four to six people, around two hours.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; a populated nine-box Canvas with photographs and a digital transcription, an explicit contradiction list from the read-aloud review, and the riskiest assumptions flagged for follow-up testing.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; a new business or product line, an investor / board / new-hire explanation, or a suspicion that pricing, costs, and value proposition don’t actually fit together. Not for sprint planning or feature prioritisation, and not when nobody in the room understands the economics (do &lt;a href=&quot;/writing/the-workshop-jobs-to-be-done/&quot;&gt;JTBD&lt;/a&gt; first if the customer or value proposition isn’t yet clear).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whats-it-for&quot;&gt;What’s It For&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;“Can everyone in the room describe the business model the same way?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s the question a facilitator asks at the start of a Canvas session, and it’s the question that makes the room go quiet. Not because nobody in the room knows the business model; they each know a version of it. The founder knows the pricing and the margin assumptions. The developer knows the delivery architecture and roughly what it costs to run. The ops lead knows the wastage rate on unsold perishables. The product person knows the customer acquisition story. Each version is coherent on its own. None of the versions agree with each other, and the business model isn’t any single one of them; it’s the intersection of all of them, which is a thing nobody has ever looked at as a single object.&lt;/p&gt;

&lt;p&gt;The gap shows up quietly. A founder whose pricing assumptions haven’t met the ops lead’s wastage numbers discovers, one quiet Sunday, that each box costs $41 to source, pack, and deliver, and they’ve been selling them for $35. Every new subscriber is losing the business money. The faster they grow, the faster they go bankrupt. Nobody did anything wrong. Each person was right about their own box. The failure was that the boxes were never put on one page where somebody had to read them out loud next to each other.&lt;/p&gt;

&lt;p&gt;The Business Model Canvas exists to be that one page. Nine boxes, filled in customer-first order, read out loud in pairs at the end so the arithmetic and the logic both have to survive contact with the rest of the model. You can’t admire the value proposition without seeing what it costs to deliver. You can’t celebrate the pricing without seeing the operational burden. You can’t forget about customer acquisition because there’s a box for it on the same page as the revenue streams it feeds.&lt;/p&gt;

&lt;p&gt;Reach for it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You’re starting a new business or a major new product line&lt;/li&gt;
  &lt;li&gt;You need to explain the business model to investors, new hires, a board, or yourself&lt;/li&gt;
  &lt;li&gt;You suspect parts of the business model don’t fit together: the value proposition doesn’t match the revenue model, or the costs don’t support the pricing&lt;/li&gt;
  &lt;li&gt;You’re comparing two different business model options and need a side-by-side view&lt;/li&gt;
  &lt;li&gt;An existing business is drifting and you want to diagnose which part of the model has changed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-its-not-for&quot;&gt;What It’s Not For&lt;/h3&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The business model is established, well-understood, and not in question. You’d be documenting, not discovering.&lt;/li&gt;
  &lt;li&gt;You’re planning features or sprints. The Canvas is strategic, not tactical.&lt;/li&gt;
  &lt;li&gt;You don’t have anyone in the room who understands the economics (revenue, costs, margins). The Canvas will have holes exactly where it needs to be sharpest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop a session that’s already started if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Four or more of the nine boxes are pure guesses, making the Canvas mostly fiction; better to pause and do research first&lt;/li&gt;
  &lt;li&gt;The founder refuses to engage with contradictions surfaced during review&lt;/li&gt;
  &lt;li&gt;The room is missing the person who owns the cost structure or the pricing, and multiple boxes depend on them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stopping and fixing the inputs is not failure. Producing a Canvas that papers over an incoherent business is.&lt;/p&gt;

&lt;h3 id=&quot;definitions--background&quot;&gt;Definitions &amp;amp; Background&lt;/h3&gt;

&lt;p&gt;The nine boxes of the Canvas, with the role each one plays:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Customer Segments: who you’re serving. Specific groups of people or organisations.&lt;/li&gt;
  &lt;li&gt;Value Propositions: what value you deliver to each segment. The benefit as the customer experiences it, not the feature you build.&lt;/li&gt;
  &lt;li&gt;Channels: how you reach and deliver to each segment, across awareness, evaluation, purchase, delivery, and after-sales.&lt;/li&gt;
  &lt;li&gt;Customer Relationships: how you acquire, retain, and grow each segment. Personal, automated, community, co-creation.&lt;/li&gt;
  &lt;li&gt;Revenue Streams: what customers pay, how, and how much. Pricing models with numbers attached.&lt;/li&gt;
  &lt;li&gt;Key Resources: the assets essential to delivering the value propositions. Physical, intellectual, human, financial.&lt;/li&gt;
  &lt;li&gt;Key Activities: the most important things the business must do well. Essential and distinctive, not every task.&lt;/li&gt;
  &lt;li&gt;Key Partners: who you depend on. Suppliers, partners, services who could break you if they disappeared.&lt;/li&gt;
  &lt;li&gt;Cost Structure: the most significant costs in the model. Fixed, variable, one-time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Boxes are filled in customer-first order, not left-to-right. Start with the customer, trace the value outward (segments → propositions → channels → relationships → revenue), then trace the economics backward (activities → resources → partners → costs). Starting with costs produces a defensive Canvas. Starting with customers produces a strategic one.&lt;/p&gt;

&lt;p&gt;The reading-aloud ritual. At the end, pairs of boxes are read out loud next to each other: Revenue Streams next to Cost Structure, Value Propositions next to Customer Relationships, Customer Segments next to Channels. The arithmetic and the logic both have to survive contact with the rest of the model. Most Canvas sessions produce at least one coherence break in this phase. The value of the session is finding it.&lt;/p&gt;

&lt;h3 id=&quot;inputs&quot;&gt;Inputs&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;An idea of the business or product concrete enough to test. &lt;em&gt;“A weekly subscription produce box”&lt;/em&gt; is enough. &lt;em&gt;“Something with food, maybe”&lt;/em&gt; is not.&lt;/li&gt;
  &lt;li&gt;People who can speak to different parts of the business. No single participant will know all nine boxes, but collectively the room should. Customer-facing voices, economic voices, operational voices.&lt;/li&gt;
  &lt;li&gt;A wall or large surface with the nine-box Canvas drawn on it (printed, taped up, or projected), sticky notes, and pens. Roughly two hours, uninterrupted.&lt;/li&gt;
  &lt;li&gt;Numbers, even rough ones, for pricing and the major costs. The Canvas can survive estimates labelled as estimates; it can’t survive nine boxes of vibes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the team can’t yet articulate the value proposition or the customer, run &lt;a href=&quot;/writing/the-workshop-jobs-to-be-done/&quot;&gt;JTBD&lt;/a&gt; first to clarify which job the customer is hiring the product to do. JTBD output feeds directly into the Value Propositions box.&lt;/p&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;What lands on the Canvas at the end:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A populated Canvas: nine boxes with sticky notes, photographed from directly in front and as close-ups of each box, with the notes readable.&lt;/li&gt;
  &lt;li&gt;A digital transcription: the Canvas captured in Miro, Mural, Figma, or a slide template, with every sticky note carried across.&lt;/li&gt;
  &lt;li&gt;A list of contradictions found during the review phase, each as its own line item: &lt;em&gt;“Revenue $35 vs variable cost $41, losing $6 per sale”&lt;/em&gt;, &lt;em&gt;“Personal-connection value vs automated relationship”&lt;/em&gt;, etc.&lt;/li&gt;
  &lt;li&gt;A list of empty or shaky boxes: the ones the room couldn’t fill confidently. These are findings, not failures.&lt;/li&gt;
  &lt;li&gt;An explicit list of the riskiest assumptions baked into the model, usually concentrated in Revenue Streams, Cost Structure, and Customer Segments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These outputs feed straight into:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt; is the natural follow-up. A Canvas is nine boxes of beliefs; Assumption Mapping surfaces and tests them. Run it specifically on Revenue Streams and Cost Structure, where incorrect assumptions are fatal.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt;. The Canvas sets the strategy; Impact Mapping picks the deliverables to execute against it. Canvas first for a new business; Impact Mapping first for an existing business with a clear goal.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt;. Once the Canvas is coherent, Story Mapping turns the value proposition into a user journey and a release plan.&lt;/li&gt;
  &lt;li&gt;Wardley Mapping. The Canvas shows what the business is; Wardley Mapping shows where its components sit in the evolution of the market and therefore how they should be treated strategically. Canvas answers “what”; Wardley answers “where.”&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming&lt;/a&gt;. Once the Canvas is agreed, Event Storming maps the processes the business will actually run to deliver on it. Canvas sets the shape; Event Storming maps the operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whos-needed&quot;&gt;Who’s Needed&lt;/h3&gt;

&lt;p&gt;Four to six people, around two hours:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Facilitator. Holds the box order, keeps the conversation moving, and catches when a feature has been smuggled into the Value Propositions box.&lt;/li&gt;
  &lt;li&gt;Founder or business owner. Mandatory. They’re the only person who knows (or at least believes they know) the economics, the pricing, the costs, and the margins. Without them, the Canvas will have the most important boxes filled in with guesses.&lt;/li&gt;
  &lt;li&gt;Product person. They’ll anchor the value propositions and the channels, and they’ll translate between the founder’s business framing and the team’s delivery framing.&lt;/li&gt;
  &lt;li&gt;Customer-facing people. Whoever talks to actual customers: sales, support, marketing, account managers, operations staff who handle complaints. They will contradict the optimistic assumptions in the room, which is exactly why they’re there.&lt;/li&gt;
  &lt;li&gt;Developers. One or two. They need to understand the model they’re building for. They will also catch the technical assumptions baked into the Key Resources and Key Activities boxes that nobody else will notice.&lt;/li&gt;
  &lt;li&gt;Operations / SRE. For any business where operations are a non-trivial cost or a differentiator (which is most of them) ops is a first-class participant. The Cost Structure box is often where ops has the most to say, and what they say is often unwelcome but essential.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Canvas works by conversation between perspectives, and the conversation collapses above six. Below four, you don’t have enough perspectives to challenge each other.&lt;/p&gt;

&lt;p&gt;Who to leave out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Investors and board members. They see the Canvas as an output, not during the conversation. Their presence changes what the team will say out loud.&lt;/li&gt;
  &lt;li&gt;Large stakeholder groups. If ten people need to shape the model, run a pre-session to agree the goal and come to the Canvas with the group down to six.&lt;/li&gt;
  &lt;li&gt;Pure feature-thinkers. Someone who can only discuss what to build, not why or for whom or at what margin, will turn the Value Propositions box into a feature list and the whole Canvas drifts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;how-to-run-it&quot;&gt;How To Run It&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Box&lt;/th&gt;
      &lt;th&gt;Duration&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;Customer Segments&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“Who are we serving?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;Value Propositions&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;“What value do we deliver?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;Channels&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“How do we reach and deliver?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;Customer Relationships&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“How do we acquire, retain, and grow?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;Revenue Streams&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“What do they pay, and how?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6&lt;/td&gt;
      &lt;td&gt;Key Activities&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“What must we actually do?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;7&lt;/td&gt;
      &lt;td&gt;Key Resources&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“What do we need to deliver this?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8&lt;/td&gt;
      &lt;td&gt;Key Partners&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“Who do we depend on?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;9&lt;/td&gt;
      &lt;td&gt;Cost Structure&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“What does it all cost?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;10&lt;/td&gt;
      &lt;td&gt;Review for coherence&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;“Does the maths work?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;~2 hours&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Order matters. Start with Customer Segments because every other box is defined in terms of the customer. End with Cost Structure because by the time you get there, you know what you’re doing, for whom, how, and how it’s delivered. Only then can you add up what it costs.&lt;/p&gt;

&lt;p&gt;The Canvas is a round-the-room conversation moderated by the facilitator, with notes going up in the current box only. Everyone speaks in every box, but the domain expert for the box takes the lead:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Customer Segments: the customer-facing people lead; everyone else pressure-tests specificity.&lt;/li&gt;
  &lt;li&gt;Value Propositions: the founder and product person lead; everyone else challenges whether the claimed value is actually the value the customer experiences.&lt;/li&gt;
  &lt;li&gt;Channels and Customer Relationships: marketing, sales, and support lead; the developers listen hard because these boxes define half of what they’ll need to build.&lt;/li&gt;
  &lt;li&gt;Revenue Streams: the founder leads, with support from anyone who knows the market. Numbers get written down, even rough ones.&lt;/li&gt;
  &lt;li&gt;Key Resources, Activities, Partners: operations and developers lead; the founder listens hard because this is where their optimism meets operational reality.&lt;/li&gt;
  &lt;li&gt;Cost Structure: operations and founder together. Developers add the technology costs.&lt;/li&gt;
  &lt;li&gt;Review: everyone. The reading-it-aloud ritual is where the coherence check happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rhythm is customer outward, then back through to costs, then check the maths.&lt;/p&gt;

&lt;h4 id=&quot;phase-1-customer-segments-10-minutes&quot;&gt;Phase 1: Customer Segments (10 minutes)&lt;/h4&gt;

&lt;p&gt;Point at the Customer Segments box and ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Who exactly are we creating value for? I want specifics. Not ‘everyone who eats food’ or ‘health-conscious consumers.’ Specific enough that I could walk down a street and tell you whether the person next to me is one or not.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write each segment on a sticky note and place it in the box. Push hard for specificity:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“‘Busy families’ is closer. Which busy families? Dual-income, both parents working full-time, kids at school, lives in a city with limited supermarket access after 7pm? Now we have an actor whose behaviour we can actually influence.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If there are multiple segments, rank them. One primary, one or two secondary. Businesses rarely serve three primary segments well in the first year.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Too broad. &lt;em&gt;“People who eat food.”&lt;/em&gt; Push for age, geography, behaviour, pain point, or life stage.&lt;/li&gt;
  &lt;li&gt;Too many segments. More than three or four is a startup trying to be everything. Pick the one or two that matter most and park the rest.&lt;/li&gt;
  &lt;li&gt;Confusing users with customers. The person who uses the product and the person who pays may be different. If a company buys boxes for employees, the company is the customer and the employee is the user. Capture both and note which one pays.&lt;/li&gt;
  &lt;li&gt;The absent customer. Nobody in the room knows the segment concretely because they’ve never spoken to one. That’s a finding; note it, because it will shape which assumptions you test later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-2-value-propositions-15-minutes&quot;&gt;Phase 2: Value Propositions (15 minutes)&lt;/h4&gt;

&lt;p&gt;For each customer segment, ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What value are we delivering to this segment specifically? What problem are we solving, what pain are we relieving, what job are we helping them get done (Jobs to be Done: the framing that customers hire products to get a specific job done; see &lt;a href=&quot;/writing/the-workshop-jobs-to-be-done/&quot;&gt;JTBD workshop&lt;/a&gt; for the deeper version)? I want the benefit as the customer would describe it, not the feature we’d describe.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write value propositions on sticky notes in the box and connect them, visually or by proximity, to the segment they serve.&lt;/p&gt;

&lt;p&gt;Value propositions come in several flavours and a good Canvas usually has a mix:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Functional: &lt;em&gt;“Fresh produce at the door every Wednesday without having to plan for it”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Problem-solving: &lt;em&gt;“No more panicked supermarket trip on a Tuesday night”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Emotional: &lt;em&gt;“Feel good about supporting local farms”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Economic: &lt;em&gt;“Better value than buying organic at the supermarket, including the time saved”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Features disguised as value. &lt;em&gt;“We have a mobile app”&lt;/em&gt; is a feature. &lt;em&gt;“Manage your subscription in thirty seconds from your phone”&lt;/em&gt; is a value proposition. Push for the benefit, not the mechanism.&lt;/li&gt;
  &lt;li&gt;Value that doesn’t match segment. If the segment is “busy professionals” and the value proposition is “learn about seasonal farming,” something is off. Challenge it: &lt;em&gt;“Would a busy professional sign up for this reason?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Founder passion masquerading as value. The founder may love supporting small farms; the customer may just want fresh produce at their door. Both can be true, but the Canvas should reflect the customer’s experienced value, not the founder’s internal motivation.&lt;/li&gt;
  &lt;li&gt;Too many value propositions per segment. If a segment has eight value propositions, the team doesn’t know which one is actually the reason the customer buys. Rank them and put the top two forward.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-3-channels-10-minutes&quot;&gt;Phase 3: Channels (10 minutes)&lt;/h4&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“How do we reach our customers? How do they hear about us, how do they decide to try us, how do we actually deliver the value to them, and how do we support them afterwards?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Channels include awareness, evaluation, purchase, delivery, and after-sales. A good Channels box covers all five phases, not just the sexy acquisition ones.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Only digital channels. For a physical product like a produce box, the delivery channel (courier, pick-up, post) is critical and often the biggest operational constraint. Don’t forget it.&lt;/li&gt;
  &lt;li&gt;Missing acquisition. The team knows how to deliver but has no plan for how customers will find them. That’s a gap worth flagging loudly.&lt;/li&gt;
  &lt;li&gt;Unreal channels. &lt;em&gt;“We’ll go viral on TikTok”&lt;/em&gt; is not a channel strategy, it’s a wish. Push: &lt;em&gt;“What specifically will we do on TikTok? Who runs the account? How do we measure whether it works?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Every channel is owned by the founder. That’s a scaling ceiling. Worth noting now, even if you don’t solve it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-4-customer-relationships-10-minutes&quot;&gt;Phase 4: Customer Relationships (10 minutes)&lt;/h4&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What kind of relationship do we maintain with each segment? How do we acquire them, keep them, and grow what they spend with us?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Relationships come in flavours: personal (dedicated account manager, farmer liaison), automated (emails, notifications, self-service), community (forums, social media groups, events), co-creation (customers help pick produce, vote on weekly boxes).&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Relationship / value proposition mismatch. If the value is personal connection to local farms but the relationship is entirely automated, something doesn’t fit. The customer signed up for connection and is getting a chatbot.&lt;/li&gt;
  &lt;li&gt;No retention strategy. Acquiring subscribers is expensive. How do you keep them? If nobody in the room has an answer, that’s a high-impact assumption to flag.&lt;/li&gt;
  &lt;li&gt;Every customer gets the same relationship. Different segments often need different relationships. A family subscriber and a corporate gift-giver behave differently and need to be managed differently.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-5-revenue-streams-10-minutes&quot;&gt;Phase 5: Revenue Streams (10 minutes)&lt;/h4&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What exactly are customers paying for, how do they pay, and how much? I want numbers, even if they’re rough.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pricing models include: subscription fees (weekly, monthly, quarterly), per-box pricing with different tiers, add-ons, gift subscriptions, one-off purchases, referral credits.&lt;/p&gt;

&lt;p&gt;Write each revenue stream as a sticky note with the price attached. &lt;em&gt;“$35 per box, weekly”&lt;/em&gt; not &lt;em&gt;“subscription fee.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Vague pricing. &lt;em&gt;“They’ll pay a fair price”&lt;/em&gt; is not a revenue stream. Push for numbers: &lt;em&gt;“If we had to set a price today, what would it be?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Only one revenue stream. Not necessarily wrong, but fragile. Are there adjacent revenue opportunities (add-ons, gifts, upgrades) the team hasn’t considered? At least note them.&lt;/li&gt;
  &lt;li&gt;Pricing that ignores willingness to pay. &lt;em&gt;“We need $50 per box to cover costs.”&lt;/em&gt; That’s a cost-plus position, not a market-led one. Note it; you’ll return to it in the coherence check.&lt;/li&gt;
  &lt;li&gt;Revenue shapes, not just revenue amounts. &lt;em&gt;“$35 per box, weekly”&lt;/em&gt; is different from &lt;em&gt;“$140 per month, billed on the first,”&lt;/em&gt; which is different again from &lt;em&gt;“$1600 per year with a renewal window.”&lt;/em&gt; The shape of the revenue determines the shape of the cost structure you need to cover, and which box the failure mode hides in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-6-key-activities-10-minutes&quot;&gt;Phase 6: Key Activities (10 minutes)&lt;/h4&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What are the most important things we must do to make this business work? Not every task; the essential, distinctive activities.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Activities might include: sourcing produce from farms, curating and packing boxes, operating delivery logistics, managing the subscriber platform, running customer acquisition marketing, handling support, navigating food safety regulations.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Listing every task in the business. Key activities are essential AND distinctive. “Payroll” is an activity but not a key one unless payroll is your business.&lt;/li&gt;
  &lt;li&gt;No mention of the hard things. The activities that are difficult AND essential are the ones that matter most. If sourcing seasonal produce at consistent quality is the hardest part of the business, it should be prominent in this box.&lt;/li&gt;
  &lt;li&gt;Forgetting acquisition as an activity. Teams treat sales and marketing as “things that happen” rather than activities the business must do well. If customer acquisition is hard, it belongs here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-7-key-resources-10-minutes&quot;&gt;Phase 7: Key Resources (10 minutes)&lt;/h4&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What do we need in order to deliver the value propositions? What assets are essential to this business model? Physical, intellectual, human, financial.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Resources include: physical (warehouse, refrigerated transport, packing equipment), intellectual (software, algorithms, brand, data, supplier relationships), human (team, expertise, farmer relationships), financial (capital, credit lines, working capital for perishable inventory).&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Forgetting people. Teams list technology and forget the farm relationships lead, the customer support agent, the on-call engineer, the person who drives the van at 5am. People are resources.&lt;/li&gt;
  &lt;li&gt;Aspirational resources. Don’t list what you wish you had; list what you actually need to make this work, and note which of those you don’t yet have.&lt;/li&gt;
  &lt;li&gt;Missing the non-obvious. &lt;em&gt;“Refrigerated storage”&lt;/em&gt; is obvious. &lt;em&gt;“A supplier network you trust enough to bet perishable inventory on”&lt;/em&gt; is less obvious and often more important.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-8-key-partners-10-minutes&quot;&gt;Phase 8: Key Partners (10 minutes)&lt;/h4&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Who do we depend on to make this work? Suppliers, partners, services we can’t deliver without? Who could break us if they disappeared?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Partners might include: farms and producers, delivery companies, payment processors, cloud providers, co-marketing partners, regulatory bodies.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Single points of failure. &lt;em&gt;“Our single farm partner supplies everything.”&lt;/em&gt; That’s a risk worth flagging. Same for a single delivery company or a single cloud provider.&lt;/li&gt;
  &lt;li&gt;Partners assumed but not secured. &lt;em&gt;“We’ll partner with local farms”&lt;/em&gt; is an assumption, not a partnership. Is there evidence the farms want to work with you?&lt;/li&gt;
  &lt;li&gt;Hidden partners. Payment processors, email providers, SMS gateways, the cloud provider. Easy to forget, easy to break the business when they fail or change pricing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-9-cost-structure-10-minutes&quot;&gt;Phase 9: Cost Structure (10 minutes)&lt;/h4&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What are the most significant costs in this business model? Fixed, variable, one-time. I want enough detail that when we look at the Revenue Streams box next to this one, we can tell whether the maths works.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Categorise:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fixed: rent, salaries, software subscriptions, insurance&lt;/li&gt;
  &lt;li&gt;Variable: produce, packaging, delivery, payment processing fees, wastage&lt;/li&gt;
  &lt;li&gt;One-time: initial equipment, software development, setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Missing costs. Teams forget customer acquisition costs, payment processing fees, wastage (unsold perishables), refunds, returns, support salaries, compliance, insurance.&lt;/li&gt;
  &lt;li&gt;Cost-per-unit vs fixed. Make sure the team separates variable from fixed. A $35 box with $25 of variable cost and $10,000 of monthly fixed cost is a very different business from one with $15 variable and $30,000 fixed.&lt;/li&gt;
  &lt;li&gt;The silent cost. The founder’s unpaid labour. At some point this becomes a real cost (a hired replacement); the Canvas should flag it even if it’s not being paid today.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-10-review-for-coherence-15-minutes&quot;&gt;Phase 10: Review for coherence (15 minutes)&lt;/h4&gt;

&lt;p&gt;Step back from the Canvas. This is the phase where the session earns its cost.&lt;/p&gt;

&lt;p&gt;Read each pair of boxes out loud, looking for contradictions:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Revenue Streams says $35 per box. Cost Structure says variable cost per box is $41. This model loses $6 every time we make a sale. Is that right?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Value Propositions says ‘personal connection to farms.’ Customer Relationships says ‘automated self-service.’ Are those consistent?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Customer Segments says ‘busy professionals.’ Channels says ‘farmers’ market stall.’ Do busy professionals go to farmers’ markets?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most Canvas sessions produce at least one coherence break. The value of the session is finding it.&lt;/p&gt;

&lt;p&gt;Once you’ve found the breaks, list them explicitly. Each one becomes an assumption worth testing or a strategic decision worth making. Add a sticky note in the margin of the Canvas for each break, so the photograph captures them.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The optimism spiral. Every box looks rosy. Force the question: &lt;em&gt;“What’s the weakest part of this Canvas? Which box are we least confident about?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;No contradictions found. Either the team has done excellent work, or they’re avoiding the hard look. Challenge them to read the specific numbers aloud: revenue minus variable cost, for example. The contradictions often hide in the arithmetic.&lt;/li&gt;
  &lt;li&gt;The empty box. If a box stayed mostly empty, that’s a signal. Either the team doesn’t know (valuable finding) or the model has a gap (also valuable finding). Don’t leave an empty box unflagged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href=&quot;/writing/business-model-canvas-does-this-actually-work/&quot;&gt;Business Model Canvas: Does This Actually Work?&lt;/a&gt; for the Greenbox team’s first Canvas session, including the moment the founder does the arithmetic between Revenue Streams and Cost Structure out loud and the room goes very quiet.&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What Can Go Wrong&lt;/h3&gt;

&lt;p&gt;The feature session. The team keeps listing product features in the Value Propositions box.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“Features go in Key Resources or Key Activities. Value Propositions is what the customer gets, not what we build.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The team can’t hold the distinction. The Canvas isn’t the right session yet; they need to finish Impact Mapping or Story Mapping first.&lt;/p&gt;

&lt;p&gt;The optimism spiral. Every box looks rosy.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“What’s the weakest part of this Canvas? Which box are we least confident about? Which assumption, if wrong, kills the business?”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The team refuses to identify a weak box. They’re not ready to be honest with themselves; the Canvas will be decorative.&lt;/p&gt;

&lt;p&gt;Analysis paralysis. Twenty minutes debating whether something is a Key Activity or a Key Resource.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“It doesn’t matter. Canvas is a thinking tool, not a taxonomy exercise. Best-fit box, move on.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The argument happens on a second box. The team is using classification to avoid the real conversation.&lt;/p&gt;

&lt;p&gt;The absent economics. Nobody in the room knows the actual costs or pricing.
  &lt;em&gt;Recovery:&lt;/em&gt; Fill those boxes with labelled estimates and flag them explicitly: &lt;em&gt;“These boxes are guesses. They go on the assumption list.”&lt;/em&gt; Continue the session.
  &lt;em&gt;Stop if:&lt;/em&gt; Four or more of the nine boxes are pure guesses. The Canvas is then mostly fiction; better to pause and do research first.&lt;/p&gt;

&lt;p&gt;The contradiction denial. The facilitator names a contradiction (cost exceeds revenue, channel doesn’t match segment) and the room brushes it off.
  &lt;em&gt;Recovery:&lt;/em&gt; Make the contradiction concrete: &lt;em&gt;“Let’s write the arithmetic on the wall. $35 minus $41 is minus $6 per box. Is that what we believe?”&lt;/em&gt; Numbers on the wall are harder to dismiss than numbers in the head.
  &lt;em&gt;Stop if:&lt;/em&gt; The room refuses to engage with the arithmetic. The session has produced its finding even if the team won’t accept it: record the contradiction and end.&lt;/p&gt;

&lt;p&gt;The wrong room. Halfway through, you realise the person who knows the cost structure isn’t in the room and nobody in the room can speak to it.
  &lt;em&gt;Recovery:&lt;/em&gt; Flag the box as unfinished, capture it as a to-do for a follow-up. Continue with the boxes the room can actually fill.
  &lt;em&gt;Stop if:&lt;/em&gt; Multiple boxes depend on absent people. Reschedule with the right invite list.&lt;/p&gt;

&lt;p&gt;The dominant founder. One person (usually the founder) talks every box, and the Canvas becomes their mental model rather than a shared one.
  &lt;em&gt;Recovery:&lt;/em&gt; Round-robin the next box. &lt;em&gt;“Let’s hear from the ops lead first on this one. Founder, hold your view until we’ve heard from everyone else.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The pattern survives a second redirect. The Canvas will reflect one person’s beliefs and won’t deliver shared literacy; better to address the dynamic outside the session.&lt;/p&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;

&lt;p&gt;The session ends; the work begins.&lt;/p&gt;

&lt;p&gt;Same day, the facilitator:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Photographs the Canvas from directly in front, and close-ups of each box. Make sure the notes are readable.&lt;/li&gt;
  &lt;li&gt;Transcribes the Canvas into a digital template (Miro, Mural, Figma, or a simple slide) with every sticky note captured.&lt;/li&gt;
  &lt;li&gt;Lists the contradictions and empty boxes found during the review phase, each as its own line item.&lt;/li&gt;
  &lt;li&gt;Sends the transcribed Canvas and the contradiction list to participants and relevant stakeholders.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This week, the founder:&lt;/p&gt;

&lt;p&gt;This is where the pattern earns its cost, and the work is mostly the founder’s. The Canvas is worthless if the contradictions aren’t resolved.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fix the arithmetic. If the revenue and cost numbers don’t work, they have to be made to work: by raising prices, cutting costs, changing the operational model, or abandoning the business. Sitting on a broken model is the most expensive option. The founder owns this call.&lt;/li&gt;
  &lt;li&gt;Run Assumption Mapping on the shaky boxes. Any box that was filled with guesses, or that was the source of a contradiction, needs its assumptions pulled apart. Book the &lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt; session for the next week.&lt;/li&gt;
  &lt;li&gt;Test the riskiest beliefs fast. Pricing, willingness to pay, cost per unit, churn rate, and customer acquisition cost are the five numbers that kill businesses quietly. If any of them are guesses, they’re the first things to validate in the real world, not in a spreadsheet.&lt;/li&gt;
  &lt;li&gt;Walk the Canvas to absent stakeholders. Anyone who should have been in the room but wasn’t gets a walk-through. Their challenges will either strengthen the Canvas or reveal problems the original group missed.&lt;/li&gt;
  &lt;li&gt;Use the Canvas to say no. Any new feature, initiative, or hire that doesn’t improve a box on the Canvas, or worse, makes a box harder, gets parked. The Canvas is the strategic filter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ongoing, the team:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Revisits the Canvas quarterly, or when the business model changes significantly. New segments, new pricing, new partners, new costs: each is a reason to update.&lt;/li&gt;
  &lt;li&gt;Keeps the photographed Canvas visible where strategic conversations happen. It’s the reference that prevents the slow drift back into feature-thinking.&lt;/li&gt;
  &lt;li&gt;When someone proposes a new initiative, asks them to point to the box it changes on the Canvas. If they can’t, the initiative is probably cost without coherence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;variants&quot;&gt;Variants&lt;/h3&gt;

&lt;p&gt;Standard BMC (default). Nine boxes, four to six people, around two hours, customer-first order. Output: a populated Canvas, a contradiction list, an assumption list. This is what most teams need, and the rest of this post describes it.&lt;/p&gt;

&lt;p&gt;Lean Canvas. Ash Maurya’s variant for early-stage problem validation. Replaces Key Partners, Key Activities, Key Resources, and Customer Relationships with Problem, Solution, Key Metrics, and Unfair Advantage. Reach for it when you’re earlier than BMC, when the question is “do we understand the problem well enough to build anything?” rather than “does this business hold together?” Lean Canvas before product-market fit; BMC once there’s something to articulate.&lt;/p&gt;

&lt;p&gt;Comparative Canvas. Fill two Canvases side by side for two business model options, &lt;em&gt;“subscription with weekly delivery”&lt;/em&gt; vs &lt;em&gt;“on-demand single-box purchase”&lt;/em&gt;, and read each pair of boxes across both Canvases. The contradictions surface faster because the alternative is right next to the option, not a hypothetical. Useful when the team is genuinely undecided between two strategic directions.&lt;/p&gt;

&lt;p&gt;Diagnostic Canvas. For an existing business that’s drifting, fill the Canvas as it actually is today, then a second Canvas as the team believed it was a year ago. The deltas, which boxes have quietly changed without anyone noticing, are usually where the drift lives. Pricing held while costs crept up. The original segment quietly shifted. The acquisition channel that worked at launch stopped working but wasn’t replaced.&lt;/p&gt;

&lt;p&gt;Remote. A Miro or Mural board with the nine-box template pinned, video call for the conversation. Slightly slower (the rhythm of &lt;em&gt;“write a sticky, place a sticky”&lt;/em&gt; is faster in person), but the structure transfers cleanly. Use one shared cursor: only the facilitator places stickies, prompted by the team, to keep the layout legible.&lt;/p&gt;

&lt;p&gt;Scaled (multi-business or multi-product). A company with several products or business lines runs one Canvas per line, then a master Canvas for the parent. Tensions between Canvases (shared resources, conflicting segments, channel cannibalisation) become visible at the parent level. Six hours total, ideally split across two days so the team can sleep on the first pass.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Time Is Wrong Everywhere All at Once</title>
    <link href="/writing/time-is-wrong-everywhere-all-at-once/"/>
    <updated>2026-06-04T06:00:00+08:00</updated>
    <id>/writing/time-is-wrong-everywhere-all-at-once/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The previous posts in this series covered &lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;how humans agree on time&lt;/a&gt;, &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;how clocks count it&lt;/a&gt;, &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;how physics bends it&lt;/a&gt;, &lt;a href=&quot;/writing/can-you-turn-back-time/&quot;&gt;whether you can travel through it&lt;/a&gt;, and &lt;a href=&quot;/writing/why-does-thursday-last-forever/&quot;&gt;why your brain gets it wrong&lt;/a&gt;. This post asks a more mundane but equally maddening question: how do computers agree on what time it is? The answer is that they don’t, not really, and the entire field of distributed systems is, in a sense, the study of what to do about that.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-fundamental-problem&quot;&gt;The fundamental problem&lt;/h3&gt;

&lt;p&gt;Two computers cannot agree on the time.&lt;/p&gt;

&lt;p&gt;This sounds like an engineering problem with an engineering solution: just synchronise the clocks. And we do. NTP (Network Time Protocol) has been synchronising clocks across the internet since 1985. A well-configured NTP client can keep its clock within a few milliseconds of UTC. That’s good enough for log files, cron jobs, and displaying the time on your screen.&lt;/p&gt;

&lt;p&gt;It’s not good enough for answering the question: “did event A happen before event B?”&lt;/p&gt;

&lt;p&gt;Take two servers, Alice and Bob. Alice receives an order at 14:00:00.003 by her clock. Bob processes a cancellation at 14:00:00.001 by his clock. Did the cancellation arrive before the order? If Alice’s clock is 5 milliseconds ahead of Bob’s, the order actually came first, but the timestamps say otherwise. Every distributed system that uses wall-clock timestamps to determine ordering is vulnerable to this. And it’s not a theoretical concern. It’s the kind of bug that causes duplicate charges, lost messages, and inventory discrepancies that take weeks to track down.&lt;/p&gt;

&lt;p&gt;The problem is fundamental, not technical. Even if you had perfect clocks (you don’t, &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;How Clocks Work&lt;/a&gt; explained why), the speed of light imposes an irreducible minimum delay on communication between machines. A signal from London to Sydney takes at least 50 milliseconds. During those 50 milliseconds, events can happen at both ends, and neither machine can know about the other’s events until the signal arrives. There is no way, not with better cables, not with faster processors, not with atomic clocks on every server, to create a globally consistent “now” across a distributed system. &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;Relativity&lt;/a&gt; says the same thing about the universe. Computer science says it about networks.&lt;/p&gt;

&lt;h3 id=&quot;lamport-clocks-forgetting-what-time-it-is&quot;&gt;Lamport clocks: forgetting what time it is&lt;/h3&gt;

&lt;p&gt;In 1978, Leslie Lamport published “Time, Clocks, and the Ordering of Events in a Distributed System.” It remains one of the most cited papers in computer science, and its core insight is deceptively simple: you don’t need to know what time it is. You only need to know what happened before what.&lt;/p&gt;

&lt;p&gt;A Lamport clock is not a clock in the physical sense. It’s a counter. Every process maintains its own counter. When a process does something, it increments its counter. When it sends a message, it attaches its current counter value. When it receives a message, it sets its counter to the maximum of its own counter and the received value, then increments.&lt;/p&gt;

&lt;p&gt;That’s it. No NTP. No atomic clocks. No synchronisation at all. The counter doesn’t represent a time. It represents a position in a causal sequence.&lt;/p&gt;

&lt;p&gt;The rule is: if event A causally precedes event B (A happened before B, and B could have been influenced by A), then A’s counter value is less than B’s. Lamport called this the “happened-before” relation. It’s a partial order, not every pair of events is comparable. If Alice does something and Bob does something at the same time with no communication between them, neither “happened before” the other. They’re concurrent. And that’s fine. The system doesn’t need to order them, because they couldn’t have influenced each other.&lt;/p&gt;

&lt;p&gt;It’s like a family tree. Your grandmother happened before you, there’s a clear causal chain. Your cousin in another country did things today that you know nothing about. Neither of you happened “before” the other. You’re concurrent. A family tree doesn’t need to put all the cousins in order. It only needs to know who descended from whom.&lt;/p&gt;

&lt;p&gt;Lamport clocks capture exactly this: causality, not chronology. They tell you “A could have caused B” or “A and B are independent.” They don’t tell you which happened first on a wall clock, because that question, in a distributed system, often has no meaningful answer.&lt;/p&gt;

&lt;h3 id=&quot;vector-clocks-who-knew-what-when&quot;&gt;Vector clocks: who knew what when&lt;/h3&gt;

&lt;p&gt;Lamport clocks have a limitation: if A’s counter is less than B’s, you know A &lt;em&gt;might&lt;/em&gt; have caused B, but you can’t be sure. The ordering is consistent with causality but doesn’t perfectly capture it. In 1988, Colin Fidge and Friedemann Mattern independently invented vector clocks, which fix this.&lt;/p&gt;

&lt;p&gt;A vector clock is an array of counters, one per process. When process Alice does something, she increments her entry. When she sends a message, she attaches the entire vector. When Bob receives it, he takes the element-wise maximum of his vector and Alice’s, then increments his own entry.&lt;/p&gt;

&lt;p&gt;The result: you can look at two vector timestamps and determine not just whether one &lt;em&gt;might&lt;/em&gt; have caused the other, but whether they’re definitely concurrent. If every entry in A’s vector is less than or equal to the corresponding entry in B’s vector, then A happened before B. If some entries are greater and some are less, they’re concurrent, neither caused the other.&lt;/p&gt;

&lt;p&gt;It’s like a group chat where everyone keeps a diary. Each diary entry notes what the writer did &lt;em&gt;and&lt;/em&gt; the last thing they heard from everyone else. If Alice’s diary says she’s seen Bob’s message #5 and Carol’s message #3, and Bob’s diary says he’s seen Alice’s message #2 and Carol’s message #4, you can reconstruct exactly who knew what when. Two entries are concurrent if neither person had seen the other’s latest update.&lt;/p&gt;

&lt;p&gt;Vector clocks are used in real systems. Amazon’s Dynamo database (the foundation of DynamoDB) used them to detect conflicting writes. Riak, a distributed key-value store, used them for the same purpose. They’re more expensive than Lamport clocks, the vector grows with the number of processes, but they give you something Lamport clocks can’t: a definitive answer about concurrency.&lt;/p&gt;

&lt;h3 id=&quot;the-cap-theorem-and-the-cost-of-consistency&quot;&gt;The CAP theorem and the cost of consistency&lt;/h3&gt;

&lt;p&gt;In 2000, Eric Brewer proposed (and in 2002, Seth Gilbert and Nancy Lynch proved) the CAP theorem: a distributed system can provide at most two of three guarantees:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Consistency: every read receives the most recent write.&lt;/li&gt;
  &lt;li&gt;Availability: every request receives a response.&lt;/li&gt;
  &lt;li&gt;Partition tolerance: the system continues to operate even if network messages between nodes are lost or delayed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since network partitions happen in real systems (cables get cut, switches fail, datacentres lose connectivity), you effectively have to choose between consistency and availability. You can’t have both when the network is broken.&lt;/p&gt;

&lt;p&gt;This is a theorem about time in disguise. “Consistency” means “every node agrees on the current state.” “Current” means “right now.” But “right now” across multiple machines separated by a network is the exact problem we started with. The CAP theorem is, at its heart, a formal proof that the speed of light makes global agreement expensive.&lt;/p&gt;

&lt;p&gt;CP systems (consistent, partition-tolerant) sacrifice availability: if the system can’t guarantee that all nodes agree, it refuses to answer rather than give a possibly-stale response. Traditional relational databases in a distributed setting often work this way. Your query might time out, but it won’t give you wrong data.&lt;/p&gt;

&lt;p&gt;AP systems (available, partition-tolerant) sacrifice consistency: every node answers every request, even if it means some nodes are serving stale data. Eventually, when the partition heals, the nodes reconcile. This is “eventual consistency”, the system &lt;em&gt;will&lt;/em&gt; converge to the correct state, but there’s a window where different nodes disagree. DynamoDB, Cassandra, and most eventually-consistent NoSQL databases work this way. Your query always gets an answer, but it might not be the latest answer.&lt;/p&gt;

&lt;p&gt;The choice between CP and AP is a choice about how to handle the impossibility of shared time. Do you pause and wait for agreement (CP), or do you keep going and sort it out later (AP)?&lt;/p&gt;

&lt;h3 id=&quot;google-spanner-buying-time-with-atomic-clocks&quot;&gt;Google Spanner: buying time with atomic clocks&lt;/h3&gt;

&lt;p&gt;In 2012, Google published a paper describing Spanner, a globally distributed database that appears to violate the CAP theorem. It offers strong consistency (every read sees the most recent write) across datacentres on different continents, with high availability. How?&lt;/p&gt;

&lt;p&gt;The trick is hardware. Google put GPS receivers and atomic clocks in every datacentre. Not NTP. Not “synchronise to a time server.” Actual atomic clocks, caesium and rubidium oscillators, sitting in the server racks, cross-checked against GPS signals. This gives each datacentre a clock that’s accurate to within about 7 milliseconds of true time, with known uncertainty bounds.&lt;/p&gt;

&lt;p&gt;Spanner uses an API called TrueTime, which doesn’t return a single timestamp. It returns an interval: “the current time is definitely between &lt;em&gt;earliest&lt;/em&gt; and &lt;em&gt;latest&lt;/em&gt;.” The interval is typically a few milliseconds wide. Every transaction gets a timestamp, and the system guarantees that if transaction A’s timestamp is before transaction B’s, then A actually happened before B in real time. If the system isn’t sure about the ordering, if the intervals overlap, it &lt;em&gt;waits&lt;/em&gt; until the uncertainty resolves. This is called “commit wait,” and it typically adds a few milliseconds to each transaction.&lt;/p&gt;

&lt;p&gt;Google is buying consistency with atomic clocks and patience. The speed of light still prevents perfect synchronisation, but by bounding the uncertainty and waiting it out, Spanner creates the illusion of a single global timeline. It’s not cheap, the atomic clocks, the GPS receivers, the global network, the engineering team that maintains all of it, but it works. It’s been running Google’s advertising system (among other things) since 2012.&lt;/p&gt;

&lt;p&gt;It’s like a courtroom. Two witnesses disagree about whether the red car or the blue car arrived first. In most distributed systems, you’d have to choose: either stop the trial until you can resolve the disagreement (CP), or let both witnesses testify and live with the inconsistency (AP). Spanner’s approach is different: give both witnesses a clock so precise that their testimony &lt;em&gt;overlaps only slightly&lt;/em&gt;, then pause just long enough for the overlap to resolve. The trial continues. The record is consistent. It costs you a good clock and a little patience.&lt;/p&gt;

&lt;h3 id=&quot;conflict-resolution-when-time-isnt-enough&quot;&gt;Conflict resolution: when time isn’t enough&lt;/h3&gt;

&lt;p&gt;Even with perfect clocks, distributed systems face a problem that time alone can’t solve: conflicting writes. Two users edit the same document at the same time. Two processes update the same database row. Two nodes accept contradicting requests during a network partition. What wins?&lt;/p&gt;

&lt;p&gt;Last-writer-wins (LWW) is the simplest policy: whichever write has the latest timestamp wins. It’s used widely. Cassandra defaults to it. It’s simple, deterministic, and almost always wrong. If Alice saves a document at 14:00:00.003 and Bob saves a different version at 14:00:00.005, Bob’s version wins and Alice’s changes vanish. Nobody is notified. The data loss is silent. If the clocks are even slightly wrong, the “wrong” write wins. LWW trades correctness for simplicity, and in many cases the trade is terrible.&lt;/p&gt;

&lt;p&gt;CRDTs (Conflict-Free Replicated Data Types) take a fundamentally different approach. Instead of asking “which write happened last?”, they design the data structure so that &lt;em&gt;all writes can be merged without conflict&lt;/em&gt;. A CRDT counter, for instance, tracks each node’s increments separately and sums them on read. Two nodes can increment independently, with no communication, and when they eventually sync, the counter is correct. No timestamps needed. No conflict resolution needed. The data type’s mathematical properties guarantee convergence.&lt;/p&gt;

&lt;p&gt;CRDTs work for counters, sets, registers, and certain kinds of text editing (Google Docs uses a CRDT-like approach for collaborative editing). They don’t work for everything, some operations are inherently conflicting (two users setting the same field to different values), and CRDTs can only merge what the data structure’s rules allow.&lt;/p&gt;

&lt;p&gt;Operational transformation (OT) is the older approach to the same problem, used by Google Docs before CRDTs and still used in many collaborative editors. OT transforms each operation against concurrent operations to produce a consistent result. If Alice inserts a character at position 5 and Bob deletes a character at position 3, the system transforms Alice’s insertion to account for Bob’s deletion: Alice’s insert moves to position 4. The result is the same regardless of the order the operations arrive.&lt;/p&gt;

&lt;p&gt;All of these techniques exist because time, even perfectly synchronised time, isn’t enough to resolve concurrent events. When two things happen at the same time, you need a &lt;em&gt;policy&lt;/em&gt;, not a clock.&lt;/p&gt;

&lt;h3 id=&quot;logical-time-in-practice&quot;&gt;Logical time in practice&lt;/h3&gt;

&lt;p&gt;The theoretical framework of Lamport clocks and vector clocks shows up in practical systems, often under different names:&lt;/p&gt;

&lt;p&gt;Version vectors in distributed databases (Riak, Dynamo) are vector clocks by another name. Each node maintains a counter, and the vectors are compared to detect conflicts. When a conflict is detected, the system either merges automatically (if it can) or presents both versions to the application for resolution.&lt;/p&gt;

&lt;p&gt;Sequence numbers in consensus protocols like Raft and Paxos are, at their core, Lamport clocks. Each proposal gets a monotonically increasing number. The ordering of proposals is determined by these numbers, not by wall-clock time. This is why consensus protocols work even when clocks disagree: they never consult a clock.&lt;/p&gt;

&lt;p&gt;Log-structured systems. Kafka, event sourcing architectures, blockchain, use an append-only log as their source of truth. The position in the log &lt;em&gt;is&lt;/em&gt; the logical time. Event #4,721 happened before event #4,722 because 4,721 &amp;lt; 4,722. No timestamps needed. The log imposes a total order. This is Lamport’s insight, made concrete.&lt;/p&gt;

&lt;p&gt;Even Git uses a form of logical time. A commit’s position in the DAG (directed acyclic graph) determines its causal relationship to other commits. Commit A is an ancestor of commit B. A happened before B. Two commits on different branches are concurrent. Git doesn’t care when they were created (the author date is just metadata). It cares about the graph structure. Causality, not chronology.&lt;/p&gt;

&lt;h3 id=&quot;the-speed-of-light-is-a-systems-problem&quot;&gt;The speed of light is a systems problem&lt;/h3&gt;

&lt;p&gt;Every problem in this post traces back to the same root cause: information takes time to travel. Light from London to Sydney: 50 milliseconds. A packet across a datacentre: maybe 0.5 milliseconds. A signal between two chips on the same board: nanoseconds. The delays are different, but they’re never zero, and as long as they’re not zero, two observers can’t agree on “now.”&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;relativity posts&lt;/a&gt; made this point about the universe. The speed of light means there’s no universal “now.” Simultaneity is relative. The block universe might be the correct picture: everything already exists, and our experience of “the present” is local and subjective.&lt;/p&gt;

&lt;p&gt;Distributed systems live in the same reality, just at a smaller scale. The speed of light in a fibre optic cable (about two-thirds the speed of light in vacuum) means that two servers in different datacentres can never share a “now.” They can get close. Google’s TrueTime gets within milliseconds, but “close” and “exact” are different things, and the gap between them is where bugs live.&lt;/p&gt;

&lt;p&gt;Leslie Lamport’s great insight was that you don’t have to solve this problem. You can &lt;em&gt;sidestep&lt;/em&gt; it. Stop asking “what time is it?” and start asking “what happened before what?” Stop synchronising clocks and start tracking causality. The universe can’t agree on “now” either. It gets along fine by tracking the causal structure of events, the light cones that determine what can influence what.&lt;/p&gt;

&lt;p&gt;Distributed computing reinvented the same solution, decades later, for the same reason. It turns out that &lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;the question&lt;/a&gt; we started this series with, “what time is it?”, is just as hard for computers as it is for physicists. And the answer, in both domains, is the same: it depends on who’s asking, and what they need to know.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Combining RAG and Fine-Tuning for a Legal Contract Assistant</title>
    <link href="/writing/combining-rag-and-fine-tuning-for-a-legal-contract-assistant/"/>
    <updated>2026-06-03T06:00:00+08:00</updated>
    <id>/writing/combining-rag-and-fine-tuning-for-a-legal-contract-assistant/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;Generative AI Developer Professional&lt;/strong&gt; · AIP-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A legal-technology startup is building a contract review assistant for a mid-sized commercial firm. The in-product model answers two shapes of question: &lt;em&gt;“What does this clause mean in the context of our past drafting?”&lt;/em&gt; and &lt;em&gt;“Where have we seen this indemnity construction before, and how did we negotiate it?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The constraints:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Corpus: ~200,000 past contracts, amendments, side letters, and internal case studies. Roughly 40 GB of text-heavy PDFs, Word documents, and Markdown notes after extraction. Growing by ~500 new matters a month.&lt;/li&gt;
  &lt;li&gt;Voice: every answer references clauses by section number (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;§3.2(b)&lt;/code&gt;), uses the firm’s preferred hedging (“the drafting is ambiguous on this point” rather than “this is unclear”), and cites internal precedents in the firm’s matter-number format.&lt;/li&gt;
  &lt;li&gt;Refusal: questions outside commercial contract law (tax, immigration, employment) get a structured decline with a pointer to the correct in-house team. Nothing off-domain.&lt;/li&gt;
  &lt;li&gt;Budget: AUD$100,000 end-to-end for customisation, data preparation, &lt;label for=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-training&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-training-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;training&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-training&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-training-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Training&lt;/span&gt;The process of fitting a model’s weights to data by minimising a loss function.
&lt;/span&gt;, evaluation, first quarter of &lt;label for=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt;.&lt;/li&gt;
  &lt;li&gt;Timeline: three months to a pilot with fee-earners.&lt;/li&gt;
  &lt;li&gt;Platform: Bedrock. Nothing self-hosted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;Three customisation levers are on the table, retrieval-augmented generation, supervised fine-tuning, continued pre-training, and the instinct to pick one of them is the mistake. The levers aren’t substitutes; they answer different questions. The first question is &lt;em&gt;what kind of problem is “be correct about 200,000 contracts”?&lt;/em&gt; It’s a retrieval problem. Facts about specific documents live in documents, not in weights, and any approach that tries to memorise 200,000 specific contracts is either astronomically expensive or silently unfaithful. That shape pushes the “what does the corpus say?” half of the design toward retrieval by default, and the choice of &lt;label for=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector store&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt; and &lt;label for=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embedding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; model becomes the interesting part.&lt;/p&gt;

&lt;p&gt;The second question is &lt;em&gt;what kind of problem is “sound like the firm”?&lt;/em&gt; It’s a behaviour problem. The firm’s voice is a set of rules, hedged phrasings, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;§&lt;/code&gt;-citations, matter-number formats, the polite decline when the question drifts into tax law. Rules about how to write aren’t facts; they’re patterns of output conditioned on input. Teaching those patterns through the &lt;label for=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-system-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-system-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;system prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-system-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-system-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;System prompt&lt;/span&gt;The instruction block that frames the model’s behaviour for a session, separate from the user’s messages.
&lt;/span&gt; works up to a point, and then starts drifting under adversarial phrasing or long conversations. Baking the rules into the weights via supervised fine-tuning means a short prompt is enough to invoke them and a jailbreak costs more than a system-prompt line to get around. That pushes the “how should the model say it?” half toward training, with the labelled dataset becoming the artefact that encodes the firm’s style guide as a training signal.&lt;/p&gt;

&lt;p&gt;The third is &lt;em&gt;what’s the planning horizon on each piece?&lt;/em&gt; The corpus grows by 500 matters a month. The style guide changes when a senior partner wins an argument about hedging. The refusal list changes when a user finds a new way to ask about divorce. A two-person platform team can absorb weekly ingestion (ingest jobs on object-storage events) and quarterly fine-tune refreshes (lawyer curates deltas, trigger a training run) but cannot absorb monthly retrains of anything that reads 40 GB. That cadence asymmetry is the strongest argument against continued pre-training in this project: its refresh cycle is weeks, not days, and its cost is per-token-processed on an unlabelled 40 GB corpus. The pay-off exists only when the base model’s vocabulary is genuinely wrong, and commercial contract English is squarely inside what a modern hosted model has already read.&lt;/p&gt;

&lt;p&gt;The fourth is &lt;em&gt;where does the budget actually get spent?&lt;/em&gt; AUD$100K in three months looks like training compute at first glance and turns out to be hosting commitments on inspection. Custom-trained models on a managed-model platform typically can’t be served on the standard pay-per-token rate, they need a reserved-capacity commitment, and that is the line item most often under-estimated. The budget shape for any approach that ships custom weights is low-training plus high-fixed-serving, and the architectural consequence is that fine-tuning earns its place only when the behaviour change is worth the always-on hourly burn. A pure retrieval approach has a different cost shape: low-fixed plus variable-per-query, which is correct for a pilot with light traffic.&lt;/p&gt;

&lt;p&gt;The fifth is &lt;em&gt;what does a wrong answer look like and who catches it?&lt;/em&gt; A model that gets the voice correct but hallucinates clause numbers is worse than an un-tuned model that cites faithfully. The evaluation harness has to score citation faithfulness (every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;§&lt;/code&gt; reference traces back to a retrieved chunk) separately from voice (did the model write like a partner?) because the two signals tell the team different things, citation faithfulness moves when retrieval changes, voice moves when training drifts. Without separate scores the team can’t tell which half to fix.&lt;/p&gt;

&lt;p&gt;Finally: &lt;em&gt;what buys the right to change our mind?&lt;/em&gt; A retrieval-only baseline ships in weeks and answers faithfully but boringly. Adding a fine-tune on top adds voice without re-doing the retrieval. If the firm decides in year two that Welsh property law has become a practice area, the retrieval corpus picks up the documents immediately and the fine-tune picks up the phrasing on the next quarterly refresh. If instead the team had picked continued pre-training, adding a new sub-domain would mean another round of training on another tranche of unlabelled text.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Five filters to score the landscape against.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Corpus &lt;label for=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-grounding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-grounding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;grounding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-grounding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-grounding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Grounding&lt;/span&gt;Constraining a model to answer from provided sources rather than from whatever it absorbed during training.
&lt;/span&gt;. Two hundred thousand documents the model has never seen, with new ones arriving weekly. The answer has to reflect the current corpus, not a snapshot frozen at training time.&lt;/li&gt;
  &lt;li&gt;Voice and format. The firm’s phrasing and citation style are &lt;em&gt;rules about how to write&lt;/em&gt;, not &lt;em&gt;facts about the world&lt;/em&gt;. The model needs to internalise them so a prompt doesn’t re-teach them every turn.&lt;/li&gt;
  &lt;li&gt;Refusal. Off-domain questions must be declined in a structured way. A behavioural policy that has to hold under adversarial prompting.&lt;/li&gt;
  &lt;li&gt;Budget and timeline. AUD$100K and 90 days. Any method that blows either is out.&lt;/li&gt;
  &lt;li&gt;Maintainability. A two-person platform team. Customisation has to be refreshable when the corpus grows or the style guide changes, without a full retrain every time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-customisation-landscape&quot;&gt;The customisation landscape&lt;/h3&gt;

&lt;p&gt;Bedrock gives five levers that could plausibly shape model behaviour.&lt;/p&gt;

&lt;p&gt;Prompt engineering alone. Cheapest. System prompt with the style guide, few-shot examples, refusal instructions. Works well for voice and refusal when the base model is capable. Claude Sonnet follows detailed style instructions to a fault. Fails the corpus attribute: 200,000 documents don’t fit in any prompt.&lt;/p&gt;

&lt;p&gt;Retrieval-augmented generation. The corpus lives in a vector store; every question retrieves relevant chunks, and those chunks ride into the prompt alongside the user’s question. Facts stay outside the weights, updating the corpus is an ingestion job, not a training job. Citations fall out naturally because the model knows which chunk each claim came from. On Bedrock: Knowledge Bases plus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt;, backed by OpenSearch Serverless, Aurora pgvector, S3 Vectors, or third-party stores.&lt;/p&gt;

&lt;p&gt;Supervised fine-tuning. Show a base model a labelled dataset of (prompt, ideal response) pairs; adjust weights so outputs move closer to the ideal. On Bedrock: Claude 3 Haiku (us-west-2), Meta Llama 3.1 / 3.2 / 3.3 across 1B-70B, Amazon Nova Micro / Lite / Pro, plus Titan Text. Writes a custom model that must be served via provisioned throughput, on-demand isn’t available. Training cost is modest (Llama 2 70B fine-tune training is ~$0.00799 per 1,000 &lt;label for=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;tokens&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-combining-rag-and-fine-tuning-for-a-legal-contract-assistant-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;; custom model storage $1.95/month). Teaches style, format, and behaviour; does not reliably teach facts.&lt;/p&gt;

&lt;p&gt;Continued pre-training. Keep training a base model on a large body of unlabelled domain text using the same objective that originally pre-trained it. Shifts the model’s distribution of language toward the domain. Historically supported on Amazon Titan Text; not on Claude, Llama, or Nova. Heavyweight; training cost proportional to tokens processed; output still needs provisioned throughput to serve.&lt;/p&gt;

&lt;p&gt;Bedrock Custom Model Import. Bring weights trained elsewhere (Llama / Mistral / compatible architectures) and serve them through the Bedrock API. Provisioned-only; us-east-1 and us-west-2. A packaging choice, not a fresh customisation lever.&lt;/p&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Lever&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Corpus grounding&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Voice &amp;amp; format&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Refusal behaviour&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Budget/timeline&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Maintainability&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Prompt engineering alone&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RAG (Knowledge Bases)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Supervised fine-tuning&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Continued pre-training&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Custom Model Import&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;No single lever clears all five. Two stacked clear all five: RAG for the corpus, fine-tuning for voice and refusal.&lt;/p&gt;

&lt;h3 id=&quot;matching-the-levers-to-the-question&quot;&gt;Matching the levers to the question&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 640&quot; style=&quot;max-width: 100%; height: auto; font-family: system-ui, -apple-system, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Three customisation levers answer three distinct questions. RAG pulls corpus facts into the prompt at inference. Fine-tuning bakes voice and refusal into the weights offline so inference prompts can be lighter. Continued pre-training shifts the base distribution, not needed here. The picked pair — RAG plus fine-tune — wraps Claude Haiku served on provisioned throughput, pulling retrieved chunks from OpenSearch Serverless and emitting answers with §-citations.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .cpv-bg         { fill: rgba(183, 138, 42, 0.05); stroke: rgba(183, 138, 42, 0.45); stroke-width: 2; }
      .cpv-q          { fill: #fff; stroke: #3a5fb5; stroke-width: 1.8; }
      .cpv-lever-rag  { fill: rgba(58, 95, 181, 0.1); stroke: #3a5fb5; stroke-width: 1.8; }
      .cpv-lever-sft  { fill: rgba(47, 125, 74, 0.12); stroke: #2f7d4a; stroke-width: 1.8; }
      .cpv-lever-cpt  { fill: rgba(168, 74, 42, 0.08); stroke: rgba(168, 74, 42, 0.7); stroke-width: 1.3; stroke-dasharray: 5 3; }
      .cpv-stack      { fill: rgba(183, 138, 42, 0.14); stroke: rgba(183, 138, 42, 0.9); stroke-width: 2; }
      .cpv-title      { font-size: 15px; font-weight: 700; fill: #222; }
      .cpv-q-title    { font-size: 14px; font-weight: 600; fill: #333; font-style: italic; }
      .cpv-detail     { font-size: 12px; fill: #333; }
      .cpv-tag        { font-size: 11px; fill: #555; font-style: italic; }
      .cpv-arrow      { fill: none; stroke: #555; stroke-width: 1.6; }
      .cpv-arrow-skip { fill: none; stroke: #bbb; stroke-width: 1.2; stroke-dasharray: 4 3; }
    &lt;/style&gt;
    &lt;marker id=&quot;cpv-head&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;cpv-head-skip&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#bbb&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;1060&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;cpv-bg&quot; /&gt;

  &lt;rect x=&quot;60&quot; y=&quot;60&quot; width=&quot;300&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;cpv-q&quot; /&gt;
  &lt;text x=&quot;210&quot; y=&quot;86&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-q-title&quot;&gt;&quot;What does the corpus say?&quot;&lt;/text&gt;
  &lt;text x=&quot;210&quot; y=&quot;106&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;200K contracts, growing weekly&lt;/text&gt;

  &lt;rect x=&quot;400&quot; y=&quot;60&quot; width=&quot;300&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;cpv-q&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;86&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-q-title&quot;&gt;&quot;How should the model say it?&quot;&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;106&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;voice, §-citations, refusals&lt;/text&gt;

  &lt;rect x=&quot;740&quot; y=&quot;60&quot; width=&quot;300&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;cpv-q&quot; /&gt;
  &lt;text x=&quot;890&quot; y=&quot;86&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-q-title&quot;&gt;&quot;What vocabulary does it know?&quot;&lt;/text&gt;
  &lt;text x=&quot;890&quot; y=&quot;106&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;commercial contract English, already fine&lt;/text&gt;

  &lt;path d=&quot;M210,120 L210,170&quot; class=&quot;cpv-arrow&quot; marker-end=&quot;url(#cpv-head)&quot; /&gt;
  &lt;path d=&quot;M550,120 L550,170&quot; class=&quot;cpv-arrow&quot; marker-end=&quot;url(#cpv-head)&quot; /&gt;
  &lt;path d=&quot;M890,120 L890,170&quot; class=&quot;cpv-arrow-skip&quot; marker-end=&quot;url(#cpv-head-skip)&quot; /&gt;

  &lt;rect x=&quot;60&quot; y=&quot;170&quot; width=&quot;300&quot; height=&quot;120&quot; rx=&quot;6&quot; class=&quot;cpv-lever-rag&quot; /&gt;
  &lt;text x=&quot;210&quot; y=&quot;196&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-title&quot;&gt;RAG&lt;/text&gt;
  &lt;text x=&quot;210&quot; y=&quot;218&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;Knowledge Bases on OpenSearch Serverless&lt;/text&gt;
  &lt;text x=&quot;210&quot; y=&quot;236&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;Titan V2 1,024-dim, hierarchical chunks&lt;/text&gt;
  &lt;text x=&quot;210&quot; y=&quot;256&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;ingestion = config change,&lt;/text&gt;
  &lt;text x=&quot;210&quot; y=&quot;272&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;not a training job&lt;/text&gt;

  &lt;rect x=&quot;400&quot; y=&quot;170&quot; width=&quot;300&quot; height=&quot;120&quot; rx=&quot;6&quot; class=&quot;cpv-lever-sft&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;196&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-title&quot;&gt;Supervised fine-tuning&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;218&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;Claude 3 Haiku (us-west-2)&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;236&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;~1,500 (prompt, ideal) pairs&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;256&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;custom model = provisioned&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;272&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;throughput only&lt;/text&gt;

  &lt;rect x=&quot;740&quot; y=&quot;170&quot; width=&quot;300&quot; height=&quot;120&quot; rx=&quot;6&quot; class=&quot;cpv-lever-cpt&quot; /&gt;
  &lt;text x=&quot;890&quot; y=&quot;196&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-title&quot;&gt;Continued pre-training&lt;/text&gt;
  &lt;text x=&quot;890&quot; y=&quot;218&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;Titan Text on raw 40 GB&lt;/text&gt;
  &lt;text x=&quot;890&quot; y=&quot;236&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;priced per training token&lt;/text&gt;
  &lt;text x=&quot;890&quot; y=&quot;256&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;parked: base vocab is already correct;&lt;/text&gt;
  &lt;text x=&quot;890&quot; y=&quot;272&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;budget can&apos;t absorb it&lt;/text&gt;

  &lt;path d=&quot;M210,290 L420,410&quot; class=&quot;cpv-arrow&quot; marker-end=&quot;url(#cpv-head)&quot; /&gt;
  &lt;path d=&quot;M550,290 L550,410&quot; class=&quot;cpv-arrow&quot; marker-end=&quot;url(#cpv-head)&quot; /&gt;
  &lt;path d=&quot;M890,290 L680,410&quot; class=&quot;cpv-arrow-skip&quot; marker-end=&quot;url(#cpv-head-skip)&quot; /&gt;

  &lt;rect x=&quot;300&quot; y=&quot;410&quot; width=&quot;500&quot; height=&quot;190&quot; rx=&quot;10&quot; class=&quot;cpv-stack&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;438&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-title&quot;&gt;Fine-tuned Haiku served on provisioned throughput&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;462&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;behind RetrieveAndGenerate&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;486&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;corpus chunks pulled at inference;&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;504&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-detail&quot;&gt;voice + refusal already in the weights&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;534&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;~AUD$74-86K of AUD$100K, room for evaluation&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;552&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;and one iteration cycle after fee-earner feedback&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;582&quot; text-anchor=&quot;middle&quot; class=&quot;cpv-tag&quot;&gt;RAG refresh = weekly cron. Fine-tune refresh = quarterly.&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary); margin-top: 0.5em;&quot;&gt;Three questions, three levers, two picked. The RAG path pulls corpus facts in at inference; the fine-tune path bakes voice and refusal into weights offline. Continued pre-training stays parked, the base vocabulary is already correct, and the budget can&apos;t carry it alongside the two that earn their place.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-rag--fine-tune-split-in-depth&quot;&gt;The RAG + fine-tune split, in depth&lt;/h3&gt;

&lt;p&gt;The instinct to pick &lt;em&gt;one&lt;/em&gt; customisation method comes from treating them as interchangeable. They aren’t. Each answers a different question.&lt;/p&gt;

&lt;p&gt;RAG answers &lt;em&gt;“what does the corpus say?”&lt;/em&gt; Facts about 200,000 specific contracts live in the vector store. A question about a force-majeure clause retrieves the dozen most relevant past instances; the model reads them at inference time and reasons about them. Adding a new matter is an ingestion job, the vector store grows by one document, the model doesn’t change. Removing a retracted matter is a delete on a few vectors. The corpus is a living index, not a snapshot baked into weights.&lt;/p&gt;

&lt;p&gt;Fine-tuning answers &lt;em&gt;“how should the model say it?”&lt;/em&gt; The firm’s voice, hedged, precise, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;§&lt;/code&gt;-citing, is a set of stylistic rules. A few hundred labelled examples teach the model those rules in its weights. After fine-tuning, a twenty-line system prompt produces voice-compliant answers where an un-tuned model would need two hundred lines of style-guide text and still drift under pressure.&lt;/p&gt;

&lt;p&gt;Continued pre-training answers &lt;em&gt;“what vocabulary does the model know?”&lt;/em&gt; Useful when the base model genuinely doesn’t speak the domain’s language, regulatory filings in a rare jurisdiction, argot from a century-old trade, notation from a narrow sub-field. Commercial contract English doesn’t qualify. Claude has read plenty of contracts.&lt;/p&gt;

&lt;p&gt;The three aren’t substitutes, they stack. A fully-customised model in a demanding domain might do all three: CPT on domain text, fine-tune on (prompt, response) pairs, then wrap in RAG at inference. For this situation, two of the three clear every attribute and the third is overkill.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-decision-trace&quot;&gt;A worked decision trace&lt;/h3&gt;

&lt;p&gt;Attribute 1, 200,000-document corpus. RAG ingests into OpenSearch Serverless via Knowledge Bases. Titan Text Embeddings V2 at 1,024 dimensions. Hierarchical chunking, child ~300 tokens for retrieval precision, parent ~1,500 tokens for generator context. Metadata sidecars tag each document with matter number, practice area, and client. Weekly refresh via EventBridge calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StartIngestionJob&lt;/code&gt;; deltas only. Fine-tuning doesn’t touch this, the fine-tuned model calls the same vector store as an un-tuned one.&lt;/p&gt;

&lt;p&gt;Attribute 2, voice and citation format. A lawyer-in-the-loop curates ~1,500 (prompt, ideal-response) pairs over four to six weeks. Each pair is a real question-and-answer exchange, reviewed and edited to the firm’s style guide: hedged phrasing, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;§X.Y(z)&lt;/code&gt; references, matter-number citations. The dataset trains Claude 3 Haiku via Bedrock fine-tuning in us-west-2, the only Claude option for fine-tuning today. Llama 3.3 70B would be the alternative if quality required it; a fine-tuned 70B on provisioned throughput is materially more expensive per hour, and Haiku should clear the bar.&lt;/p&gt;

&lt;p&gt;Attribute 3, refusal on off-domain questions. A subset, perhaps 300 of the 1,500 pairs, are refusal examples. Fine-tuning bakes this into the weights. The system prompt reinforces it; default behaviour under a prompt-injection attempt holds much better than a prompt-only approach would.&lt;/p&gt;

&lt;p&gt;Attribute 4. AUD$100K and 90 days. Budget pass below. Both methods fit; CPT doesn’t.&lt;/p&gt;

&lt;p&gt;Attribute 5, maintainability. RAG updates are ingestion; no retrain needed when a new matter lands. Fine-tuning refreshes happen quarterly, when the style guide evolves or refusal patterns grow. A two-person platform team runs ingestion continuously and the fine-tune four times a year.&lt;/p&gt;

&lt;h3 id=&quot;cost-shape-where-the-dollars-land&quot;&gt;Cost shape: where the dollars land&lt;/h3&gt;

&lt;p&gt;The cost profile differs in &lt;em&gt;shape&lt;/em&gt;, not just size.&lt;/p&gt;

&lt;p&gt;RAG: low fixed, variable with queries. One-off ingestion cost (embedding 40 GB at Titan V2’s per-token rate, a few thousand dollars, plus incremental weekly deltas), baseline vector-store cost (OpenSearch Serverless at 2-OCU minimum, ~AUD$520/month), per-query embedding plus generation cost.&lt;/p&gt;

&lt;p&gt;Fine-tuning: low training, high fixed serving. Training a Haiku fine-tune on 1,500 pairs runs in the low hundreds of dollars; custom model storage $1.95/month. The catch is serving: fine-tuned models run on provisioned throughput only, a minimum hourly burn from deployment. Haiku-tier MUs are cheaper than the Llama 2 70B reference ($21.18/hour, ~$15,750/month on a 1-month commit) but still add up to several thousand dollars a month.&lt;/p&gt;

&lt;p&gt;Continued pre-training: high training &lt;em&gt;and&lt;/em&gt; high fixed serving. Pricing is per token processed; at 40 GB raw text (~10 billion tokens), one pass is a serious bill before fine-tuning or evaluation begin.&lt;/p&gt;

&lt;p&gt;Budget pass, AUD:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Data preparation. PDF extraction, chunking pipeline, metadata tagging, the 1,500-pair dataset curated by a lawyer: ~AUD$30K.&lt;/li&gt;
  &lt;li&gt;RAG ingestion + 2-OCU OpenSearch Serverless for three months: ~AUD$6K.&lt;/li&gt;
  &lt;li&gt;Fine-tune training plus iteration cycles: ~AUD$2K.&lt;/li&gt;
  &lt;li&gt;Provisioned throughput for the fine-tuned Haiku, three months: ~AUD$30-40K.&lt;/li&gt;
  &lt;li&gt;Bedrock Evaluations weekly against a 200-question golden set: ~AUD$4K.&lt;/li&gt;
  &lt;li&gt;Generation cost for the pilot at low query volume: ~AUD$2-4K.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: ~AUD$74-86K of AUD$100K, headroom for a Sonnet evaluation judge and a round of iteration.&lt;/p&gt;

&lt;h3 id=&quot;evaluation-the-quiet-third-leg&quot;&gt;Evaluation: the quiet third leg&lt;/h3&gt;

&lt;p&gt;A contract review assistant that gets the voice correct but hallucinates clauses is worse than one that gets the voice vaguely correct but cites faithfully. Evaluation matters as much as the customisation choice.&lt;/p&gt;

&lt;p&gt;The golden dataset: ~200 real questions from the firm’s advice history, with expected answers reviewed by a senior lawyer. Refreshed quarterly. Includes questions the system should refuse.&lt;/p&gt;

&lt;p&gt;Automatic metrics via Bedrock Evaluations: citation faithfulness (every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;§&lt;/code&gt; reference traces back to a retrieved chunk), answer accuracy against the lawyer-reviewed reference, and refusal correctness. Citation faithfulness tells you whether RAG is doing its job; refusal correctness tells you whether fine-tuning is doing its job.&lt;/p&gt;

&lt;p&gt;Human review: a weekly spot check by a senior lawyer on a random sample, scoring on “would I have said it this way?” When rubric scores drop, the fine-tune dataset needs refreshing; when citation faithfulness drops, retrieval is returning the wrong chunks.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Three customisation methods answer three different questions. RAG: what does the corpus say? Fine-tuning: how should the model say it? CPT: what vocabulary does it know? Treating them as substitutes leads to picking wrong.&lt;/li&gt;
  &lt;li&gt;RAG via Bedrock Knowledge Bases handles 200K-document corpora with weekly updates through incremental ingestion, no retrain required. Citations fall out of retrieval, not out of weights.&lt;/li&gt;
  &lt;li&gt;Supervised fine-tuning on Bedrock supports Claude 3 Haiku (us-west-2), Meta Llama 3.1 / 3.2 / 3.3, Amazon Nova Micro / Lite / Pro, and Amazon Titan. Not Sonnet, not Opus, not Llama 4 MoE.&lt;/li&gt;
  &lt;li&gt;Fine-tuned custom models must be served via provisioned throughput. On-demand isn’t available. The minimum hourly commitment is the line item that most often blows a customisation budget.&lt;/li&gt;
  &lt;li&gt;Continued pre-training uses unlabelled text and the base pre-training objective to shift the model’s language distribution. Heavyweight, priced per training token, still needs provisioned throughput. Correct when base vocabulary is wrong; wrong when the corpus is just &lt;em&gt;more of what the base already reads&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;Cost shapes differ. RAG: low fixed, variable with queries. Fine-tuning: low training, high fixed serving. CPT: high training &lt;em&gt;and&lt;/em&gt; high fixed serving. Budget discipline comes from knowing the shape, not just the sticker price.&lt;/li&gt;
  &lt;li&gt;Custom Model Import packages an externally-trained model into Bedrock’s inference surface, a deployment choice, not a customisation method. Provisioned-only; us-east-1 and us-west-2 only.&lt;/li&gt;
  &lt;li&gt;Evaluation is the third leg. Bedrock Evaluations for automatic citation faithfulness, accuracy, and refusal correctness; human review for voice. Without it, neither RAG nor fine-tuning is maintainable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answer: Bedrock Knowledge Bases for RAG over the 200,000-document corpus. Titan Text Embeddings V2 at 1,024 dimensions, hierarchical chunking, metadata filtering by matter number and practice area, weekly incremental ingestion from S3. Supervised fine-tuning of Claude 3 Haiku in us-west-2 on ~1,500 lawyer-curated (prompt, response) pairs covering voice, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;§&lt;/code&gt;-citation format, and structured refusals. The fine-tuned Haiku serves via provisioned throughput behind &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt;, so every inference call pulls relevant chunks from the knowledge base and hands them to a model that already knows how to write in the firm’s voice. Continued pre-training is parked, the sub-domain doesn’t need it, and the budget can’t afford it alongside fine-tuning and RAG. Evaluation runs weekly. RAG for the &lt;em&gt;what&lt;/em&gt;, fine-tuning for the &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>How to Build a Citations-Required RAG Over 50K Internal Documents</title>
    <link href="/writing/how-to-build-a-citations-required-rag-over-50k-internal-documents/"/>
    <updated>2026-06-01T06:00:00+08:00</updated>
    <id>/writing/how-to-build-a-citations-required-rag-over-50k-internal-documents/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;Generative AI Developer Professional&lt;/strong&gt; · AIP-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A 6,000-person enterprise is standing up an internal assistant. The corpus is ~50,000 documents across four domains. HR policies, engineering runbooks, security guidelines, product specs, totalling ~5 GB of mostly text-dense PDFs, Markdown, Word, and Confluence exports. New documents land weekly, old ones get superseded, a handful are retracted. The assistant has to reflect the current state within a day of a change.&lt;/p&gt;

&lt;p&gt;On the answer path:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;P95 end-to-end latency &amp;lt; 3 s from question to last &lt;label for=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;token&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;, across retrieval, generation, and network.&lt;/li&gt;
  &lt;li&gt;Document-level access control. An engineer asking “what are the band-5 engineering salaries?” must get a polite refusal, not an HR document. A security auditor asking about an incident-response runbook gets the runbook. Identity drives what the retriever can see.&lt;/li&gt;
  &lt;li&gt;Citations on every answer. Every factual claim points back to a source chunk. No citation, no answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;A RAG system lives or dies at the boundary where identity meets retrieval, so the first question is &lt;em&gt;who owns that boundary?&lt;/em&gt; A product team that ships “the assistant” without owning the access-control fabric under it is building a compliance incident with a generative front-end. The design has to make the seam explicit: identity in, filter out, retriever sees only what the caller is allowed to see. Anywhere else in the stack is the wrong place to apply the check, filtering results after retrieval leaves the top-K polluted with chunks the user can’t read, and filtering at generation leaves the citation hanging off something the user shouldn’t have seen in the first place.&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;what’s the blast radius of a bad answer?&lt;/em&gt; An engineer who asks about someone else’s salary and gets a careful decline is fine. An engineer who asks about someone else’s salary and gets the answer is a wrongful-disclosure incident, and the remediation isn’t a &lt;label for=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; tweak, it’s legal notice, HR escalation, and a six-month trust deficit with the workforce that was just asked to share more data with the tool. The cost of a single leakage dominates every other cost on the project. That shape pushes the design toward managed components where the access-control path is a first-class API, not a piece of glue the team maintains.&lt;/p&gt;

&lt;p&gt;The third is &lt;em&gt;what’s the cost curve as the corpus grows?&lt;/em&gt; Five gigabytes today, seven next year, thirty when the internal wiki finally gets ingested. The ingestion story has to be incremental by default, a full weekly reprocess of 5 GB is doable, a full weekly reprocess of 30 GB eats the evening. The &lt;label for=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector store&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt; bill scales with vector dimensions × chunks × replicas, so the &lt;label for=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embedding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-build-a-citations-required-rag-over-50k-internal-documents-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; model choice locks in a multi-year storage footprint. Changing embedding models means reindexing everything, so whatever dimension trade-off gets baked in at install time is the one the team lives with, cheap to choose, expensive to reverse.&lt;/p&gt;

&lt;p&gt;The fourth is &lt;em&gt;what are the failure modes we have to design against?&lt;/em&gt; A citation the user can’t load because the S3 object is gated by a different policy. A retrieval that returns zero chunks for a legitimate question because the filter is too tight. A chunking strategy that slices a procedure in half and leaves the generator stitching two halves of two runbooks together. A metadata-sidecar path where a file was added without its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.metadata.json&lt;/code&gt; and therefore has no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allowed_groups&lt;/code&gt;, defaulting to nobody or everybody depending on how the filter is composed. Each of those wants a test, a runbook, and a monitoring line, the managed service takes care of about half; the application team owns the other half.&lt;/p&gt;

&lt;p&gt;The fifth is &lt;em&gt;where does a small platform team want to spend its operational attention?&lt;/em&gt; Not on owning a vector-store operator, not on writing chunking pipelines, not on re-implementing citation extraction for the fourth time. Managed services buy back that attention at the cost of flexibility; the trade is good when the workload is standard and bad when it has a weird shape. A 50K-document corpus with vanilla group-based access control is standard. A SOX-grade audit requirement with multi-hop ACL joins is weird and wants SQL.&lt;/p&gt;

&lt;p&gt;Finally: &lt;em&gt;what does “current state” mean in practice?&lt;/em&gt; The brief says “within a day” but the business will discover it means “within an hour” the first time a retracted policy keeps answering questions. The ingestion cadence has to scale from weekly-cron down to per-object event without re-architecting, because the product requirement will tighten under production pressure.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Five filters, and the landscape either clears them or doesn’t.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Document-level access control enforced during retrieval. Not a post-hoc scrub of results, otherwise the top-K is polluted with chunks the user can’t see and quality collapses.&lt;/li&gt;
  &lt;li&gt;Sub-3-second end-to-end latency at P95. Retrieval under a second, generation streamed, first tokens visible to the user inside one.&lt;/li&gt;
  &lt;li&gt;Citations that survive the model summarising or paraphrasing. The generation path has to propagate “which chunk came from which document” all the way to the response.&lt;/li&gt;
  &lt;li&gt;Incremental weekly ingestion. New files picked up, changed files re-embedded, deleted files removed. Not a full weekly reprocess of 5 GB.&lt;/li&gt;
  &lt;li&gt;Reasonable operational overhead. A small platform team. Managed components where the differentiation isn’t worth hand-rolling.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-rag-architecture-landscape&quot;&gt;The RAG architecture landscape&lt;/h3&gt;

&lt;p&gt;Five plausible shapes on AWS.&lt;/p&gt;

&lt;p&gt;Fine-tune a foundation model on the corpus. No retrieval at all, the knowledge goes into the weights. Weekly refresh means weekly fine-tune cycles at 5-GB scale. Citations are impossible because fine-tuning merges sources into weights with no pointer back. Per-user access control is impossible because once a chunk is in the weights, every user sees it.&lt;/p&gt;

&lt;p&gt;Bedrock Knowledge Bases. A managed RAG service that ingests documents from a data source (S3, SharePoint, Confluence, Salesforce, web crawler, custom), chunks them, embeds them through a chosen model, stores the vectors, and exposes two runtime APIs – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Retrieve&lt;/code&gt; for raw chunks and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; for the full round-trip with citations. Eight supported vector stores: OpenSearch Serverless, OpenSearch managed clusters, S3 Vectors, Aurora pgvector, Neptune Analytics (GraphRAG), Pinecone, Redis Enterprise Cloud, MongoDB Atlas. Four supported embedding models: Titan Embeddings G1 (1,536 dim), Titan Text Embeddings V2 (256 / 512 / 1,024), Cohere Embed English v3 (1,024), Cohere Embed Multilingual v3 (1,024). Metadata filtering during retrieval and citations in generation are first-class.&lt;/p&gt;

&lt;p&gt;Custom RAG with Bedrock + OpenSearch Serverless vector engine. Same substrate as Knowledge Bases’ most common configuration, but you write the pipeline: ingestion Lambdas, embedding invocations, k-NN mappings, prompt assembly, citation extraction. Every component is under your control and yours to operate. OpenSearch Serverless supports HNSW with Faiss, cosine / L2 / dot-product metrics, up to 16,000 dimensions, and scales in OCU increments (2-OCU minimum for production, $0.24 per OCU-hour).&lt;/p&gt;

&lt;p&gt;Custom RAG with Bedrock + Aurora PostgreSQL pgvector. Same DIY pipeline, but the vector store is Aurora with pgvector 0.5.0+ and HNSW indexes on a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vector(n)&lt;/code&gt; column. Knowledge Bases can also consume Aurora as a vector store via the RDS Data API plus Secrets Manager. The selling point is SQL: embeddings sit next to the metadata you already keep relationally, and filters become ordinary &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE&lt;/code&gt; clauses.&lt;/p&gt;

&lt;p&gt;Custom RAG with Bedrock + Amazon Kendra. Kendra is not a vector database, it’s an intelligent search service with its own ranking models, ML-based relevance tuning, and built-in document-level security. GenAI Enterprise Edition runs $0.32/hour base plus $0.25/hour per storage unit plus $0.07/hour per query unit; Basic Enterprise starts at $1.40/hour. Point it at data sources, hit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Retrieve&lt;/code&gt;, stuff results into a Bedrock prompt, emit citations from Kendra’s result URIs.&lt;/p&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Option&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Access control in retrieval&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&amp;lt;3 s P95&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Citations&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Incremental sync&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Low ops overhead&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Fine-tune foundation model&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Bedrock Knowledge Bases&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Custom RAG on OpenSearch Serverless&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Custom RAG on Aurora pgvector&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Custom RAG on Kendra&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;matching-the-shape-to-the-managed-service&quot;&gt;Matching the shape to the managed service&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 640&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;A user question with an authenticated identity flows through identity translation to a group list, then through Bedrock Knowledge Bases metadata-filtered retrieval against OpenSearch Serverless, returning hierarchical parent chunks the caller is allowed to see, then through Claude Sonnet for generation, emitting an answer with inline citations.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .ftd-bg          { fill: rgba(47, 125, 74, 0.06); stroke: rgba(47, 125, 74, 0.45); stroke-width: 2; }
      .ftd-node       { fill: #fff; stroke: #2f7d4a; stroke-width: 1.8; }
      .ftd-identity   { fill: #fff; stroke: #3a5fb5; stroke-width: 1.8; }
      .ftd-store      { fill: #fff; stroke: #b78a2a; stroke-width: 1.8; }
      .ftd-output     { fill: rgba(47, 125, 74, 0.14); stroke: rgba(47, 125, 74, 0.9); stroke-width: 2; }
      .ftd-title      { font-size: 15px; font-weight: 700; fill: #222; }
      .ftd-detail     { font-size: 12px; fill: #333; }
      .ftd-tag        { font-size: 11px; fill: #555; font-style: italic; }
      .ftd-arrow      { fill: none; stroke: #555; stroke-width: 1.8; }
      .ftd-arrow-filter { fill: none; stroke: #2f7d4a; stroke-width: 2; stroke-dasharray: 5 3; }
    &lt;/style&gt;
    &lt;marker id=&quot;ftd-head&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;ftd-head-filter&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#2f7d4a&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;1060&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;ftd-bg&quot; /&gt;

  &lt;rect x=&quot;40&quot; y=&quot;60&quot; width=&quot;240&quot; height=&quot;80&quot; rx=&quot;6&quot; class=&quot;ftd-identity&quot; /&gt;
  &lt;text x=&quot;160&quot; y=&quot;88&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-title&quot;&gt;Authenticated user&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;110&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;engineer, on-call&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;128&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;session from IdP&lt;/text&gt;

  &lt;rect x=&quot;40&quot; y=&quot;180&quot; width=&quot;240&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;ftd-identity&quot; /&gt;
  &lt;text x=&quot;160&quot; y=&quot;206&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-title&quot;&gt;Identity translation&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;226&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;server-side only&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;242&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;groups = [engineering, on-call]&lt;/text&gt;

  &lt;path d=&quot;M160,140 L160,180&quot; class=&quot;ftd-arrow&quot; marker-end=&quot;url(#ftd-head)&quot; /&gt;

  &lt;rect x=&quot;40&quot; y=&quot;290&quot; width=&quot;240&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;ftd-node&quot; /&gt;
  &lt;text x=&quot;160&quot; y=&quot;316&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-title&quot;&gt;Filter composition&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;336&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;orAll listContains&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;352&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;allowed_groups ∈ user groups&lt;/text&gt;

  &lt;path d=&quot;M160,250 L160,290&quot; class=&quot;ftd-arrow&quot; marker-end=&quot;url(#ftd-head)&quot; /&gt;

  &lt;rect x=&quot;400&quot; y=&quot;180&quot; width=&quot;300&quot; height=&quot;180&quot; rx=&quot;6&quot; class=&quot;ftd-node&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;208&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-title&quot;&gt;Bedrock Knowledge Bases&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;232&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;Retrieve + metadata filter&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;252&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;HNSW cosine, numberOfResults 10&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;274&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;hierarchical: child 300 tok,&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;290&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;parent 1,500 tok returned&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;316&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;filter applied *during* k-NN,&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;332&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;not after&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;350&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;HR chunks never enter top-K&lt;/text&gt;

  &lt;path d=&quot;M280,325 L400,280&quot; class=&quot;ftd-arrow-filter&quot; marker-end=&quot;url(#ftd-head-filter)&quot; /&gt;

  &lt;rect x=&quot;820&quot; y=&quot;100&quot; width=&quot;240&quot; height=&quot;100&quot; rx=&quot;6&quot; class=&quot;ftd-store&quot; /&gt;
  &lt;text x=&quot;940&quot; y=&quot;128&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-title&quot;&gt;OpenSearch Serverless&lt;/text&gt;
  &lt;text x=&quot;940&quot; y=&quot;150&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;Titan V2 1,024-dim&lt;/text&gt;
  &lt;text x=&quot;940&quot; y=&quot;168&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;metadata sidecars&lt;/text&gt;
  &lt;text x=&quot;940&quot; y=&quot;186&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;2 OCUs, HNSW + Faiss&lt;/text&gt;

  &lt;rect x=&quot;820&quot; y=&quot;220&quot; width=&quot;240&quot; height=&quot;80&quot; rx=&quot;6&quot; class=&quot;ftd-store&quot; /&gt;
  &lt;text x=&quot;940&quot; y=&quot;248&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-title&quot;&gt;Weekly ingestion&lt;/text&gt;
  &lt;text x=&quot;940&quot; y=&quot;268&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;StartIngestionJob on S3&lt;/text&gt;
  &lt;text x=&quot;940&quot; y=&quot;284&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;deltas only, per-object triggers ready&lt;/text&gt;

  &lt;path d=&quot;M700,250 L820,155&quot; class=&quot;ftd-arrow&quot; marker-end=&quot;url(#ftd-head)&quot; /&gt;
  &lt;path d=&quot;M820,260 L700,280&quot; class=&quot;ftd-arrow&quot; marker-end=&quot;url(#ftd-head)&quot; /&gt;

  &lt;rect x=&quot;400&quot; y=&quot;430&quot; width=&quot;300&quot; height=&quot;80&quot; rx=&quot;6&quot; class=&quot;ftd-node&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;458&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-title&quot;&gt;Claude Sonnet&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;480&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;RetrieveAndGenerate&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;498&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-tag&quot;&gt;$output_format_instructions$ preserved&lt;/text&gt;

  &lt;path d=&quot;M550,360 L550,430&quot; class=&quot;ftd-arrow&quot; marker-end=&quot;url(#ftd-head)&quot; /&gt;

  &lt;rect x=&quot;300&quot; y=&quot;540&quot; width=&quot;500&quot; height=&quot;70&quot; rx=&quot;10&quot; class=&quot;ftd-output&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;568&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-title&quot;&gt;Answer with inline citations&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;590&quot; text-anchor=&quot;middle&quot; class=&quot;ftd-detail&quot;&gt;each span linked to retrievedReferences[*].location.s3Location&lt;/text&gt;

  &lt;path d=&quot;M550,510 L550,540&quot; class=&quot;ftd-arrow&quot; marker-end=&quot;url(#ftd-head)&quot; /&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.85em; color: var(--color-ink-secondary); margin-top: 0.5em;&quot;&gt;Identity in, filter composed server-side, metadata filter applied during retrieval (green dashed), citations emitted by preserving the default prompt template&apos;s `$output_format_instructions$` placeholder.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;knowledge-bases-in-depth&quot;&gt;Knowledge Bases, in depth&lt;/h3&gt;

&lt;p&gt;Chunking. Five strategies: default (~300 tokens, sentence-aware), fixed-size (tunable), hierarchical (child for precision, parent for context), semantic (LLM-driven boundaries with buffer and percentile threshold), no-chunking (one chunk per document, loses page-number citations). For runbooks and policies, structured documents where the correct answer is a two-sentence span but the generator needs surrounding subsection context, hierarchical earns its place. Child 300 tokens, parent 1,500. Parent + child above 8,000 combined tokens hits metadata-size limits; not supported on the S3 Vectors backend.&lt;/p&gt;

&lt;p&gt;Embedding model. Titan V2 at 1,024 dimensions is the default for an English corpus: cheapest option that clears the quality bar, reasonable per-vector footprint. Dropping to 512 halves vector storage at some retrieval-quality cost. Cohere Embed English v3 is the upgrade when lexical-vs-semantic ranking matters. Dimensions are locked to the embedding model, switching models means reindexing the whole corpus.&lt;/p&gt;

&lt;p&gt;Access control through metadata filtering. Every document has a companion &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;filename&amp;gt;.metadata.json&lt;/code&gt; declaring &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allowed_groups&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domain&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;classification&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;effective_date&lt;/code&gt;. Every retrieval call passes a filter composed server-side from the authenticated caller’s group membership:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;vectorSearchConfiguration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;numberOfResults&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;orAll&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;listContains&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;allowed_groups&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;engineering&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;listContains&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;allowed_groups&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;on-call&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The filter is applied &lt;em&gt;during&lt;/em&gt; vector search, not after. Chunks whose metadata doesn’t satisfy it never enter the top-K. Available operators: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notEquals&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;greaterThan(OrEquals)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lessThan(OrEquals)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notIn&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startsWith&lt;/code&gt; (OpenSearch Serverless only), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stringContains&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listContains&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;andAll&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orAll&lt;/code&gt; (minimum 2 conditions each). Enough for group-based rules; not enough for full ABAC with clearance-level comparisons.&lt;/p&gt;

&lt;p&gt;Critical: the filter is composed by a trusted backend on every call. If the browser gets to construct it, there’s no access control at all.&lt;/p&gt;

&lt;p&gt;Incremental ingestion. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StartIngestionJob&lt;/code&gt; walks the data source, diffs against the vector store via S3 metadata (ETags), re-embeds what changed, removes vectors for deleted documents. Weekly cron via EventBridge; per-object triggers from S3 event notifications when the product tightens to near-real-time.&lt;/p&gt;

&lt;p&gt;Citations. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; preserves a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;citations&lt;/code&gt; array in the response linking spans of the generated text to retrieved chunks plus their S3 URIs and metadata. Citations require the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$output_format_instructions$&lt;/code&gt; placeholder in the prompt template; removing it to hand-tune instructions silently disables citations.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-retrieval-trace&quot;&gt;A worked retrieval trace&lt;/h3&gt;

&lt;p&gt;One question, end to end. An engineer asks &lt;em&gt;“What’s the runbook for rotating the production database password?”&lt;/em&gt; Groups &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[&quot;engineering&quot;, &quot;on-call&quot;]&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Identity translation. Backend looks up groups, confirms the session is live, composes the retrieval filter.&lt;/li&gt;
  &lt;li&gt;Embed the query. Titan V2 returns a 1,024-dim vector in ~30-80 ms.&lt;/li&gt;
  &lt;li&gt;Vector search with filter. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Retrieve&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;numberOfResults: 10&lt;/code&gt; and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orAll&lt;/code&gt; filter. OpenSearch Serverless runs HNSW k-NN with metadata filtering during search, returning ten chunks. HR chunks never contribute noise. ~100-250 ms.&lt;/li&gt;
  &lt;li&gt;Hierarchical replacement. Child chunks sharing a parent collapse to the parent. Ten children might become six parents, each 1,500-token, each with surrounding procedural context.&lt;/li&gt;
  &lt;li&gt;Prompt assembly. Knowledge Bases populates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$search_results$&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$query$&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$output_format_instructions$&lt;/code&gt;, removing the last silently disables citations.&lt;/li&gt;
  &lt;li&gt;Generation. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; calls Claude Sonnet via a cross-region inference profile. First token ~800 ms; a 300-token answer finishes in ~1.8 s.&lt;/li&gt;
  &lt;li&gt;Citations. Response includes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;citations&lt;/code&gt; array linking spans of generated text to retrieved chunks plus S3 URIs. The app renders each as a numbered inline reference.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total end-to-end: embedding 60 ms + vector search 180 ms + orchestration 50 ms + first-token 800 ms + streaming 1,000 ms = ~2.1 s P95. Comfortably inside the 3-second budget.&lt;/p&gt;

&lt;h3 id=&quot;when-aurora-pgvector-earns-its-place-instead&quot;&gt;When Aurora pgvector earns its place instead&lt;/h3&gt;

&lt;p&gt;Reach for Aurora pgvector directly when the access-control logic exceeds what metadata-filter operators express: multi-hop joins across user / group / ACL / classification tables, clearance-level ≤ user-clearance via a lookup table, time-windowed validity (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;effective_date &amp;lt;= now() AND (expiry_date IS NULL OR expiry_date &amp;gt; now())&lt;/code&gt;). SQL eats all of that; metadata attributes can’t. Also correct when the ops muscle for Postgres already exists and adding pgvector plus an HNSW index is a smaller jump than owning an OpenSearch Serverless collection, or when transactional consistency between documents and metadata matters (an ACL change and its embedding update atomically, no stale-filter window).&lt;/p&gt;

&lt;p&gt;For 50,000 documents with a vanilla group-membership filter, Aurora is overkill. For 5 million documents with SOX-grade audit against a mature Postgres estate, it’s the correct answer.&lt;/p&gt;

&lt;h3 id=&quot;when-kendra-earns-its-place-instead&quot;&gt;When Kendra earns its place instead&lt;/h3&gt;

&lt;p&gt;Kendra is an intelligent-search service that happens to be useful in a RAG pipeline. Favour it when ranking quality on messy natural-language queries matters more than embedding flexibility (Kendra’s ML-based ranking beats plain vector similarity when user phrasing diverges sharply from source text), when document-level access control via user tokens and group context in the Retrieve API is easier to wire than metadata sidecars, and when the maintained connectors (SharePoint, Confluence, ServiceNow, Salesforce, Box, Slack) earn the premium. For 50,000 documents a GenAI Enterprise Edition base runs ~$500-700/month before queries versus OpenSearch Serverless’s 2-OCU minimum at ~$350/month. For the situation as stated, Knowledge Bases wins on cost and flexibility. For “users consistently phrase things weirdly enough that vector similarity misses,” Kendra earns the premium.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Bedrock Knowledge Bases is the managed RAG path. A data source, a chunking strategy, an embedding model, a vector store, and two runtime APIs: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Retrieve&lt;/code&gt; for raw chunks and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; for the full round-trip with citations.&lt;/li&gt;
  &lt;li&gt;Chunking is the lever nobody thinks about until answers are wrong. Five strategies; hierarchical (child for precision, parent for generator context) is the pragmatic default for structured documents.&lt;/li&gt;
  &lt;li&gt;Embedding model locks dimensions and therefore storage footprint. Titan V2 at 1,024 is the sensible English-corpus default; changing embedding models means reindexing.&lt;/li&gt;
  &lt;li&gt;Metadata filters run &lt;em&gt;during&lt;/em&gt; vector search, not after. That’s what makes access control effective rather than cosmetic, disallowed chunks never enter the top-K and never pollute the generator.&lt;/li&gt;
  &lt;li&gt;Filter operators cover &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;, numeric comparisons, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notIn&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stringContains&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listContains&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startsWith&lt;/code&gt; (OpenSearch-Serverless only), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;andAll&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orAll&lt;/code&gt;. Enough for group-based access; not enough for multi-hop SQL-style ACL joins.&lt;/li&gt;
  &lt;li&gt;Identity-to-groups translation happens server-side. The browser never composes filters; that’s the one non-negotiable security boundary in the design.&lt;/li&gt;
  &lt;li&gt;Citations depend on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$output_format_instructions$&lt;/code&gt; placeholder. Remove it to hand-tune the prompt and citations vanish silently.&lt;/li&gt;
  &lt;li&gt;Incremental ingestion scales from weekly cron to per-object S3 event triggers without rearchitecting. “Weekly” becomes “within an hour” with a config change, not a redesign.&lt;/li&gt;
  &lt;li&gt;Aurora pgvector is the upgrade path when access-control logic exceeds metadata-filter operators. Kendra is the upgrade when ranking quality beats embedding flexibility. Fine-tuning is the wrong path entirely for living, access-controlled corpora.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answer: Bedrock Knowledge Bases on OpenSearch Serverless, Titan Text Embeddings V2 at 1,024 dimensions, hierarchical chunking with child 300 tokens and parent 1,500, metadata sidecars declaring &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allowed_groups&lt;/code&gt;, every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; filtered by the caller’s group membership via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orAll&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listContains&lt;/code&gt;. Weekly EventBridge-triggered &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StartIngestionJob&lt;/code&gt;; Claude Sonnet for generation with the default prompt template preserving &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$output_format_instructions$&lt;/code&gt;. Latency closes at ~2.1 s P95, generation dominates the time budget, retrieval barely registers. A configured managed service plus a small orchestration Lambda, not a pipeline to own.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Rules, Grammars, and Regex</title>
    <link href="/writing/rules-grammars-and-regex/"/>
    <updated>2026-05-30T06:00:00+08:00</updated>
    <id>/writing/rules-grammars-and-regex/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;A regulator emails the compliance team: every customer email mentioning a competitor must be flagged for review within ten minutes of receipt, with an audit trail of why each one was flagged. The team starts designing an &lt;label for=&quot;sn-writing-rules-grammars-and-regex-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-rules-grammars-and-regex-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-rules-grammars-and-regex-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-rules-grammars-and-regex-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; pipeline. Six weeks in, the regulator wants to see the &lt;label for=&quot;sn-writing-rules-grammars-and-regex-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-rules-grammars-and-regex-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-rules-grammars-and-regex-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-rules-grammars-and-regex-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; card and asks why the system flagged a borderline message yesterday at 3:47pm. Nobody can answer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A list of competitor names in a regex would have shipped on day one and answered the regulator’s question in three seconds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post is about the AI that isn’t AI, deterministic, hand-written rules. They have no learning, no embeddings, no probabilistic outputs. They’re often dismissed as “primitive,” and they’re often the correct answer.&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;/writing/the-boring-baseline-that-wins/&quot;&gt;the previous post&lt;/a&gt; we covered the classical statistical baselines that beat fancy models on small problems. This post covers the rule-based systems that beat statistical models on problems where you actually know the answer.&lt;/p&gt;

&lt;h3 id=&quot;the-case-for-rules&quot;&gt;The case for rules&lt;/h3&gt;

&lt;p&gt;A rule-based system is one where every decision is dictated by code a human wrote, not weights a machine learned. Three properties make rules valuable:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;They’re deterministic. Same input, same output, every time. No drift, no hallucination, no surprise.&lt;/li&gt;
  &lt;li&gt;They’re auditable. Every decision can be traced to a specific line of code. You can explain to a regulator, an auditor, or a customer exactly why the system did what it did.&lt;/li&gt;
  &lt;li&gt;They’re free at inference time. A regex match runs in microseconds. A finite-state transducer runs at gigabytes per second. There’s no per-call cost, no rate limit, no GPU.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In return for those properties, rules are inflexible. They handle exactly what you wrote down, and nothing else. The first time the world produces an input you didn’t anticipate, your rule fails, silently or loudly, depending on the system.&lt;/p&gt;

&lt;p&gt;It all comes down to the shape of the input space. When it’s bounded and the rules are knowable, hand-written rules are unbeatable. When it’s open-ended and full of paraphrase and ambiguity, hand-written rules are useless and you need a learning system.&lt;/p&gt;

&lt;h3 id=&quot;regular-expressions-the-workhorse&quot;&gt;Regular expressions: the workhorse&lt;/h3&gt;

&lt;p&gt;You know regular expressions. They’re a small language for describing patterns in text. They came out of theoretical computer science in the 1950s and have been quietly running production systems ever since.&lt;/p&gt;

&lt;p&gt;Things you can do with regular expressions and shouldn’t reach for an LLM for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Validating an email address looks plausible.&lt;/li&gt;
  &lt;li&gt;Extracting phone numbers, postcodes, dates, ABNs, account numbers, IP addresses.&lt;/li&gt;
  &lt;li&gt;Tokenising structured logs. Every webserver log, syslog entry, and audit trail is parseable by regex.&lt;/li&gt;
  &lt;li&gt;Recognising fixed product codes in customer support tickets (“KB-2847-FATAL”) for routing.&lt;/li&gt;
  &lt;li&gt;Stripping HTML, normalising whitespace, redacting sensitive fields.&lt;/li&gt;
  &lt;li&gt;Implementing a basic spam filter for known bad strings.&lt;/li&gt;
  &lt;li&gt;Anything where the pattern is exact, even if there’s some variation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can write a regex that matches your target with high precision and recall, you don’t need a model. You’re done. Ship the regex.&lt;/p&gt;

&lt;h4 id=&quot;the-known-dangers&quot;&gt;The known dangers&lt;/h4&gt;

&lt;p&gt;Regular expressions have a well-earned reputation for sharp edges, and the relevant ones are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Catastrophic backtracking. A poorly written regex can take exponential time on adversarial inputs. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;re2&lt;/code&gt; library (Google) sidesteps this with a different engine. If you’re processing untrusted input, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;re2&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Unicode is harder than ASCII. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\w&lt;/code&gt; doesn’t always mean what you think it means once you leave ASCII land.&lt;/li&gt;
  &lt;li&gt;The “more is more” trap. A regex that grows past 200 characters is usually a sign you should be writing a parser, not a pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The discipline that pays off is treating regex as a focused tool. Use it when the pattern is genuinely regular. Reach for something else when it isn’t.&lt;/p&gt;

&lt;h3 id=&quot;finite-state-transducers-regex-with-structure&quot;&gt;Finite-state transducers: regex with structure&lt;/h3&gt;

&lt;p&gt;A finite-state transducer (FST) is a regex with two important upgrades: it can produce output, and it can be composed with other FSTs.&lt;/p&gt;

&lt;p&gt;An FST is a state machine that consumes input symbols and emits output symbols based on its current state. The classic use is morphological analysis, mapping &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;walked&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;walk + PAST&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mice&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mouse + PLURAL&lt;/code&gt;. The transducer encodes the rules of a language’s morphology directly.&lt;/p&gt;

&lt;p&gt;FSTs are the workhorse of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Speech recognition lexicons, mapping phoneme sequences to words.&lt;/li&gt;
  &lt;li&gt;Computational morphology for low-resource languages.&lt;/li&gt;
  &lt;li&gt;Spell-checkers and stemmers for languages with rich inflection.&lt;/li&gt;
  &lt;li&gt;Pre-processing pipelines for NLP in production search systems.&lt;/li&gt;
  &lt;li&gt;Machine translation grammars, particularly rule-based MT for language pairs without enough parallel corpus for neural translation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dominant tool here is OpenFST (a C++ library originally from AT&amp;amp;T). The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pynini&lt;/code&gt; Python wrapper is the practitioner’s way in. For most people building a normal application this is overkill, but if you’re working on production search, speech, or non-English NLP, FSTs are part of the toolkit and they’re not going away.&lt;/p&gt;

&lt;h3 id=&quot;context-free-grammars-parsing-structured-language&quot;&gt;Context-free grammars: parsing structured language&lt;/h3&gt;

&lt;p&gt;Beyond regular languages live context-free grammars (CFGs). The tool you reach for when you have a language with structure that a regex can’t capture, nested brackets, recursive expressions, anything where the validity of one part depends on another part you haven’t seen yet.&lt;/p&gt;

&lt;p&gt;CFGs are the foundation of programming-language compilers. They’re also production tools for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Parsing structured user input, query languages, formula syntax, search expressions.&lt;/li&gt;
  &lt;li&gt;Validating semi-structured documents. LaTeX, JSON, XML, all defined by grammars.&lt;/li&gt;
  &lt;li&gt;Information extraction from templated text, forms, contracts, regulatory filings.&lt;/li&gt;
  &lt;li&gt;Implementing natural-language interfaces with bounded vocabularies, voice command systems, where the user can only say a fixed set of patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You write the grammar, you run a parser-generator (ANTLR, Bison, Lark for Python), you get a parser. The result is fast, deterministic, and tells you exactly which production rule matched.&lt;/p&gt;

&lt;p&gt;For most application code, CFGs are overkill, regex handles it. But the moment you find yourself writing nested-condition regex with manual depth tracking, stop and reach for a grammar.&lt;/p&gt;

&lt;h3 id=&quot;decision-trees-and-rule-lists&quot;&gt;Decision trees and rule lists&lt;/h3&gt;

&lt;p&gt;A decision tree is a sequence of if-then rules organised as a tree. Each internal node tests a feature; each leaf is a decision. They sit on the boundary between rules and ML, you can hand-write a decision tree (it’s just a flowchart) or learn one from data (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sklearn.tree.DecisionTreeClassifier&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Hand-written decision trees are the correct answer for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Eligibility logic. “Customer is eligible for the discount if they’ve been with us for more than 12 months AND have spent more than $500 AND haven’t used a discount in the last 90 days.”&lt;/li&gt;
  &lt;li&gt;Triage and routing. Support ticket routing, document workflows, customer-service escalation.&lt;/li&gt;
  &lt;li&gt;Compliance gating. Regulatory rules that must be applied exactly as written.&lt;/li&gt;
  &lt;li&gt;Game logic. Rules in a turn-based game, transitions in a state machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The advantage of a decision tree (or its equivalent: a sequence of if-elif statements in code) is that the entire decision process is visible and reviewable. The disadvantage: it doesn’t generalise. If a new condition shows up that wasn’t in the rules, the tree has no opinion.&lt;/p&gt;

&lt;p&gt;The hybrid pattern that pays off: start with a hand-written decision tree, instrument it for the cases it handles badly, then either add rules or switch to a learned model when the rule list gets unmaintainable. Many production systems live in this hybrid space for years.&lt;/p&gt;

&lt;h3 id=&quot;expert-systems-the-ancestor&quot;&gt;Expert systems: the ancestor&lt;/h3&gt;

&lt;p&gt;A rule-based expert system is a large collection of if-then rules with an inference engine that chains them together. They were the fashionable AI of the 1970s and 1980s. MYCIN for medical diagnosis, DENDRAL for chemistry, XCON for configuring DEC computers.&lt;/p&gt;

&lt;p&gt;The expert-system winter, when these projects mostly disappointed, gave rule-based AI a bad name in popular memory. But the practical lessons remain:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Rules work well for stable, well-understood domains.&lt;/li&gt;
  &lt;li&gt;Maintaining a rule base of more than a few thousand rules is hard. Conflicts emerge. Edge cases pile up. The system becomes brittle.&lt;/li&gt;
  &lt;li&gt;Combining rules with statistical methods, using rules for the cases you understand and ML for the rest, is often more practical than picking one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern descendants live in:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Drools and similar business-rules engines, used in insurance underwriting, banking compliance, and benefits administration.&lt;/li&gt;
  &lt;li&gt;Prolog and other logic-programming systems, mostly in academia but still used commercially in some niches.&lt;/li&gt;
  &lt;li&gt;Datalog for analytic and policy reasoning over relational data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you hear “rule-based system” today, it’s usually a Drools-style production rule engine making decisions in a regulated domain.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-rules-a-triage&quot;&gt;When to use rules: a triage&lt;/h3&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Property of your problem&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Lean toward rules&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Lean toward ML&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Auditability requirement&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Strong (regulatory, legal, safety)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Weak (best-effort relevance)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Latency budget&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Microseconds&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Milliseconds or seconds OK&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Per-call cost tolerance&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Must be near-zero&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Some cost is fine&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Input variability&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Bounded (formats, codes, structured)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open-ended (natural language)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Domain expert availability&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Yes, can write down the rules&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;No, has to be learned from data&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Drift&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Slow (years)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Fast (weeks/months)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Volume of labelled data&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Zero, rules are the labels&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Thousands of examples&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Acceptance of failures&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Failures must be debuggable&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Probabilistic failures OK&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-hybrid-pattern&quot;&gt;The hybrid pattern&lt;/h3&gt;

&lt;p&gt;The best production systems usually mix rules and learning. The pattern, in rough form:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Rules at the edges. Fast pre-filters that reject obvious garbage and recognise unambiguous cases.&lt;/li&gt;
  &lt;li&gt;Statistical models in the middle. ML for the genuinely ambiguous cases the rules can’t handle.&lt;/li&gt;
  &lt;li&gt;Rules at the edges again. Post-filters that catch known-bad model outputs and apply business logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A spam filter does this: regex catches the obvious phishing patterns; a statistical model handles the borderline cases; a final rule layer applies user preferences and explicit allowlists. A search relevance system does this: lexical matching with rules first; semantic ranking with embeddings; business-rule re-ranking last.&lt;/p&gt;

&lt;p&gt;The reason the hybrid wins is that rules and ML have inverse strengths and weaknesses. Rules are precise but rigid; ML is flexible but fuzzy. Used together, they cover each other’s gaps.&lt;/p&gt;

&lt;p&gt;Rules are AI’s quiet underclass. They run more production systems than transformers do, and most teams forget about them until the regulator emails or the latency budget collapses. Regex handles patterns that are genuinely regular. Finite-state transducers extend that into composition and structured output for speech and morphology. Context-free grammars take over when nesting and recursion show up. Hand-written decision trees encode the business logic nobody wants buried in a model. Production rule engines like Drools still run the parts of insurance, banking, and compliance where every decision needs a trace.&lt;/p&gt;

&lt;p&gt;The version that wins in production is rarely all-rules or all-learning. It’s rules at the edges, fast pre-filters that catch the obvious cases and post-filters that apply business policy, with statistical models in the middle handling the genuinely ambiguous inputs. Rules and learning have inverse strengths. Used together, they cover each other’s gaps. The next four posts in the series leave the language-and-text neighbourhood and pick up the rest of the classical AI textbook, search and planning, logic, constraints, probability.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Salt in the Dish</title>
    <link href="/writing/the-salt-in-the-dish/"/>
    <updated>2026-05-29T06:00:00+08:00</updated>
    <id>/writing/the-salt-in-the-dish/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/consulting-and-craft/&quot;&gt;Consulting and Craft&lt;/a&gt; &amp;middot; &lt;a href=&quot;/writing/through-the-kitchen/&quot;&gt;Through the Kitchen&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;There are four kinds of salt in my kitchen drawer, and they are not interchangeable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Maldon flaky sea salt for finishing steak. Cooking salt in a big jar by the stove, cheap and honest, for seasoning as I go and for brines. Oak smoked salt in a tin my mother-in-law gave me, so intensely flavoured I use it by the pinch on things off the grill. And a grinder of powdered salt I crush from cooking salt with a mortar and pestle, because powdered salt disperses evenly over popcorn without forming the salty pockets that spoil a bowl halfway through.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;None of these is a substitute for any of the others. A pinch of oak smoked where cooking salt is called for would be overwhelming. A teaspoon of Maldon where fine salt is called for would leave most of the food unseasoned and some of it disagreeably gritty. They are tools. Each does one thing well and refuses to do the other things at all.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is about salt. Most of it is about dependencies, and about the discipline of knowing what you’re putting into the dish before you put it in.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;not-all-salts-are-the-same-kind-of-salty&quot;&gt;Not all salts are the same kind of salty&lt;/h3&gt;

&lt;p&gt;A teaspoon of one salt is not a teaspoon of another. Morton’s table salt is dense and finely crystalline, a teaspoon weighs about six grams. Diamond Crystal kosher salt is the same compound, but its crystals are hollow and flaky, and a teaspoon weighs about three grams. A recipe written for Morton’s and executed with Diamond Crystal will be under-seasoned. The reverse will be inedibly salty. Same chemical, same teaspoon, half the delivered dose.&lt;/p&gt;

&lt;p&gt;Flaky finishing salts like Maldon are designed to sit on top of the food as a textural element, not to dissolve into it. Grinding Maldon into a braise is a waste; sprinkling it on a finished steak is exactly right. Smoked salts carry flavours as well as sodium and must be used sparingly. Fine salts for brining need to dissolve completely and can’t contain anti-caking agents that cloud the brine.&lt;/p&gt;

&lt;p&gt;What you actually want, at every point in every dish, is &lt;em&gt;the right form of salt for this moment&lt;/em&gt;. Grabbing the nearest box because it says “salt” on the side is a mistake that shows up later, in the taste of the thing you served to people who trusted you with their dinner.&lt;/p&gt;

&lt;h3 id=&quot;most-salts-are-not-food&quot;&gt;Most “salts” are not food&lt;/h3&gt;

&lt;p&gt;Most of the compounds called “salts” are not edible. “Salt” is a chemistry term, not a culinary one, any compound formed when an acid reacts with a base. Sodium chloride is one. There are thousands of others.&lt;/p&gt;

&lt;p&gt;Epsom salt is magnesium sulfate. It is a laxative. Lead acetate is a salt; the Romans used it to sweeten wine and it probably poisoned a fair chunk of the aristocracy. Potassium nitrate is a salt, used in gunpowder and in curing bacon, and which application you have in mind very much matters.&lt;/p&gt;

&lt;p&gt;The word “salt” doesn’t tell you whether the thing is safe to put in food. You have to know which salt you’re looking at, and in what quantity.&lt;/p&gt;

&lt;p&gt;The parallel to software is exact. A package on npm called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fast-json-parser&lt;/code&gt; could be a perfectly fine JSON parser, or a package published last week that quietly exfiltrates environment variables while also, technically, parsing JSON. The name tells you nothing. “It is, technically, a JSON parser” is the software equivalent of “it is, technically, a salt.”&lt;/p&gt;

&lt;h3 id=&quot;the-cake-contest&quot;&gt;The cake contest&lt;/h3&gt;

&lt;p&gt;There is an old story, probably apocryphal, about a baking contest in which one contestant sabotaged another by swapping the labels on two unmarked jars in the victim’s pantry the night before the final. The victim reached for the jar they thought was sugar and measured out a cup of salt. The cake was inedible. They lost.&lt;/p&gt;

&lt;p&gt;The attack was not on the salt. The salt was fine. The attack was on the assumption that &lt;em&gt;the label on the jar reflected the contents of the jar&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Software supply chain attacks work the same way. They are attacks on the assumption that the package name on npm, or PyPI, matches what’s inside. Someone takes over an abandoned package. Registers a name one letter away from a popular one. Pushes a malicious version to a legitimate project. By the time anyone notices, the poisoned version has been installed by a hundred thousand &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; commands, each one issued by an engineer who trusted that the label matched the jar.&lt;/p&gt;

&lt;p&gt;Software Bills of Materials. SBoMs, are, in essence, the practice of writing on every jar exactly what’s in it, when it arrived, and where it came from. Boring paperwork. Tedious to maintain. Exactly the kind of thing an engineer who is moving fast will skip, and then one morning will discover they needed.&lt;/p&gt;

&lt;h3 id=&quot;taste-before-you-use&quot;&gt;Taste before you use&lt;/h3&gt;

&lt;p&gt;The single most important discipline in a kitchen is tasting the dish at every stage, and seasoning in response to what you taste, not in response to what the recipe said to do.&lt;/p&gt;

&lt;p&gt;Recipes are approximations. The tomatoes were different tomatoes. The stock had different baseline salt. The cheese you’re melting contains sodium the recipe writer didn’t know about. So you taste. When the onions are sweating. When the liquid goes in. Halfway through the braise. Right before you plate. Each time you add a little if it needs it, and nothing if it doesn’t. You are adjusting based on current state, not on a timer and a hope.&lt;/p&gt;

&lt;p&gt;Engineers should do exactly the same with dependencies. Read the README. Skim the entry point. Look at the issue tracker. Check the release cadence. Run the tests locally. Try it against your actual use case before you commit to it. &lt;em&gt;Taste the dish.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The engineer who runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; against the first Google result is the cook who empties the first jar they grab into the pot without tasting. Sometimes this works. Often it produces something under-seasoned. Every so often, and this is the one that keeps me awake, it produces something that tastes fine at first and is slowly poisoning everyone who eats it.&lt;/p&gt;

&lt;h3 id=&quot;the-drawer-has-four-salts-for-a-reason&quot;&gt;The drawer has four salts for a reason&lt;/h3&gt;

&lt;p&gt;My drawer has four salts because each does something the others can’t. Learning which is which, and how much, and when, is the patient unglamorous work of becoming someone who can cook.&lt;/p&gt;

&lt;p&gt;Your codebase’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; should have the same relationship with its dependencies. Each one chosen for a specific reason you remember. Each one tasted before it was committed. Each one revisited periodically to see whether the reason still holds. None grabbed at random because the name on the jar sounded about right.&lt;/p&gt;

&lt;p&gt;Taste. Decide. Add. Taste again. Adjust. That’s the discipline. Everything else is built on top of it.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Why Does Thursday Last Forever?</title>
    <link href="/writing/why-does-thursday-last-forever/"/>
    <updated>2026-05-28T06:00:00+08:00</updated>
    <id>/writing/why-does-thursday-last-forever/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The previous posts in this series asked &lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;what time even is&lt;/a&gt;, &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;how we count it&lt;/a&gt;, &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;how physics bends it&lt;/a&gt;, &lt;a href=&quot;/writing/does-time-even-exist/&quot;&gt;whether it exists at all&lt;/a&gt;, &lt;a href=&quot;/writing/can-you-turn-back-time/&quot;&gt;whether you can go backwards&lt;/a&gt;, and &lt;a href=&quot;/writing/the-clock-inside-you/&quot;&gt;how your body keeps its own time&lt;/a&gt;. All of that was about time out there: in clocks, in spacetime, in the equations, in your biology. This post is about time in here. In your head. Where it behaves worst of all.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-afternoon-that-wouldnt-end&quot;&gt;The afternoon that wouldn’t end&lt;/h3&gt;

&lt;p&gt;You know the feeling. You’re in a meeting on a Thursday afternoon. It started at 2.00. You’ve been through two agenda items, a disagreement about scope, and someone’s screen-share that wouldn’t connect. You check the clock, certain it must be nearly 3.00. It’s 2.12.&lt;/p&gt;

&lt;p&gt;This isn’t boredom distorting your memory. Your brain is actively constructing a wrong answer about how much time has passed. It does this reliably, predictably, and for reasons that neuroscience is starting to understand.&lt;/p&gt;

&lt;p&gt;The clock on the wall is objective. It ticks at the same rate whether you’re watching it or not (well, &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;mostly&lt;/a&gt;). But the clock in your head, the one that tells you “that felt like an hour” or “where did the day go”, runs on completely different hardware, and it has no quartz crystal, no caesium atom, no oscillator of any kind. It’s a guess assembled from scraps, and it’s wrong more often than it’s right.&lt;/p&gt;

&lt;h3 id=&quot;your-brain-doesnt-have-a-clock&quot;&gt;Your brain doesn’t have a clock&lt;/h3&gt;

&lt;p&gt;This is the first surprise. Despite our constant awareness of time passing, the human brain has no dedicated timekeeping organ. There’s no neural metronome ticking away in your cortex. Unlike vision (which has the visual cortex) or hearing (the auditory cortex), time perception is distributed across multiple brain regions, none of which is specifically &lt;em&gt;for&lt;/em&gt; time.&lt;/p&gt;

&lt;p&gt;The leading model (still debated) is something called the striatal beat frequency model, proposed by Matthew Matell and Warren Meck in 2004. The idea: cortical neurons oscillate at different frequencies, like a room full of musicians each playing at their own tempo. The striatum, a structure deep in the brain involved in decision-making and reward, listens to the pattern of beats. When a familiar pattern recurs, the brain recognises it as a familiar duration. “That felt like about five seconds” isn’t a measurement. It’s a pattern match.&lt;/p&gt;

&lt;p&gt;This is astonishingly imprecise compared to a caesium clock. But it works well enough to catch a ball, keep a beat, and sense that Thursday afternoon is dragging.&lt;/p&gt;

&lt;h3 id=&quot;why-time-slows-down-when-youre-watching&quot;&gt;Why time slows down when you’re watching&lt;/h3&gt;

&lt;p&gt;A watched pot never boils. Psychologists call this the attentional gate model (Zakay &amp;amp; Block, 1995). The theory: when you direct attention toward the passage of time itself, you notice more temporal information, and more noticed information makes the interval feel longer.&lt;/p&gt;

&lt;p&gt;It’s like counting cars on a motorway. If you’re not paying attention, you’d guess “a few went past.” If you’re actively counting, you’d say “seventeen.” The cars didn’t speed up. You just noticed more of them. Time works the same way. When you’re clock-watching in that Thursday meeting, you’re accumulating more temporal “ticks” in working memory, and more ticks means the interval feels stretched.&lt;/p&gt;

&lt;p&gt;The reverse is equally real. When you’re absorbed in something (what Csikszentmihalyi called flow) attention is consumed by the task, leaving nothing spare for monitoring the clock. Time doesn’t slow down or speed up. You just stop counting. An hour vanishes because you didn’t notice it passing.&lt;/p&gt;

&lt;h3 id=&quot;why-holidays-evaporate&quot;&gt;Why holidays evaporate&lt;/h3&gt;

&lt;p&gt;Here’s the paradox. That Thursday meeting felt endless &lt;em&gt;while it was happening&lt;/em&gt;. But ask someone about it a week later and they’ll say “I barely remember it.” Meanwhile, a two-week holiday felt like it flew past &lt;em&gt;while you were on it&lt;/em&gt;, but looking back, it feels like it lasted ages.&lt;/p&gt;

&lt;p&gt;This is the difference between prospective time (how long something feels while it’s happening) and retrospective time (how long it seems in memory). They use different mechanisms, and they often give opposite answers.&lt;/p&gt;

&lt;p&gt;Prospective time is driven by attention. The more you monitor the clock, the longer it feels. Retrospective time is driven by memory density: how many distinct, novel memories were formed. A boring Thursday produces almost no memorable events, so in retrospect it collapses to nothing. A holiday in an unfamiliar place (new food, new streets, new language, daily surprises) lays down dense, rich memories, and the brain interprets that density as duration.&lt;/p&gt;

&lt;p&gt;This is why the first day of a holiday feels longest in retrospect, and the last day feels shortest. By day ten, you know how the coffee machine works, where the beach is, what the breakfast buffet looks like. Novelty drops. Memory formation slows. The days start blurring together, just like they do at home.&lt;/p&gt;

&lt;p&gt;William James wrote about this in 1890: “In youth we may have an absolutely new experience, subjective or objective, every hour of the day. Apprehension is vivid, retentiveness strong, and our recollections of that time, like those of a time spent in rapid and interesting travel, are of something intricate, multitudinous, and long-drawn-out. But as each passing year converts some of this experience into automatic routine which we hardly note at all, the days and the weeks smooth themselves out in recollection to contentless units, and the years grow hollow and collapse.”&lt;/p&gt;

&lt;p&gt;He was 48. He was describing the effect from the inside.&lt;/p&gt;

&lt;h3 id=&quot;why-childhood-lasted-forever&quot;&gt;Why childhood lasted forever&lt;/h3&gt;

&lt;p&gt;This is the same mechanism writ large. Children experience almost everything for the first time. The first day of school. The first time you ride a bike. The first thunderstorm that actually scares you. Every one of these is a dense, vivid memory. A year of childhood contains thousands of novel events, and looking back, the brain reads that density as duration. A year felt enormous because it &lt;em&gt;was&lt;/em&gt; enormous, in terms of encoded experience.&lt;/p&gt;

&lt;p&gt;By your thirties, most experiences are variations on things you’ve already done. Another commute. Another Monday. Another Christmas that’s almost the same as last Christmas. The events are real, but they don’t register as novel, so memory formation is thin. A year passes and when you look back, there’s not much there. Not because nothing happened, but because nothing &lt;em&gt;new&lt;/em&gt; happened.&lt;/p&gt;

&lt;p&gt;Daniel Kahneman makes a useful distinction between the experiencing self (the one who lives through each moment) and the remembering self (the one who tells the story afterward). The experiencing self had a perfectly normal year. The remembering self says it was over in a flash, because it has almost nothing to report.&lt;/p&gt;

&lt;p&gt;This has a practical corollary that sounds like self-help but is grounded in psychology: if you want time to feel longer in retrospect, seek novelty. New places, new skills, new routines. Not because happiness requires novelty (it doesn’t) but because memory does. The years you remember are the ones that were different from the years before.&lt;/p&gt;

&lt;h3 id=&quot;temperature-emotion-and-the-internal-clock&quot;&gt;Temperature, emotion, and the internal clock&lt;/h3&gt;

&lt;p&gt;Your internal clock isn’t just attention-dependent. It’s also affected by body temperature, emotional state, and neurochemistry.&lt;/p&gt;

&lt;p&gt;Temperature: raising body temperature speeds up the internal clock. In studies where participants’ core temperature was elevated (via warm rooms or mild fever), they consistently overestimated how much time had passed; their internal clock was running fast. This was first demonstrated by Hudson Hoagland in 1933, when he noticed his wife, who had a fever, complained that he’d been away for ages when he’d only left the room for a few minutes. He tested her repeatedly during the fever, and found her time estimates were consistently inflated. Then, being a scientist, he published it.&lt;/p&gt;

&lt;p&gt;Fear: time slows down. Not literally. David Eagleman tested this directly by dropping people from a 45-metre tower (with a net) while they watched a fast-flickering display. If time genuinely slowed, they’d be able to read the display. They couldn’t. What actually happens is that the amygdala (the brain’s threat-response system) kicks into high gear during fear, laying down memories at a much higher density than normal. Afterward, looking back, the dense memory makes the event feel like it lasted longer than it did. Your brain didn’t slow time down. It just took more notes.&lt;/p&gt;

&lt;p&gt;Dopamine: the neurotransmitter most associated with reward and motivation affects time perception directly. Higher dopamine speeds up the internal clock; lower dopamine slows it. This is why stimulant drugs (which increase dopamine) make time feel like it’s dragging: your internal clock is running fast, so objective time seems to crawl. And it’s why the anticipation of a reward makes the wait feel longer. You want the thing. Your dopamine is up. Your internal clock speeds up. The five minutes until dinner feels like twenty.&lt;/p&gt;

&lt;h3 id=&quot;age-and-the-shrinking-year&quot;&gt;Age and the shrinking year&lt;/h3&gt;

&lt;p&gt;There’s a popular mathematical explanation for why years feel shorter as you age: when you’re five, a year is 20% of your life. When you’re fifty, it’s 2%. Each year is a smaller fraction of your total experience, so it &lt;em&gt;should&lt;/em&gt; feel proportionally shorter.&lt;/p&gt;

&lt;p&gt;This is neat, intuitive, and probably wrong, or at least insufficient. The ratio theory predicts a smooth logarithmic curve, but subjective reports don’t follow it precisely. The memory-density explanation is better supported: years feel shorter because they contain less novelty, and less novelty means fewer memories, and fewer memories means the year collapses in retrospect.&lt;/p&gt;

&lt;p&gt;But there’s a third factor that matters, especially in middle age: routine. When your days are structured by the same alarm, same commute, same meetings, same evening pattern, the brain doesn’t bother encoding each day individually. It compresses. Monday through Friday becomes a single unit in memory. Weeks blur into months. This is efficient (you don’t &lt;em&gt;need&lt;/em&gt; to remember every identical Tuesday) but it creates the unsettling sensation that time is accelerating.&lt;/p&gt;

&lt;p&gt;Breaking routine doesn’t add hours to your day. It adds anchors to your memory. A Wednesday that’s different from every other Wednesday gets its own entry in the ledger. The weeks that contain an unusual Wednesday feel, in retrospect, longer than the weeks that don’t.&lt;/p&gt;

&lt;h3 id=&quot;why-two-hours-of-coding-disappears&quot;&gt;Why two hours of coding disappears&lt;/h3&gt;

&lt;p&gt;Programmers know this feeling intimately. You sit down to fix a bug. The next time you surface, two hours have gone and you didn’t notice.&lt;/p&gt;

&lt;p&gt;Flow states are the extreme case of the attentional gate closing. When you’re deeply absorbed, attention is entirely consumed by the task. The gate that lets temporal information into working memory swings shut. You stop counting ticks. There’s nothing to estimate duration from.&lt;/p&gt;

&lt;p&gt;But here’s the interesting part: the same two hours spent in a meeting that you don’t care about will feel like four hours. Same clock time. Opposite subjective experience. And afterward, the two-hour coding session will feel like “not long at all” in retrospect (low novelty, high focus, few distinct memories formed), while the two-hour meeting will &lt;em&gt;also&lt;/em&gt; feel like nothing in retrospect (boring, unmemorable). Both collapse, but for different reasons. One was too engaging to notice. The other was too dull to remember.&lt;/p&gt;

&lt;h3 id=&quot;the-3-am-effect&quot;&gt;The 3 AM effect&lt;/h3&gt;

&lt;p&gt;Anyone who’s been awake at 3 AM with worry knows that the small hours last forever. There’s a neurochemical basis for this. Cortisol (the stress hormone) is at its lowest between midnight and 4 AM, and your body temperature drops to its daily minimum around the same time. Both of these affect time perception. Low body temperature slows the internal clock, making objective time feel like it’s crawling. Anxiety directs attention toward the passage of time itself, opening the attentional gate wide. The combination is brutal: you’re cold, stressed, and clock-watching. Every minute expands.&lt;/p&gt;

&lt;p&gt;This is also why night shifts feel so different from day shifts, even after you’ve adjusted your sleep schedule. Your circadian rhythm still modulates body temperature and cortisol independently of when you’re sleeping. At 3 AM, your body thinks time should be crawling, regardless of whether you went to bed at 7 PM or not.&lt;/p&gt;

&lt;h3 id=&quot;so-what-time-is-it-really&quot;&gt;So what time is it, really?&lt;/h3&gt;

&lt;p&gt;The &lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;first post in this series&lt;/a&gt; asked what time it is and discovered a tower of conventions, politics, and compromise. The &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;second&lt;/a&gt; found that even physical clocks are approximations. The &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;third&lt;/a&gt; showed that time itself bends. The &lt;a href=&quot;/writing/does-time-even-exist/&quot;&gt;fourth&lt;/a&gt; asked whether time fundamentally exists. The &lt;a href=&quot;/writing/can-you-turn-back-time/&quot;&gt;fifth&lt;/a&gt; asked whether you can go backwards. The &lt;a href=&quot;/writing/the-clock-inside-you/&quot;&gt;sixth&lt;/a&gt; looked at the biology: the SCN, the circadian rhythm, the shift-worker’s bill.&lt;/p&gt;

&lt;p&gt;This post adds one more layer. The time you actually &lt;em&gt;experience&lt;/em&gt;, the time that determines whether your day felt long or short, whether your year flew or crawled, whether that meeting was bearable, is constructed by a brain that has no clock, uses attention as a proxy, stores memories as a ledger, and gets reliably fooled by temperature, emotion, novelty, and age.&lt;/p&gt;

&lt;p&gt;The clock on the wall says 17:04. Your brain says Thursday lasted a week. &lt;a href=&quot;/writing/time-is-wrong-everywhere-all-at-once/&quot;&gt;Next up&lt;/a&gt;: computers can’t agree on what time it is either, and it turns out their problem is disturbingly similar.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Picking a Bedrock Model for High-Volume RAG</title>
    <link href="/writing/picking-a-bedrock-model-for-high-volume-rag/"/>
    <updated>2026-05-27T06:00:00+08:00</updated>
    <id>/writing/picking-a-bedrock-model-for-high-volume-rag/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;Generative AI Developer Professional&lt;/strong&gt; · AIP-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A B2B SaaS platform is shipping an in-product assistant. Users ask questions of their own data; the application retrieves relevant records, stitches them into a &lt;label for=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt;, and asks a foundation model to answer. Measured over three months of production traffic:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;~1,000,000 requests per day, peaking at 30 RPS during US/EU business-hours overlap.&lt;/li&gt;
  &lt;li&gt;Median request: ~3,000 input &lt;label for=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;tokens&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt; (&lt;label for=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-system-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-system-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;system prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-system-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-system-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;System prompt&lt;/span&gt;The instruction block that frames the model’s behaviour for a session, separate from the user’s messages.
&lt;/span&gt; + retrieved context + user question), ~400 output tokens.&lt;/li&gt;
  &lt;li&gt;P99 first-token latency target &amp;lt; 1.5 s. The UI streams the answer.&lt;/li&gt;
  &lt;li&gt;Quality bar: complex reasoning over structured retrieved context, tables, JSON, pulling answers from multiple documents.&lt;/li&gt;
  &lt;li&gt;Multi-region failover is hard-required. Customers in both us-east-1 and eu-west-1; a regional Bedrock incident must not take either customer base down.&lt;/li&gt;
  &lt;li&gt;Bedrock-native. No separate model-serving infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;Before reaching for a model card, ask what the application is actually paying for.&lt;/p&gt;

&lt;p&gt;The first question is &lt;em&gt;whose product is this?&lt;/em&gt; A model choice is a product choice, it decides who owns the upgrade cadence, who tracks the pricing page, and who gets paged when the answer quality drifts after a point release. On a hosted-foundation-model platform, those answers split three ways: the model vendor ships the behaviour, the platform ships the availability, the team owns the integration. That shape is cheap to buy into and expensive to reverse; the cost of moving a production &lt;label for=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-rag&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-rag-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;RAG&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-rag&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-rag-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RAG&lt;/span&gt;A pattern where you retrieve relevant documents at query time and stuff them into the prompt so the model can ground its answer on them.
&lt;/span&gt; application between model families is rarely smaller than the savings, and any decision worth making pairs the model with the throughput mode underneath it.&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;what does a bad day look like?&lt;/em&gt; At a million requests a day the interesting failure isn’t an individual bad answer, it’s a region going dark for forty-five minutes. Every second of that outage has a customer-visible consequence, and the blast radius is the entire customer base on the affected region unless the architecture spreads the load. That pushes the design toward something the application can call with a single model identifier while the platform fans the request out across regions behind the scenes, because the alternative is the application owning its own regional routing table and every deploy carrying the risk of a misrouted call.&lt;/p&gt;

&lt;p&gt;The third question is &lt;em&gt;what does the bill look like when the product wins?&lt;/em&gt; At ~3 billion input tokens and ~400 million output tokens a day, the gap between a cheap-tier and a premium-tier model is the difference between a few hundred thousand dollars a month and a couple of million. That’s not a line-item on a finance review, it’s a budget conversation with the CFO. The interesting economics aren’t “which model is cheapest” but “where can we spend cheap-tier prices on questions the cheap tier can answer, and premium prices on the ones that actually need reasoning?”&lt;/p&gt;

&lt;p&gt;The fourth is &lt;em&gt;what happens when the easy answer is wrong?&lt;/em&gt; A single-model architecture pays premium rates for the FAQ slice of traffic and gets premium-grade failure modes when a region saturates. A two-model architecture splits the traffic by difficulty and buys a load-shed path when the premium tier’s capacity tightens. The sophistication isn’t picking the model, it’s designing the cascade that lets the cheaper model carry the tail.&lt;/p&gt;

&lt;p&gt;The fifth is &lt;em&gt;how do we know it’s still working?&lt;/em&gt; Model behaviour drifts across point releases. A RAG system prompt calibrated against one minor version doesn’t automatically work the same on the next, and the gap between “the answers are slightly worse this week” and “we lost 3% accuracy across the board” is an evaluation pipeline that runs nightly against a golden set. That pipeline is a first-class piece of the design, not an afterthought.&lt;/p&gt;

&lt;p&gt;Finally: &lt;em&gt;what buys us the right to change our mind?&lt;/em&gt; &lt;label for=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt; profiles that hide the specific region, prompt templates that separate cacheable prefix from volatile context, evaluation harnesses that can A/B a new model version, all of those are optionality the architecture builds in, and they’re the difference between “we upgraded to the new minor last Tuesday” and “we spent six weeks revalidating the prompt.”&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Distilling that exploration into filters we can score each model against:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Reasoning quality on retrieved context. Reasoning across long structured prompts, not just fluent extraction.&lt;/li&gt;
  &lt;li&gt;First-token latency under 1.5 s at P99 for ~3,000-token inputs. Tail, not average.&lt;/li&gt;
  &lt;li&gt;Cost-per-token that survives a million requests a day. Daily volume is ~3B input + ~400M output tokens; a 10x pricing gap between families is $60k vs $600k a month.&lt;/li&gt;
  &lt;li&gt;Bedrock-native multi-region availability across US and EU, surviving one region offline.&lt;/li&gt;
  &lt;li&gt;Throughput predictability at 30 RPS peak. Traffic is smooth, not spiky, so the capacity question is which mode gives predictable latency without over-buying.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-bedrock-model-landscape&quot;&gt;The Bedrock model landscape&lt;/h3&gt;

&lt;p&gt;Bedrock’s catalogue sorts into seven families.&lt;/p&gt;

&lt;p&gt;Anthropic Claude. Three live tiers: Haiku 4.5 (~$1 / $5 per million input / output tokens; 200K context), Sonnet 4.5 and Sonnet 4.6 (~$3 / $15; 200K context), Opus 4.5 and Opus 4.6 (~$15 / $75; 200K context). All three support global and geographic cross-region inference profiles, prompt caching, and vision inputs. Sonnet is available in US East (N. Virginia, Ohio, Oregon) and EU (Frankfurt, Ireland, Paris, Zurich). First-token latency on Sonnet 4.5 in a warm region sits around 1-1.8 s; Haiku 4.5 under a second.&lt;/p&gt;

&lt;p&gt;Amazon Nova. Four text tiers: Nova Micro (~$0.03 / $0.12 per million; 128K context), Nova Lite (~$0.06 / $0.24; 300K context), Nova Pro (~$0.80 / $3.20; 300K context), Nova Premier (~$2.50 / $12.50; frontier-class). Nova Pro is cheapest-per-token at its quality tier by a wide margin. Regional availability is broad within the US; EU coverage is thinner and largely via cross-region profiles anchored in US regions.&lt;/p&gt;

&lt;p&gt;Meta Llama. Llama 3.1 (8B, 70B, 405B), Llama 3.2 (1B, 11B vision), Llama 4 Maverick and Scout (MoE). Among the lowest pricing on the platform. Llama 3.1 70B around $2.65 / $3.50 per million. The top-end 405B and the Llama 4 MoE models are concentrated in US regions only; cross-region profiles don’t cover EU for the top tiers.&lt;/p&gt;

&lt;p&gt;Mistral AI. Mistral Large 3 (~$2 / $6 per million, 128K context) is the flagship; Ministral 3B and Mixtral 8x7B sit lower. Decent mid-tier reasoning, strong multilingual. Doesn’t beat Sonnet on quality or Nova Pro on cost; EU coverage thinner than Claude’s.&lt;/p&gt;

&lt;p&gt;Cohere. Command R+ is specifically tuned for RAG, citation generation, grounded answers, tool-use. Available in us-east-1 and us-west-2 only; no native EU. First-class option for US-only RAG; ruled out by the EU requirement.&lt;/p&gt;

&lt;p&gt;Amazon Titan. The family has shifted to embeddings (Titan Text Embeddings V2) and image generation. Useful for the embedding side of a RAG pipeline; not the generation model.&lt;/p&gt;

&lt;p&gt;AI21 Labs. Jamba 1.5 Mini and Large, 256K context, Jamba hybrid SSM/Transformer. Good at long-context extraction; limited EU presence; mid-tier reasoning.&lt;/p&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Family&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Reasoning on retrieved context&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;P99 first-token &amp;lt; 1.5 s&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Cost at 1M req/day&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;EU region availability&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Predictable at 30 RPS&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Anthropic Claude (Sonnet)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Amazon Nova (Pro / Premier)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Meta Llama&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Mistral AI&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cohere Command R+&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Amazon Titan&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AI21 Jamba&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Two families make the shortlist on all five: Anthropic Claude and (in a US-only variant) Amazon Nova. Nova wins on cost-per-token but fails EU availability for the Pro and Premier tiers that would clear the reasoning bar. Cohere’s Command R+ is purpose-built for RAG but currently lives in us-east-1 and us-west-2 only. Claude Sonnet is the only row with all ticks, and the “complex reasoning over structured retrieved context” constraint keeps it there.&lt;/p&gt;

&lt;h3 id=&quot;matching-the-workload-to-the-model&quot;&gt;Matching the workload to the model&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 640&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Workload flows through four gates — reasoning on retrieved context, EU residency, P99 latency target, cost per million requests — each narrowing the seven-family Bedrock landscape down to Claude Sonnet as the primary, with Haiku as cost-tier fallback and geographic cross-region profiles as the availability strategy.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .mbp-bg          { fill: rgba(58, 95, 181, 0.06); stroke: rgba(58, 95, 181, 0.45); stroke-width: 2; }
      .mbp-workload    { fill: #fff; stroke: #3a5fb5; stroke-width: 1.8; }
      .mbp-gate        { fill: #fff; stroke: #555; stroke-width: 1.3; stroke-dasharray: 4 3; }
      .mbp-drop        { fill: rgba(168, 74, 42, 0.08); stroke: rgba(168, 74, 42, 0.7); stroke-width: 1.3; }
      .mbp-pick        { fill: rgba(47, 125, 74, 0.12); stroke: rgba(47, 125, 74, 0.9); stroke-width: 2; }
      .mbp-title       { font-size: 18px; font-weight: 700; fill: #222; }
      .mbp-detail      { font-size: 12px; fill: #333; }
      .mbp-gate-text   { font-size: 12px; fill: #333; font-style: italic; }
      .mbp-drop-text   { font-size: 11px; fill: #a84a2a; }
      .mbp-pick-label  { font-size: 15px; font-weight: 700; fill: #222; }
      .mbp-arrow       { fill: none; stroke: #555; stroke-width: 1.8; }
    &lt;/style&gt;
    &lt;marker id=&quot;mbp-head&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;1060&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;mbp-bg&quot; /&gt;

  &lt;rect x=&quot;400&quot; y=&quot;50&quot; width=&quot;300&quot; height=&quot;70&quot; rx=&quot;6&quot; class=&quot;mbp-workload&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;78&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-title&quot;&gt;1M req/day RAG&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;100&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-detail&quot;&gt;3K input, 400 output, US + EU, P99 &amp;lt; 1.5 s&lt;/text&gt;

  &lt;path d=&quot;M550,120 L550,150&quot; class=&quot;mbp-arrow&quot; marker-end=&quot;url(#mbp-head)&quot; /&gt;

  &lt;rect x=&quot;350&quot; y=&quot;150&quot; width=&quot;400&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;mbp-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;177&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-gate-text&quot;&gt;Reasoning on retrieved context?&lt;/text&gt;

  &lt;rect x=&quot;800&quot; y=&quot;150&quot; width=&quot;260&quot; height=&quot;44&quot; rx=&quot;6&quot; class=&quot;mbp-drop&quot; /&gt;
  &lt;text x=&quot;930&quot; y=&quot;171&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-drop-text&quot;&gt;Titan (embeddings only)&lt;/text&gt;
  &lt;text x=&quot;930&quot; y=&quot;186&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-drop-text&quot;&gt;AI21 Jamba (extraction-biased)&lt;/text&gt;

  &lt;path d=&quot;M550,194 L550,224&quot; class=&quot;mbp-arrow&quot; marker-end=&quot;url(#mbp-head)&quot; /&gt;

  &lt;rect x=&quot;350&quot; y=&quot;224&quot; width=&quot;400&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;mbp-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;251&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-gate-text&quot;&gt;EU residency for EU tenants?&lt;/text&gt;

  &lt;rect x=&quot;800&quot; y=&quot;224&quot; width=&quot;260&quot; height=&quot;44&quot; rx=&quot;6&quot; class=&quot;mbp-drop&quot; /&gt;
  &lt;text x=&quot;930&quot; y=&quot;245&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-drop-text&quot;&gt;Nova Pro / Premier, Llama 405B,&lt;/text&gt;
  &lt;text x=&quot;930&quot; y=&quot;260&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-drop-text&quot;&gt;Command R+, no EU presence&lt;/text&gt;

  &lt;path d=&quot;M550,268 L550,298&quot; class=&quot;mbp-arrow&quot; marker-end=&quot;url(#mbp-head)&quot; /&gt;

  &lt;rect x=&quot;350&quot; y=&quot;298&quot; width=&quot;400&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;mbp-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;325&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-gate-text&quot;&gt;Warm first-token &amp;lt; 1.5 s?&lt;/text&gt;

  &lt;rect x=&quot;800&quot; y=&quot;298&quot; width=&quot;260&quot; height=&quot;44&quot; rx=&quot;6&quot; class=&quot;mbp-drop&quot; /&gt;
  &lt;text x=&quot;930&quot; y=&quot;319&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-drop-text&quot;&gt;Opus (too slow for streaming)&lt;/text&gt;
  &lt;text x=&quot;930&quot; y=&quot;334&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-drop-text&quot;&gt;Mistral Large (mid reasoning)&lt;/text&gt;

  &lt;path d=&quot;M550,342 L550,372&quot; class=&quot;mbp-arrow&quot; marker-end=&quot;url(#mbp-head)&quot; /&gt;

  &lt;rect x=&quot;350&quot; y=&quot;372&quot; width=&quot;400&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;mbp-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;399&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-gate-text&quot;&gt;Cost at 1M req/day tolerable?&lt;/text&gt;

  &lt;rect x=&quot;800&quot; y=&quot;372&quot; width=&quot;260&quot; height=&quot;44&quot; rx=&quot;6&quot; class=&quot;mbp-drop&quot; /&gt;
  &lt;text x=&quot;930&quot; y=&quot;393&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-drop-text&quot;&gt;Opus at ~$15/$75 out; Haiku&lt;/text&gt;
  &lt;text x=&quot;930&quot; y=&quot;408&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-drop-text&quot;&gt;on easy slice via cascade&lt;/text&gt;

  &lt;path d=&quot;M550,416 L550,446&quot; class=&quot;mbp-arrow&quot; marker-end=&quot;url(#mbp-head)&quot; /&gt;

  &lt;rect x=&quot;300&quot; y=&quot;446&quot; width=&quot;500&quot; height=&quot;150&quot; rx=&quot;10&quot; class=&quot;mbp-pick&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;475&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-pick-label&quot;&gt;Claude Sonnet 4.6&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;498&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-detail&quot;&gt;us.anthropic.claude-sonnet-4-6 for US tenants&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;516&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-detail&quot;&gt;eu.anthropic.claude-sonnet-4-6 for EU tenants&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;540&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-detail&quot;&gt;on-demand throughput + prompt caching on the system prompt&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;558&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-detail&quot;&gt;Haiku 4.5 load-shed fallback; cross-geography retry on 5xx&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;580&quot; text-anchor=&quot;middle&quot; class=&quot;mbp-detail&quot;&gt;nightly Bedrock Evaluations against a 500-question golden set&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.85em; color: var(--color-ink-secondary); margin-top: 0.5em;&quot;&gt;Four gates, reasoning, residency, latency, cost, and the seven-family catalogue collapses to Sonnet on a geographic cross-region profile with Haiku as the cost-tier fallback.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;sonnet-in-depth&quot;&gt;Sonnet, in depth&lt;/h3&gt;

&lt;p&gt;Sonnet is where most production RAG applications land: Opus-grade reasoning on most realistic prompts, Haiku-competitive latency for typical RAG input sizes, mid-tier pricing that makes a million-requests-a-day application viable.&lt;/p&gt;

&lt;p&gt;Version choice. 4.5 and 4.6 are both live on Bedrock at the same price. 4.6 is newer; 4.5 has the longer &lt;label for=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-benchmark&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-benchmark-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;benchmark&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-benchmark&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-benchmark-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Benchmark&lt;/span&gt;A standardised test set used to score and compare models.
&lt;/span&gt; track record. New applications default to 4.6; calibrated pipelines stay on 4.5 until the re-calibration is done, because behaviours differ across minor versions and a RAG system prompt is typically tuned against one specific model.&lt;/p&gt;

&lt;p&gt;&lt;label for=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-context-window&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-context-window-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Context window&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-context-window&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-a-bedrock-model-for-high-volume-rag-context-window-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Context window&lt;/span&gt;The maximum number of tokens an LLM can attend to in a single call – prompt plus output combined.
&lt;/span&gt;. 200K tokens. A 3K input sits far below the ceiling; context pressure is nowhere near a concern.&lt;/p&gt;

&lt;p&gt;Latency profile. First-token for a 3K input in a warm region runs under 1.8 s. That’s close to the 1.5 s target, which makes latency a &lt;em&gt;throughput-mode&lt;/em&gt; question rather than a &lt;em&gt;model&lt;/em&gt; question, on-demand variance can push P99 above target under peak load.&lt;/p&gt;

&lt;p&gt;Four ways to buy capacity. On-demand pays per token with no commitment, variable latency under noisy-neighbour contention. Provisioned throughput buys model units at a guaranteed rate on a 1-month or 6-month commitment, predictable latency, committed spend. Batch inference ships 50% off at a 24-hour SLA, fine for offline jobs. Flex tier ships 50% off at best-effort latency, fine for tolerant async. The right default for 30 RPS peak is on-demand with raised quotas; provisioned earns its place when the peak sustains into the hundreds of RPS or a hard latency SLA demands isolated capacity.&lt;/p&gt;

&lt;p&gt;Prompt caching is the cost lever most applications miss. Cache reads cost ~10% of the normal input-token price; cache writes cost ~25% more than normal and populate the cache for ~5 minutes. The scenario’s 3K input is almost certainly ~800 tokens of shared system prompt plus ~1,700 of retrieved context plus ~500 of user question. Marking the system prompt cacheable pays full price once per 5-minute window and 10% everywhere else, a 23% reduction in input cost at the stated numbers, larger if tool definitions and few-shot examples live in the cached prefix. Caching also cuts first-token latency by hundreds of milliseconds, directly against the 1.5 s budget.&lt;/p&gt;

&lt;p&gt;Cross-region inference profiles are how the multi-region requirement collapses to a config change. Call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us.anthropic.claude-sonnet-4-6&lt;/code&gt; and the US invocation spreads across us-east-1, us-east-2, and us-west-2; call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eu.anthropic.claude-sonnet-4-6&lt;/code&gt; and the EU invocation spreads across Frankfurt, Ireland, Paris, and Zurich. If one constituent region fails, the others serve. No code change; on-demand rate applies; no cross-region data-transfer charge on the inference path. Global profiles exist for maximum availability but trade residency; geographic profiles are the right default when US and EU customers are separate.&lt;/p&gt;

&lt;p&gt;Cascading for cost. Not every question needs Sonnet. Routing the easy slice, short queries, straightforward extraction, to Haiku 4.5 at roughly a third of Sonnet’s price is where the daily bill bends. Three shapes in the wild: cascade (try Haiku, retry on Sonnet when confidence is low), pre-route (classify first, choose once), load-shed (Sonnet by default, drop to Haiku when Sonnet’s P99 climbs). Cascading is the most common because it degrades gracefully when the judge is uncertain.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-architecture&quot;&gt;A worked architecture&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Primary model: Sonnet 4.6 via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us.anthropic.claude-sonnet-4-6&lt;/code&gt; for US tenants, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eu.anthropic.claude-sonnet-4-6&lt;/code&gt; for EU. The application routes tenants to the right profile by home region.&lt;/li&gt;
  &lt;li&gt;Throughput mode: on-demand. Quotas raised in advance for peak 30 RPS with margin. Provisioned reviewed quarterly against actual utilisation.&lt;/li&gt;
  &lt;li&gt;Prompt caching: system prompt (roles, instructions, tool definitions) marked cacheable. Cache hit rate monitored as a first-class metric.&lt;/li&gt;
  &lt;li&gt;Cost-tier fallback: shed to Haiku 4.5 when profile P99 exceeds 2 s for 5 min. Haiku via the matching geographic profile.&lt;/li&gt;
  &lt;li&gt;Cross-geography failover: on repeated 5xx from the primary profile, retry once against the other geography. Degraded-residency mode for continuity.&lt;/li&gt;
  &lt;li&gt;Evaluation: 500-question golden dataset, nightly run via Bedrock Evaluations with Sonnet 4.6 as judge, alert on aggregate drops above 5% WoW.&lt;/li&gt;
  &lt;li&gt;Embedding model: Titan Text Embeddings V2 in each region, vector store local to where it’s queried.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rough monthly cost at 1M requests/day, 3K/400 token median, ~25% input cached:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Input: 3,000 x 1M x 30 = 90B/month. Effective ~70B after caching x $3/M = ~$210k.&lt;/li&gt;
  &lt;li&gt;Output: 400 x 1M x 30 = 12B x $15/M = ~$180k.&lt;/li&gt;
  &lt;li&gt;Evaluations, embeddings, incidental Haiku: ~$10k.&lt;/li&gt;
  &lt;li&gt;Total: ~$400k/month, before any volume discounts from the account team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Routing ~40% of traffic to Haiku via a well-calibrated cascade drops total cost to roughly $260k/month for similar quality on the easy slice. That’s where the investment in evaluation pays off, a trustworthy judge makes the cost curve bend.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Seven Bedrock families, only some in both US and EU. Claude, Nova, Llama, Mistral, Cohere, Titan, AI21. The EU-residency gate is what rules most of the catalogue out for a dual-geography product.&lt;/li&gt;
  &lt;li&gt;Claude’s three-tier split (Haiku / Sonnet / Opus) maps to working points. Haiku for latency and cost, Sonnet as the production default, Opus as an escalation rather than a daily-driver.&lt;/li&gt;
  &lt;li&gt;Geographic cross-region inference profiles (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;us.&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eu.&lt;/code&gt;) give automatic in-geography failover at on-demand rates with no surcharge. The primary mechanism for multi-region availability on Bedrock.&lt;/li&gt;
  &lt;li&gt;Four throughput modes. On-demand for smooth sub-hundreds-RPS; provisioned for sustained high throughput or hard latency SLAs; batch for 24-hour async; flex for tolerant async.&lt;/li&gt;
  &lt;li&gt;Prompt caching’s 90% discount on cache reads is the biggest lever on input cost for any RAG workload with a stable system prompt. The 5-minute window and ~25% write premium are the two numbers to remember.&lt;/li&gt;
  &lt;li&gt;Cascading, pre-routing, and load-shedding are three shapes for splitting traffic between Sonnet and Haiku. Cascading is the common production default; load-shedding is the cleaner degradation path than 503s under peak.&lt;/li&gt;
  &lt;li&gt;Bedrock Evaluations runs automatic and LLM-as-a-judge modes against a golden dataset. Nightly evaluation plus alert-on-drop is the discipline that catches model-version drift before users do.&lt;/li&gt;
  &lt;li&gt;Custom Model Import is a niche tool, provisioned-only, limited regions, fine-tuned open-source only. Not a general alternative when Bedrock’s hosted models already clear the quality bar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model choice is the easy part. The work that actually matters is the throughput mode, the failover topology, the caching discipline, and the evaluation harness wrapped around the chosen model, all pieces that compound when the application outgrows a single region and a single quality tier.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Prioritisation: What Changes First</title>
    <link href="/writing/prioritisation-what-changes-first/"/>
    <updated>2026-05-26T06:00:00+08:00</updated>
    <id>/writing/prioritisation-what-changes-first/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/finding-the-fit/&quot;&gt;Finding the Fit&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Greenbox has 200 subscribers but 8% monthly churn. Over three weeks of discovery (&lt;a href=&quot;/writing/jobs-to-be-done-why-subscribers-actually-stay/&quot;&gt;Jobs to Be Done&lt;/a&gt;, &lt;a href=&quot;/writing/assumption-mapping-testing-what-you-believe/&quot;&gt;Assumption Mapping&lt;/a&gt;, &lt;a href=&quot;/writing/business-model-canvas-does-this-actually-work/&quot;&gt;Business Model Canvas&lt;/a&gt;) the team has uncovered more problems than they can fix at once. Now they need to decide what changes first.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s Monday morning and the office whiteboard is covered. Three weeks of discovery work have produced a wall of insights, sticky notes, canvas printouts, and scribbled questions. Maya stands in front of it with a coffee that’s gone cold.&lt;/p&gt;

&lt;p&gt;Sam arrives early, which is unusual. He puts his bag down and opens his laptop before he takes off his jacket. “We lost three subscribers over the weekend.”&lt;/p&gt;

&lt;p&gt;Maya turns. “Churn?”&lt;/p&gt;

&lt;p&gt;“Not exactly. They switched. To Freshly.” Sam turns his laptop around. Freshly’s Perth launch page fills the screen: a clean hero image, the $18 price tag prominent, a “Now delivering in Perth” banner. “They went live on Friday. Three of our subscribers signed up over the weekend and cancelled with us. One of them, Louise, from the JTBD interviews, sent a message: ‘Sorry, but $18 is $18.’”&lt;/p&gt;

&lt;p&gt;Maya stares at the screen. She knew this was coming. Dave had told her Freshly was calling farms. Charlotte’s BMC questions had forced the pricing conversation. But knowing it’s coming and seeing it on a Monday morning are different experiences.&lt;/p&gt;

&lt;p&gt;“Seven dollars a week. Three hundred and sixty-four dollars a year. Of course people switch.”&lt;/p&gt;

&lt;h3 id=&quot;too-much-to-fix&quot;&gt;Too much to fix&lt;/h3&gt;

&lt;p&gt;The insights are clear. A two-tier pricing model could fix the economics. A pause button would reduce churn. The value proposition needs repositioning around convenience. SEO is underinvested. The recipe cards are working but the marketing doesn’t match what customers actually care about.&lt;/p&gt;

&lt;p&gt;Maya knows all of this. The team knows all of this. And that’s the problem.&lt;/p&gt;

&lt;p&gt;By the time everyone arrives, Maya has written five priorities on the whiteboard, each circled in red.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Ship the pause button (reduces churn)&lt;/li&gt;
  &lt;li&gt;Launch two-tier pricing model (fixes unit economics)&lt;/li&gt;
  &lt;li&gt;Reposition the value prop in all marketing&lt;/li&gt;
  &lt;li&gt;Run a mixed-sourcing pilot&lt;/li&gt;
  &lt;li&gt;Start SEO foundation work&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Priya reads the list. “We’re five people.”&lt;/p&gt;

&lt;p&gt;“I know.”&lt;/p&gt;

&lt;p&gt;“That’s five initiatives for five people.”&lt;/p&gt;

&lt;p&gt;Sam looks at the board. “Plus we still have to pack and ship two hundred boxes a week, manage farm relationships, keep the platform running, and prepare a board presentation.” He’s listing his own workload, though he doesn’t frame it that way. He has forty-three unread support emails from the weekend.&lt;/p&gt;

&lt;p&gt;Maya puts down her marker. “I don’t know how to choose.”&lt;/p&gt;

&lt;h3 id=&quot;everyone-has-a-different-answer&quot;&gt;Everyone has a different answer&lt;/h3&gt;

&lt;p&gt;Lee and Charlotte are on the call. The team spends thirty minutes arguing.&lt;/p&gt;

&lt;p&gt;Tom thinks the pause button should be first: highest leverage, small engineering lift. Sam disagrees; fix the value prop messaging and you’ll acquire better-fit customers who churn less in the first place. Jas pushes for two-tier pricing because the board meeting is in three weeks. Priya wants the mixed-sourcing pilot first: you can’t pitch two-tier pricing without validating the supply chain. Maya keeps circling back to SEO.&lt;/p&gt;

&lt;p&gt;Charlotte lets the argument run past the point where it’s productive. Then she says: “Five people, five answers. That’s not a disagreement about priorities. That’s the absence of a framework for deciding.”&lt;/p&gt;

&lt;h3 id=&quot;what-were-optimising-for&quot;&gt;What we’re optimising for&lt;/h3&gt;

&lt;p&gt;“Before we sort anything,” Charlotte says, “we agree on what we’re optimising for this quarter. Otherwise the 2x2 is just opinions in a grid.”&lt;/p&gt;

&lt;p&gt;She types into a shared doc and turns the screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3 Theme: Fix the leaky bucket. Reduce monthly churn below 5%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;“Eight percent monthly means we lose a quarter of our subscribers every year. Everything else is downstream of that. So the first question on every initiative, including the five on the whiteboard, is: does it move churn? By how much, and how fast?”&lt;/p&gt;

&lt;p&gt;Tom frowns. “Two-tier pricing isn’t a churn play. It’s unit economics.”&lt;/p&gt;

&lt;p&gt;“It’s churn through a longer chain. Better economics means we can afford the convenience features that hold subscribers. And the $20 tier closes the gap to Freshly, which is already costing us churn. So yes, it serves the theme. But that’s the test for every initiative on the wall.”&lt;/p&gt;

&lt;h3 id=&quot;impact-and-effort&quot;&gt;Impact and effort&lt;/h3&gt;

&lt;p&gt;With the theme in place, the 2x2 has meaning. The horizontal axis is effort/risk. The vertical axis is impact on churn, the metric the theme picked out. Charlotte scores each initiative.&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: 1fr 1fr; min-height: 0;&quot;&gt;
    &lt;div style=&quot;padding: var(--space-md); background: rgba(46,139,87,0.08); border-right: 1px solid var(--color-rule); border-bottom: 1px solid var(--color-rule);&quot;&gt;
      &lt;strong style=&quot;display: block; margin-bottom: 0.25em; color: var(--color-accent);&quot;&gt;Do First&lt;/strong&gt;
      &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;High impact, low effort/risk&lt;/span&gt;
      &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
        &lt;li&gt;Pause button&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div style=&quot;padding: var(--space-md); background: rgba(220,50,50,0.08); border-bottom: 1px solid var(--color-rule);&quot;&gt;
      &lt;strong style=&quot;display: block; margin-bottom: 0.25em; color: var(--color-accent);&quot;&gt;Big Bet&lt;/strong&gt;
      &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;High impact, high effort/risk&lt;/span&gt;
      &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
        &lt;li&gt;Two-tier pricing model&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div style=&quot;padding: var(--space-md); background: rgba(65,105,225,0.08); border-right: 1px solid var(--color-rule);&quot;&gt;
      &lt;strong style=&quot;display: block; margin-bottom: 0.25em; color: var(--color-ink-tertiary);&quot;&gt;Fill In&lt;/strong&gt;
      &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;Low impact, low effort/risk&lt;/span&gt;
      &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
        &lt;li&gt;Value prop repositioning&lt;/li&gt;
        &lt;li&gt;SEO foundation&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div style=&quot;padding: var(--space-md); background: rgba(184,134,11,0.06);&quot;&gt;
      &lt;strong style=&quot;display: block; margin-bottom: 0.25em; color: var(--color-ink-tertiary);&quot;&gt;Defer&lt;/strong&gt;
      &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;Low impact, high effort/risk&lt;/span&gt;
      &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
        &lt;li&gt;Mixed-sourcing pilot&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Priya objects to the mixed-sourcing pilot being deferred. “We need supply chain data before we can commit to two-tier pricing.”&lt;/p&gt;

&lt;p&gt;“You’re right,” Charlotte says. “But you don’t need a full pilot. You need three phone calls to wholesale suppliers and a week of test orders. That’s not a separate initiative; it’s part of the pricing preparation. The fuller pilot can come later.”&lt;/p&gt;

&lt;h3 id=&quot;now--next--later&quot;&gt;Now / Next / Later&lt;/h3&gt;

&lt;p&gt;Charlotte shares the next screen. Three columns.&lt;/p&gt;

&lt;p&gt;Now is the next four weeks. High impact, high urgency. You can name the people and describe what “done” looks like.&lt;/p&gt;

&lt;p&gt;Next is four to twelve weeks. Important but can wait, or needs more information first.&lt;/p&gt;

&lt;p&gt;Later is beyond twelve weeks. Good ideas that aren’t ready.&lt;/p&gt;

&lt;p&gt;“Everything can’t be Now. If it is, nothing is.”&lt;/p&gt;

&lt;p&gt;The 2x2 doesn’t sort itself into columns. Three things bend it:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Capacity.&lt;/em&gt; Five people. Now holds at most two big initiatives.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Dependencies.&lt;/em&gt; The supply-chain checks the pricing model needs are folded into the pricing work, not listed as a separate Next item.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;External deadlines.&lt;/em&gt; The board meeting is in three weeks. Two-tier pricing is a Big Bet, not a Do First, but the timing pulls it into Now anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The roadmap:&lt;/p&gt;

&lt;div style=&quot;display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0; border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(46,139,87,0.08); border-right: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-accent); font-size: 1rem;&quot;&gt;Now&lt;/strong&gt;
    &lt;span style=&quot;display: block; font-size: 0.8rem; color: var(--color-ink-tertiary); margin-bottom: 0.75em;&quot;&gt;Next 4 weeks&lt;/span&gt;
    &lt;ul style=&quot;padding-left: 1.2em; font-size: 0.88rem; margin: 0;&quot;&gt;
      &lt;li style=&quot;margin-bottom: 0.5em;&quot;&gt;&lt;strong&gt;Pause button&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;font-size: 0.82rem; color: var(--color-ink-secondary);&quot;&gt;Reduce churn from 8% toward 5%&lt;/span&gt;&lt;/li&gt;
      &lt;li style=&quot;margin-bottom: 0.5em;&quot;&gt;&lt;strong&gt;Two-tier pricing model&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;font-size: 0.82rem; color: var(--color-ink-secondary);&quot;&gt;Viable unit economics for board&lt;/span&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(65,105,225,0.08); border-right: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-accent); font-size: 1rem;&quot;&gt;Next&lt;/strong&gt;
    &lt;span style=&quot;display: block; font-size: 0.8rem; color: var(--color-ink-tertiary); margin-bottom: 0.75em;&quot;&gt;4 &amp;ndash; 12 weeks&lt;/span&gt;
    &lt;ul style=&quot;padding-left: 1.2em; font-size: 0.88rem; margin: 0;&quot;&gt;
      &lt;li style=&quot;margin-bottom: 0.5em;&quot;&gt;Mixed-sourcing pilot&lt;/li&gt;
      &lt;li style=&quot;margin-bottom: 0.5em;&quot;&gt;SEO foundation&lt;/li&gt;
      &lt;li style=&quot;margin-bottom: 0.5em;&quot;&gt;Value prop repositioning&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(184,134,11,0.06);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-accent); font-size: 1rem;&quot;&gt;Later&lt;/strong&gt;
    &lt;span style=&quot;display: block; font-size: 0.8rem; color: var(--color-ink-tertiary); margin-bottom: 0.75em;&quot;&gt;Beyond 12 weeks&lt;/span&gt;
    &lt;ul style=&quot;padding-left: 1.2em; font-size: 0.88rem; margin: 0;&quot;&gt;
      &lt;li style=&quot;margin-bottom: 0.5em;&quot;&gt;B2B offerings&lt;/li&gt;
      &lt;li style=&quot;margin-bottom: 0.5em;&quot;&gt;Second city expansion&lt;/li&gt;
      &lt;li style=&quot;margin-bottom: 0.5em;&quot;&gt;Referral programme&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;“Anything that doesn’t move churn, directly or through a short chain, waits.”&lt;/p&gt;

&lt;h3 id=&quot;building-the-now&quot;&gt;Building the Now&lt;/h3&gt;

&lt;p&gt;Tom and Priya take the pause button. They Example Map it on Monday afternoon; twenty-five minutes produces twelve concrete examples and three red cards. They build it in six days.&lt;/p&gt;

&lt;p&gt;Maya and Jas take the two-tier pricing model. Maya spends two days on the phone with Dave, Rachel, and their third farm partner, explaining what mixed sourcing means for local orders.&lt;/p&gt;

&lt;p&gt;Dave is quiet for a long time. Then he asks: “Will the local box subscribers grow?”&lt;/p&gt;

&lt;p&gt;Maya doesn’t know. She says so.&lt;/p&gt;

&lt;p&gt;“Here’s what I need. Don’t blindside me. Give me three months’ notice if the local orders are going to drop. I can find other buyers, but I need time.”&lt;/p&gt;

&lt;p&gt;Maya commits to it. She adds “quarterly farm partner review” to the Later column.&lt;/p&gt;

&lt;p&gt;Jas designs the pricing page. But first, she presents something she’s been working on privately.&lt;/p&gt;

&lt;p&gt;She’d taken the value prop repositioning, the one Maya moved from Now to Next, and done it anyway. Three evenings at home in Leederville, Moleskine open, laptop beside her. She connects her laptop to the office projector without asking anyone’s permission.&lt;/p&gt;

&lt;p&gt;The homepage: “Dinner decided.” Mrs Patterson’s words, now a headline in Greenbox’s brand typeface. Below it, not a photo of vegetables but a photo of a family kitchen, a recipe card propped against a cutting board. The message: we deliver the moment after the decision is made.&lt;/p&gt;

&lt;p&gt;The pricing page: “Local Box, $25/week, 100% locally sourced, seasonal produce from farms within fifty kilometres” and “Fresh Box, $20/week, a mix of local and market-fresh produce, same quality, more variety.” The mixed box isn’t framed as the cheap option. It’s framed as the variety option.&lt;/p&gt;

&lt;p&gt;The about page: not “we source from local farms” but “we take Tuesday night off your plate.” The farm stories are still there, halfway down the page. But the lead is the job.&lt;/p&gt;

&lt;p&gt;Maya stands in front of the projector. She reads every screen twice. “This is the first time the website matches what we actually do.”&lt;/p&gt;

&lt;p&gt;Jas’s eyes fill. She blinks hard and looks down at her Moleskine. She’s been waiting to hear something like that since week one, when she designed the customisation interface that got thrown away, when Maya redirected the product without telling her, when she sat in her Leederville flat thinking about quitting. Her mum’s words about her grandmother: “She never grew what she thought people should eat. She grew what they actually wanted.” The napkin sketch from Mrs Patterson’s interview, with “dinner decided” underlined twice, is still in her Moleskine. It might be the most important thing she’s ever drawn.&lt;/p&gt;

&lt;p&gt;“We can’t ship this yet,” Charlotte says, gently. “Value prop repositioning is Next, not Now. But save every one of these files.”&lt;/p&gt;

&lt;p&gt;Sam catches Jas’s eye across the table and mouths: &lt;em&gt;That was brilliant.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-board-meeting&quot;&gt;The board meeting&lt;/h3&gt;

&lt;p&gt;Maya presents on a Thursday afternoon. Charlotte coaches her the night before: “Don’t start with the product. Start with the problem.”&lt;/p&gt;

&lt;p&gt;Maya starts with the churn number. She walks through the JTBD insight, the assumption mapping, the broken unit economics. Then the plan: Now/Next/Later roadmap, quarterly theme, early results (pause button already shipped, churn trending down in week one).&lt;/p&gt;

&lt;p&gt;One investor, Angela, leans forward. “This is the first time you’ve presented something that isn’t a feature list. You’re showing me the thinking behind the choices.”&lt;/p&gt;

&lt;p&gt;The board approves the next tranche of funding. Not because the plan is guaranteed, but because it’s coherent and evidence-based.&lt;/p&gt;

&lt;p&gt;Angela stays on the call after the others drop off. “The fact that you were willing to present a plan that partially walks away from 100% local sourcing tells me you’re making decisions based on data, not sentiment. That’s what we needed to see.”&lt;/p&gt;

&lt;h3 id=&quot;four-weeks-later&quot;&gt;Four weeks later&lt;/h3&gt;

&lt;p&gt;The pause button: twenty-three subscribers used it. Nineteen resumed. Four extended but none cancelled. Monthly churn dropped from 8% to 5.5%.&lt;/p&gt;

&lt;p&gt;The two-tier model: fourteen new subscribers chose the Fresh Box ($20), six chose Local ($25). Nobody switched from Local to Fresh: the new tier is expanding the market, not cannibalising the existing one. Sam checked five of the Fresh Box subscribers in their welcome call. Three had compared Greenbox to Freshly. The $20 price point made the comparison close enough that the recipe cards tipped the balance.&lt;/p&gt;

&lt;p&gt;“Freshly has better technology and a lower price,” Charlotte says. “You have better curation and a clearer job-to-be-done. The question is which one matters more in six months.”&lt;/p&gt;

&lt;h3 id=&quot;the-draft&quot;&gt;The draft&lt;/h3&gt;

&lt;p&gt;On the evening after the board call, Maya sits at the kitchen table. Nadia pours her a glass of wine.&lt;/p&gt;

&lt;p&gt;“They said yes?”&lt;/p&gt;

&lt;p&gt;“They said yes.”&lt;/p&gt;

&lt;p&gt;“Then why do you look like that?”&lt;/p&gt;

&lt;p&gt;Maya opens her email drafts. The “pausing operations” email is still there: three sentences, unsent, from the night after the BMC session. She reads it once. Then she closes the draft folder. Not deleting it. Not yet.&lt;/p&gt;

&lt;p&gt;“I look like this because the hard part isn’t over. It’s changing shape.”&lt;/p&gt;

&lt;p&gt;Freshly has ninety subscribers in Perth after one month. Sam tracks the number. Greenbox has two hundred and thirty-one. But Freshly’s growth rate is steeper. Dave reported that Rachel got a call from them last week. Rachel told them to get stuffed, but Rachel is one farmer.&lt;/p&gt;

&lt;p&gt;Greenbox raises its funding. The board is satisfied. Churn is dropping. The two-tier model is expanding the market without cannibalising the existing one. The team understands the customer, not the customer they imagined at the Margaret River market, but the real one, the one who hires Greenbox so that dinner is already decided when they walk through the door.&lt;/p&gt;

&lt;p&gt;That’s product-market fit. Not a guess. Evidence.&lt;/p&gt;

&lt;p&gt;The team grows from five to fifteen. Two cities. New subscribers arriving faster than at any point in the company’s history. And then the problems change.&lt;/p&gt;

&lt;p&gt;The codebase that five people understood becomes a system fifteen people need to work in. The architecture that worked at startup scale starts creaking. New developers join and don’t know why things are built the way they are. A change in the billing module breaks the delivery scheduler because nobody realised they were coupled. Tom fixes it in an hour, but the look on his face says he knows: this will happen again, and next time it might not be the billing module. It might be the substitution engine, or the allergen flags, or something that sends the wrong produce to the wrong person.&lt;/p&gt;

&lt;p&gt;The techniques from the first two series got Greenbox here. But “here” is a different kind of problem. Not “what should we build?” but “how do we build at scale without the system collapsing under its own weight?”&lt;/p&gt;

&lt;p&gt;Charlotte has a name for the approach: &lt;a href=&quot;/writing/domain-driven-design-drawing-the-boundaries/&quot;&gt;Domain-Driven Design&lt;/a&gt;. It starts with drawing boundaries around the parts of the system that change for different reasons.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Choosing Between SageMaker, Bedrock, and Purpose-Built AI APIs</title>
    <link href="/writing/choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis/"/>
    <updated>2026-05-25T06:00:00+08:00</updated>
    <id>/writing/choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;AI Practitioner&lt;/strong&gt; · AIF-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;The platform team at a mid-size enterprise has a backlog of five AI-shaped requests, all due by end of sprint:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Call-centre transcription. The support team records 40,000 calls a month. They want searchable transcripts with speaker diarisation (“&lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-ai-agent&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-ai-agent-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Agent&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-ai-agent&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-ai-agent-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Agent&lt;/span&gt;A system that wraps an LLM with tools, memory, and a loop, so it can take multi-step actions toward a goal rather than just answering one prompt.
&lt;/span&gt; said X, then Customer said Y”) and redaction of credit-card numbers spoken aloud.&lt;/li&gt;
  &lt;li&gt;Sensor anomaly detection. The facilities team has 2,000 IoT sensors across 12 sites streaming temperature, humidity, and vibration data. They want alerts when readings stray from normal patterns, patterns that vary by site, sensor type, and time of day.&lt;/li&gt;
  &lt;li&gt;Form text extraction. Ops receives 3,000 scanned supplier invoices a week. They want the invoice number, date, line items, and total extracted into a structured row in a database.&lt;/li&gt;
  &lt;li&gt;Email summarisation. The sales team wants one-paragraph summaries of customer email threads auto-inserted at the top of their CRM view.&lt;/li&gt;
  &lt;li&gt;Visitor-badging face detection. Reception has a camera at the front door; they want a system that detects faces, checks them against an approved-visitor list for that day, and prints a badge (or alerts security).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five requests, five different ML problem types, one sprint, one platform team of six. The question is which AWS service shape fits each, without the team writing five bespoke ML pipelines.&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;AWS organises its AI/ML offerings into three broad tiers, and recognising which tier a request belongs to is most of the work.&lt;/p&gt;

&lt;p&gt;The first question is &lt;em&gt;what kind of ML problem this is&lt;/em&gt;. “AI” is a wide word. Speech-to-text is a solved problem that almost every cloud provider offers as a managed API. Anomaly detection on time-series data has well-understood algorithms but needs tuning per deployment. Document parsing with structured extraction is a specific service category (intelligent document processing). Summarisation is generative text, which points at Bedrock. Face detection is a computer-vision primitive available as a managed API. The &lt;em&gt;problem shape&lt;/em&gt; is the first filter; the &lt;em&gt;platform choice&lt;/em&gt; follows from it.&lt;/p&gt;

&lt;p&gt;The second is the three tiers. The top tier is purpose-built AI services: fully managed APIs for well-defined tasks, one service per named problem (speech-to-text, document extraction, image analysis, NLP, recommendations, fraud detection, and the like). Input goes in, structured output comes out. No &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; to train, no infrastructure to provision. This tier is where you want to live if the problem matches a named service and the service’s accuracy meets your bar.&lt;/p&gt;

&lt;p&gt;The middle tier is foundation-model APIs: managed access to general-purpose &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLMs&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; for generative and broad-language tasks. Text generation, summarisation, &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embedding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt;, &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-rag&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-rag-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;RAG&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-rag&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-rag-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RAG&lt;/span&gt;A pattern where you retrieve relevant documents at query time and stuff them into the prompt so the model can ground its answer on them.
&lt;/span&gt;, agents, image generation. Pay per &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;token&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;. The tier for problems where a general-purpose model is a reasonable substrate (most text understanding, most generation, most RAG).&lt;/p&gt;

&lt;p&gt;The bottom tier is the full ML platform: train your own model, bring your own data, deploy your own endpoint, &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-fine-tuning&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-fine-tuning-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;fine-tune&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-fine-tuning&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-fine-tuning-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Fine-tuning&lt;/span&gt;Continuing to train an already-trained model on a smaller dataset to adapt its behaviour.
&lt;/span&gt; open-source models, run notebooks, orchestrate pipelines. The tier for problems where no managed service fits, custom domains, custom metrics, proprietary architectures, regulated contexts where data can’t leave your VPC, or unusual volumes that shift the cost calculus.&lt;/p&gt;

&lt;p&gt;The third thing is &lt;em&gt;the default pull toward the bottom tier&lt;/em&gt;. Engineers with ML backgrounds reach for the platform because it’s the most powerful. Engineers without ML backgrounds reach for the foundation-model API because it’s the most recent. Neither is the correct first question. The correct first question is: “is there a purpose-built service for this?” If there is, that’s almost always the correct answer, faster to integrate, more accurate on the specific task, cheaper per request, and the team doesn’t become responsible for ongoing model maintenance.&lt;/p&gt;

&lt;p&gt;The fourth is &lt;em&gt;the cost shape per tier&lt;/em&gt;. Purpose-built AI services are usually priced per transaction: per minute of audio, per page of document, per image analysed. Foundation-model APIs are per token (input + output). The full ML platform is per instance-hour for &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-training&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-training-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;training&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-training&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-training-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Training&lt;/span&gt;The process of fitting a model’s weights to data by minimising a loss function.
&lt;/span&gt; and per instance-hour for hosted endpoints, regardless of traffic. At low-to-medium volume, managed APIs crush the alternatives on cost; at very high volume, fixed-per-hour pricing can win if your traffic saturates the endpoint. The crossover point is usually higher than teams assume.&lt;/p&gt;

&lt;p&gt;The fifth is &lt;em&gt;accuracy and customisation&lt;/em&gt;. A managed API gives you the accuracy the vendor tuned for the general case. If your domain is specific enough, medical transcription, legal documents, audio in a noisy factory, accuracy may fall short, and a custom model trained on your data can beat the general API. But this is empirical, not assumed: benchmark the managed API against a test set before concluding you need to build.&lt;/p&gt;

&lt;p&gt;The sixth is &lt;em&gt;the operational cost&lt;/em&gt;. A managed API is an HTTPS call. Bedrock is an HTTPS call plus a bit of &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt engineering&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt;. SageMaker requires endpoint capacity planning, auto-scaling configuration, model-version deployment, CloudWatch monitoring, and a team that stays current on SageMaker’s operational surface. These costs compound over the model’s life, not just at launch.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Five filters, applied to each of the three tiers.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Time to first working version, hours, days, or weeks?&lt;/li&gt;
  &lt;li&gt;Customisability, can you fine-tune, retrain, or change the model?&lt;/li&gt;
  &lt;li&gt;Cost shape, per-transaction, per-token, or per-instance-hour?&lt;/li&gt;
  &lt;li&gt;Infrastructure overhead, do you run an endpoint, or does AWS?&lt;/li&gt;
  &lt;li&gt;Breadth of task types, narrow, broad, or arbitrary?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-three-tier-landscape&quot;&gt;The three-tier landscape&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;AI services (top tier). Purpose-built, task-specific managed APIs. Each service does one thing well:
    &lt;ul&gt;
      &lt;li&gt;Transcribe, speech-to-text with diarisation, custom vocabulary, PII redaction, real-time streaming option.&lt;/li&gt;
      &lt;li&gt;Comprehend, sentiment, entities, key phrases, language detection, topic modelling; custom classification and entity recognition via Comprehend Custom.&lt;/li&gt;
      &lt;li&gt;Textract. OCR with table and form extraction, plus specialised APIs for invoices and identity documents.&lt;/li&gt;
      &lt;li&gt;Rekognition, face detection and matching, object and scene detection, moderation, text-in-image, video analysis.&lt;/li&gt;
      &lt;li&gt;Translate, neural machine translation, 75+ languages.&lt;/li&gt;
      &lt;li&gt;Polly, text-to-speech.&lt;/li&gt;
      &lt;li&gt;Personalize, recommendation systems.&lt;/li&gt;
      &lt;li&gt;Fraud Detector, custom fraud models on structured data.&lt;/li&gt;
      &lt;li&gt;Forecast (retired as standalone; folded into SageMaker Canvas’s time-series mode).&lt;/li&gt;
      &lt;li&gt;Kendra, intelligent search over enterprise documents.&lt;/li&gt;
      &lt;li&gt;Lookout for Equipment / Metrics / Vision, anomaly detection for industrial equipment, metrics, and visual inspection.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pricing: per-transaction (per minute, per page, per image). No infrastructure.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Bedrock (middle tier). Foundation-model APIs for generative and general-purpose tasks. Catalogue of models (Anthropic Claude, Amazon Nova and Titan, Meta Llama, Mistral, Cohere, AI21). Primary APIs: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; for synchronous generation, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; for managed RAG, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeAgent&lt;/code&gt; for agent workflows. Pricing: per input + output token, with provisioned-throughput option for high-volume or fine-tuned models. No infrastructure.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;SageMaker (bottom tier). Full ML platform. Surfaces include:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;SageMaker Studio, the IDE for ML: notebooks, experiments, pipelines.&lt;/li&gt;
      &lt;li&gt;SageMaker Training, run training jobs on managed compute (CPU, GPU, Trainium).&lt;/li&gt;
      &lt;li&gt;SageMaker Endpoints, hosted &lt;label for=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-sagemaker-bedrock-and-purpose-built-ai-apis-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt;, real-time or asynchronous or batch transform.&lt;/li&gt;
      &lt;li&gt;SageMaker Autopilot. AutoML for tabular data.&lt;/li&gt;
      &lt;li&gt;SageMaker Canvas, no-code UI on top of Autopilot plus time-series forecasting.&lt;/li&gt;
      &lt;li&gt;SageMaker JumpStart, catalogue of open-weight foundation models deployable to your own endpoint.&lt;/li&gt;
      &lt;li&gt;SageMaker Ground Truth, data labelling.&lt;/li&gt;
      &lt;li&gt;SageMaker Clarify, bias detection and explainability.&lt;/li&gt;
      &lt;li&gt;SageMaker Feature Store, managed feature storage.&lt;/li&gt;
      &lt;li&gt;SageMaker Pipelines. MLOps orchestration.&lt;/li&gt;
      &lt;li&gt;SageMaker Model Registry, versioned model artefacts with approval workflow.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pricing: per-instance-hour for training jobs and endpoints (ml.m5, ml.g5, ml.p5 etc.), plus storage and data transfer. Full VPC isolation; your models live in your account.&lt;/p&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tier&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Time to v1&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Customisable&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Cost shape&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Infra&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Task breadth&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;AI services (Transcribe, etc.)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Hours&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Limited (custom vocabs, classifiers)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Per-transaction&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;None&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Narrow (one task each)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Bedrock&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Hours-days&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Prompt + RAG + fine-tune&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Per-token&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;None&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Broad (any text task; growing image)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SageMaker&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Days-weeks&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Any&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Per-instance-hour&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Endpoint management&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Arbitrary&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Reading the table in reverse is instructive. If your problem matches an AI service, you’re done in hours. If it’s a generative or general-text task, Bedrock is hours to days. Only drop to SageMaker if neither fits, because the task is so domain-specific no managed service addresses it, or because volume makes per-transaction pricing lose to per-hour.&lt;/p&gt;

&lt;h3 id=&quot;mapping-the-five-requests&quot;&gt;Mapping the five requests&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 640&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;AWS AI service tiers shown as three horizontal bands. Top band labelled AI Services contains Transcribe, Comprehend, Textract, Rekognition, Translate, Polly, Personalize, Lookout, Kendra. Middle band labelled Bedrock contains Claude, Nova, Titan, Llama, Knowledge Bases, Agents, Guardrails. Bottom band labelled SageMaker contains Studio, Training, Endpoints, JumpStart, Canvas, Autopilot, Pipelines, Model Registry. Five request tokens overlaid, each landing on the appropriate tier: call-centre transcription lands on Transcribe in the top band, sensor anomaly detection lands on Lookout for Equipment in the top band, form extraction lands on Textract in the top band, email summarisation lands on Bedrock in the middle band, visitor face detection lands on Rekognition in the top band.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .tax-band-ai    { fill: rgba(46, 138, 90, 0.08); stroke: rgb(46, 138, 90); stroke-width: 2; }
      .tax-band-bed   { fill: rgba(100, 60, 180, 0.08); stroke: rgb(90, 58, 160); stroke-width: 2; }
      .tax-band-sm    { fill: rgba(70, 140, 200, 0.08); stroke: rgb(50, 110, 170); stroke-width: 2; }
      .tax-band-label-ai  { font-size: 14px; font-weight: 700; fill: rgb(36, 108, 70); }
      .tax-band-label-bed { font-size: 14px; font-weight: 700; fill: rgb(80, 45, 140); }
      .tax-band-label-sm  { font-size: 14px; font-weight: 700; fill: rgb(40, 95, 150); }
      .tax-svc        { fill: #fff; stroke: #333; stroke-width: 1; }
      .tax-svc-text   { font-size: 11px; fill: #222; }
      .tax-req        { fill: rgba(214, 142, 41, 0.15); stroke: #b08020; stroke-width: 2; }
      .tax-req-title  { font-size: 11px; font-weight: 700; fill: #333; }
      .tax-req-sub    { font-size: 10px; fill: #444; }
      .tax-header     { font-size: 16px; font-weight: 700; fill: #222; }
      .tax-tier-tag   { font-size: 10px; font-style: italic; fill: #555; }
      .tax-arrow      { fill: none; stroke: #b08020; stroke-width: 1.5; stroke-dasharray: 4 3; }
    &lt;/style&gt;
    &lt;marker id=&quot;tax-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;6&quot; markerHeight=&quot;6&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#b08020&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;40&quot; y=&quot;30&quot; class=&quot;tax-header&quot;&gt;AWS AI tiers and where the five requests land&lt;/text&gt;

  &lt;!-- AI Services band --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;60&quot; width=&quot;1020&quot; height=&quot;150&quot; rx=&quot;8&quot; class=&quot;tax-band-ai&quot; /&gt;
  &lt;text x=&quot;60&quot; y=&quot;84&quot; class=&quot;tax-band-label-ai&quot;&gt;AI Services (purpose-built managed APIs)&lt;/text&gt;
  &lt;text x=&quot;60&quot; y=&quot;100&quot; class=&quot;tax-tier-tag&quot;&gt;pricing: per-transaction · infra: none · time-to-v1: hours&lt;/text&gt;

  &lt;!-- Services in AI band --&gt;
  &lt;rect x=&quot;60&quot; y=&quot;115&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;105&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Transcribe&lt;/text&gt;
  &lt;rect x=&quot;160&quot; y=&quot;115&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;205&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Comprehend&lt;/text&gt;
  &lt;rect x=&quot;260&quot; y=&quot;115&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;305&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Textract&lt;/text&gt;
  &lt;rect x=&quot;360&quot; y=&quot;115&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;405&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Rekognition&lt;/text&gt;
  &lt;rect x=&quot;460&quot; y=&quot;115&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;505&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Translate&lt;/text&gt;
  &lt;rect x=&quot;560&quot; y=&quot;115&quot; width=&quot;70&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;595&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Polly&lt;/text&gt;
  &lt;rect x=&quot;640&quot; y=&quot;115&quot; width=&quot;100&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;690&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Personalize&lt;/text&gt;
  &lt;rect x=&quot;750&quot; y=&quot;115&quot; width=&quot;110&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;805&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Lookout family&lt;/text&gt;
  &lt;rect x=&quot;870&quot; y=&quot;115&quot; width=&quot;80&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Kendra&lt;/text&gt;
  &lt;rect x=&quot;960&quot; y=&quot;115&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;1005&quot; y=&quot;134&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Fraud Detector&lt;/text&gt;

  &lt;rect x=&quot;60&quot; y=&quot;155&quot; width=&quot;150&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;135&quot; y=&quot;174&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Comprehend Medical&lt;/text&gt;
  &lt;rect x=&quot;220&quot; y=&quot;155&quot; width=&quot;150&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;295&quot; y=&quot;174&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Transcribe Medical&lt;/text&gt;
  &lt;rect x=&quot;380&quot; y=&quot;155&quot; width=&quot;150&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;455&quot; y=&quot;174&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Textract (Invoices API)&lt;/text&gt;

  &lt;!-- Bedrock band --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;230&quot; width=&quot;1020&quot; height=&quot;130&quot; rx=&quot;8&quot; class=&quot;tax-band-bed&quot; /&gt;
  &lt;text x=&quot;60&quot; y=&quot;254&quot; class=&quot;tax-band-label-bed&quot;&gt;Bedrock (foundation-model APIs)&lt;/text&gt;
  &lt;text x=&quot;60&quot; y=&quot;270&quot; class=&quot;tax-tier-tag&quot;&gt;pricing: per-token · infra: none · time-to-v1: hours to days&lt;/text&gt;

  &lt;rect x=&quot;60&quot; y=&quot;285&quot; width=&quot;80&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;100&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Claude&lt;/text&gt;
  &lt;rect x=&quot;150&quot; y=&quot;285&quot; width=&quot;80&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Nova&lt;/text&gt;
  &lt;rect x=&quot;240&quot; y=&quot;285&quot; width=&quot;80&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;280&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Titan&lt;/text&gt;
  &lt;rect x=&quot;330&quot; y=&quot;285&quot; width=&quot;80&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;370&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Llama&lt;/text&gt;
  &lt;rect x=&quot;420&quot; y=&quot;285&quot; width=&quot;80&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;460&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Mistral&lt;/text&gt;
  &lt;rect x=&quot;510&quot; y=&quot;285&quot; width=&quot;80&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Cohere&lt;/text&gt;
  &lt;rect x=&quot;610&quot; y=&quot;285&quot; width=&quot;140&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;680&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Knowledge Bases&lt;/text&gt;
  &lt;rect x=&quot;760&quot; y=&quot;285&quot; width=&quot;80&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;800&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Agents&lt;/text&gt;
  &lt;rect x=&quot;850&quot; y=&quot;285&quot; width=&quot;100&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;900&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Guardrails&lt;/text&gt;
  &lt;rect x=&quot;960&quot; y=&quot;285&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;1005&quot; y=&quot;304&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Evaluation&lt;/text&gt;

  &lt;rect x=&quot;60&quot; y=&quot;325&quot; width=&quot;150&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;135&quot; y=&quot;344&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Fine-tuning (Custom Models)&lt;/text&gt;
  &lt;rect x=&quot;220&quot; y=&quot;325&quot; width=&quot;170&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;305&quot; y=&quot;344&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Provisioned Throughput&lt;/text&gt;
  &lt;rect x=&quot;400&quot; y=&quot;325&quot; width=&quot;150&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;475&quot; y=&quot;344&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Invocation Logging&lt;/text&gt;

  &lt;!-- SageMaker band --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;380&quot; width=&quot;1020&quot; height=&quot;130&quot; rx=&quot;8&quot; class=&quot;tax-band-sm&quot; /&gt;
  &lt;text x=&quot;60&quot; y=&quot;404&quot; class=&quot;tax-band-label-sm&quot;&gt;SageMaker (full ML platform)&lt;/text&gt;
  &lt;text x=&quot;60&quot; y=&quot;420&quot; class=&quot;tax-tier-tag&quot;&gt;pricing: per-instance-hour · infra: you run it · time-to-v1: days to weeks&lt;/text&gt;

  &lt;rect x=&quot;60&quot; y=&quot;435&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;105&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Studio&lt;/text&gt;
  &lt;rect x=&quot;160&quot; y=&quot;435&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;205&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Training&lt;/text&gt;
  &lt;rect x=&quot;260&quot; y=&quot;435&quot; width=&quot;100&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;310&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Endpoints&lt;/text&gt;
  &lt;rect x=&quot;370&quot; y=&quot;435&quot; width=&quot;100&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;420&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;JumpStart&lt;/text&gt;
  &lt;rect x=&quot;480&quot; y=&quot;435&quot; width=&quot;90&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;525&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Canvas&lt;/text&gt;
  &lt;rect x=&quot;580&quot; y=&quot;435&quot; width=&quot;100&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;630&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Autopilot&lt;/text&gt;
  &lt;rect x=&quot;690&quot; y=&quot;435&quot; width=&quot;100&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;740&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Pipelines&lt;/text&gt;
  &lt;rect x=&quot;800&quot; y=&quot;435&quot; width=&quot;130&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;865&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Model Registry&lt;/text&gt;
  &lt;rect x=&quot;940&quot; y=&quot;435&quot; width=&quot;110&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;995&quot; y=&quot;454&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Ground Truth&lt;/text&gt;

  &lt;rect x=&quot;60&quot; y=&quot;475&quot; width=&quot;100&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;110&quot; y=&quot;494&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Clarify&lt;/text&gt;
  &lt;rect x=&quot;170&quot; y=&quot;475&quot; width=&quot;150&quot; height=&quot;30&quot; rx=&quot;3&quot; class=&quot;tax-svc&quot; /&gt;
  &lt;text x=&quot;245&quot; y=&quot;494&quot; text-anchor=&quot;middle&quot; class=&quot;tax-svc-text&quot;&gt;Feature Store&lt;/text&gt;

  &lt;!-- Five request tokens --&gt;
  &lt;rect x=&quot;80&quot; y=&quot;540&quot; width=&quot;170&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;tax-req&quot; /&gt;
  &lt;text x=&quot;165&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-title&quot;&gt;Call-centre transcription&lt;/text&gt;
  &lt;text x=&quot;165&quot; y=&quot;580&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;Transcribe + PII redaction&lt;/text&gt;
  &lt;text x=&quot;165&quot; y=&quot;595&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;(AI services tier)&lt;/text&gt;
  &lt;path d=&quot;M165,540 L105,146&quot; class=&quot;tax-arrow&quot; marker-end=&quot;url(#tax-arrow)&quot; /&gt;

  &lt;rect x=&quot;270&quot; y=&quot;540&quot; width=&quot;170&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;tax-req&quot; /&gt;
  &lt;text x=&quot;355&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-title&quot;&gt;Sensor anomaly detection&lt;/text&gt;
  &lt;text x=&quot;355&quot; y=&quot;580&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;Lookout for Equipment&lt;/text&gt;
  &lt;text x=&quot;355&quot; y=&quot;595&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;(AI services tier)&lt;/text&gt;
  &lt;path d=&quot;M355,540 L805,146&quot; class=&quot;tax-arrow&quot; marker-end=&quot;url(#tax-arrow)&quot; /&gt;

  &lt;rect x=&quot;460&quot; y=&quot;540&quot; width=&quot;170&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;tax-req&quot; /&gt;
  &lt;text x=&quot;545&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-title&quot;&gt;Form text extraction&lt;/text&gt;
  &lt;text x=&quot;545&quot; y=&quot;580&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;Textract Invoices API&lt;/text&gt;
  &lt;text x=&quot;545&quot; y=&quot;595&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;(AI services tier)&lt;/text&gt;
  &lt;path d=&quot;M545,540 L455,186&quot; class=&quot;tax-arrow&quot; marker-end=&quot;url(#tax-arrow)&quot; /&gt;

  &lt;rect x=&quot;650&quot; y=&quot;540&quot; width=&quot;170&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;tax-req&quot; /&gt;
  &lt;text x=&quot;735&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-title&quot;&gt;Email summarisation&lt;/text&gt;
  &lt;text x=&quot;735&quot; y=&quot;580&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;Bedrock + Claude/Nova&lt;/text&gt;
  &lt;text x=&quot;735&quot; y=&quot;595&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;(Bedrock tier)&lt;/text&gt;
  &lt;path d=&quot;M735,540 L100,316&quot; class=&quot;tax-arrow&quot; marker-end=&quot;url(#tax-arrow)&quot; /&gt;

  &lt;rect x=&quot;840&quot; y=&quot;540&quot; width=&quot;170&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;tax-req&quot; /&gt;
  &lt;text x=&quot;925&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-title&quot;&gt;Visitor face detection&lt;/text&gt;
  &lt;text x=&quot;925&quot; y=&quot;580&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;Rekognition&lt;/text&gt;
  &lt;text x=&quot;925&quot; y=&quot;595&quot; text-anchor=&quot;middle&quot; class=&quot;tax-req-sub&quot;&gt;(AI services tier)&lt;/text&gt;
  &lt;path d=&quot;M925,540 L405,146&quot; class=&quot;tax-arrow&quot; marker-end=&quot;url(#tax-arrow)&quot; /&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Four of the five requests land on purpose-built AI services. Only summarisation genuinely wants Bedrock. None of them justifies dropping to SageMaker.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-picks-in-depth&quot;&gt;The picks in depth&lt;/h3&gt;

&lt;p&gt;Call-centre transcription. Amazon Transcribe. The service has a call-analytics mode (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StartCallAnalyticsJob&lt;/code&gt;) specifically for contact-centre audio: produces transcripts with speaker labels (“AGENT” / “CUSTOMER”), automatic sentiment scoring, issue detection, and content redaction that blanks out card numbers, SSNs, and other PII in the output. Input is an S3 URI of audio; output is JSON to another S3 URI. 40,000 calls a month at, say, 8 minutes average = 320,000 minutes; Transcribe Call Analytics prices per minute, so the monthly bill is straightforward to forecast. Custom vocabularies handle product names, internal jargon, and employee names. No model to train, no endpoint to host.&lt;/p&gt;

&lt;p&gt;Sensor anomaly detection. Amazon Lookout for Equipment. Purpose-built for multivariate anomaly detection on industrial sensor data. You upload historical sensor readings, Lookout trains a site-specific model automatically, and you stream readings in for real-time inference. Handles site-to-site variation naturally (each asset or site can have its own model), doesn’t need you to hand-label anomalies (it learns normal patterns from the healthy history). The alternative, building this on SageMaker with a custom model, is a weeks-to-months project; Lookout is days. (Note: the service is being wound down; at the time of writing, Amazon is directing new customers toward SageMaker’s own anomaly-detection capabilities. Check current guidance before new builds.)&lt;/p&gt;

&lt;p&gt;Form text extraction. Amazon Textract. Textract has a dedicated Invoice API (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AnalyzeExpense&lt;/code&gt;) that returns structured fields from invoices: vendor name, invoice number, date, line items, totals, tax, currency. A per-page API call per invoice, output is JSON with each field tagged by type. 3,000 invoices a week at a page each is ~12,000 pages a month; straightforward per-page pricing. For non-invoice forms the generic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AnalyzeDocument&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FORMS&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TABLES&lt;/code&gt; feature types does the same for arbitrary structured layouts.&lt;/p&gt;

&lt;p&gt;Email summarisation. Bedrock. No purpose-built AI service exists for generic summarisation. Comprehend has a summarisation capability but it’s extractive (pulls existing sentences); for a paragraph summary written in the company’s voice, a foundation model is the correct tool. Bedrock with Claude Sonnet or Nova Lite, one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; call per email thread, a well-engineered prompt. Volume is low (sales-team-scale, probably low-thousands a day); on-demand per-token pricing. The Bedrock-tier pick that earns its place.&lt;/p&gt;

&lt;p&gt;Visitor face detection. Amazon Rekognition. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IndexFaces&lt;/code&gt; adds today’s approved visitors to a face collection at the start of the day; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SearchFacesByImage&lt;/code&gt; on each camera frame returns matches above a similarity threshold. Low latency, fully managed. Quotas and pricing scale with API calls; the whole system is a Lambda + S3 + Rekognition pipeline. No custom model training.&lt;/p&gt;

&lt;p&gt;Note that four of five are AI services and the fifth is Bedrock. &lt;em&gt;None&lt;/em&gt; of them is SageMaker. That’s the correct outcome for this backlog: purpose-built services cover the named tasks, Bedrock covers the generative task, and SageMaker is reserved for the problems none of the above handles.&lt;/p&gt;

&lt;h3 id=&quot;when-would-sagemaker-win&quot;&gt;When would SageMaker win?&lt;/h3&gt;

&lt;p&gt;“SageMaker” is still the default someone types, so it’s worth being clear when it actually wins. SageMaker is the correct tier when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The task isn’t covered by an AI service. A custom computer-vision model for detecting specific defects in your specific product on your specific assembly line, where Rekognition’s general models don’t have the precision. A custom NLP classifier for a domain-specific taxonomy Comprehend Custom can’t learn. A custom regression on industrial sensor data that goes beyond what Lookout handles.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The workload is high-volume enough that per-transaction pricing loses. If you’d be calling Transcribe 10 million times a month and your contract negotiated you a bulk price that’s still more than running a self-hosted ASR model on a few ml.g5 endpoints, then self-hosting wins. This is unusual; the crossover is higher than teams assume.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Data residency or VPC isolation. Some AI services have VPC endpoints; some don’t. If your data can’t leave a specific VPC, or can’t be sent to a managed API under any circumstances, SageMaker endpoints inside your VPC are the answer.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;You need to explain or audit the model’s behaviour in detail. Managed APIs are black boxes. SageMaker lets you inspect, explain (SageMaker Clarify), and version every model; for regulated contexts where “why did the model do this?” needs a substantive answer, that visibility matters.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;You’re doing research, experimentation, or model development, not consumption. SageMaker Studio is an ML development environment. Managed APIs are ML consumption surfaces. If your team’s job is to build models, Studio is the workspace.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this platform team’s five requests, none of those conditions applies. They’re consuming ML capabilities, not developing models. AI services and Bedrock are correct; SageMaker would be over-engineering.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-sprint&quot;&gt;A worked sprint&lt;/h3&gt;

&lt;p&gt;How the team could schedule the five builds across a two-week sprint:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Week 1
  Day 1: team walks the five requests against the taxonomy.
         Writes up tier assignments + rough cost estimates.
  Day 2: parallelise.
    Pair A: Transcribe call-analytics pipeline
      (S3 + Lambda + StartCallAnalyticsJob + result consumer)
    Pair B: Textract Invoices pipeline
      (S3 + Lambda + AnalyzeExpense + DB writer)
    Pair C: Bedrock email summariser
      (Lambda + InvokeModel + prompt tuning + CRM integration)
  Day 3-4: each pair gets to a working end-to-end version.
  Day 5: review, measure accuracy against a held-out set of
         real inputs, tune thresholds / prompts / custom vocabs.

Week 2
  Day 1-2: Pair A picks up visitor face detection
    (Rekognition + face collection management + badge printing).
  Day 1-2: Pair B picks up sensor anomaly detection
    (Lookout for Equipment, historical data ingest, inference
    stream wiring, alerting).
  Day 3-4: Pair C does guardrails, monitoring, IAM scoping
           across all five pipelines.
  Day 5: end-to-end review with stakeholder sign-off per request.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Six engineers delivering five AI-shaped features in a sprint is only realistic because none of them is a from-scratch ML project. Each is “wire up a managed service correctly.” If any of the five had required SageMaker, that one alone would have consumed the sprint.&lt;/p&gt;

&lt;h3 id=&quot;the-default-to-hold&quot;&gt;The default to hold&lt;/h3&gt;

&lt;p&gt;When a new AI-shaped request lands, the sequence worth running is:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Is there a purpose-built AI service for this task? (Transcribe for speech, Textract for forms, Rekognition for images, Comprehend for NLP, Lookout for sensors, etc.) If yes and accuracy meets the bar, use it.&lt;/li&gt;
  &lt;li&gt;If no, is it a generative / general-text / RAG task? If yes, Bedrock.&lt;/li&gt;
  &lt;li&gt;If neither, does the problem genuinely require custom ML? If yes, SageMaker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most requests terminate at step 1. A significant minority at step 2. A small minority at step 3. Reversing the sequence, starting with SageMaker and asking “could this be done simpler?”, is how teams end up running ML pipelines for problems Transcribe would have solved in a day.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;AWS organises AI/ML into three tiers. Purpose-built AI services (Transcribe, Textract, Rekognition, Comprehend, etc.); Bedrock for foundation models; SageMaker for the full ML platform. The tier is the first question; the specific service follows.&lt;/li&gt;
  &lt;li&gt;AI services win when the task matches. Transcribe does call-centre audio with diarisation and PII redaction out of the box. Textract does forms. Rekognition does faces. These services are tuned by teams of specialists; competing with them from scratch takes months.&lt;/li&gt;
  &lt;li&gt;Bedrock is the foundation-model tier. Generative text, summarisation, RAG, agents, embeddings. Per-token pricing. When the task is “something with text that’s not covered by a specific AI service,” Bedrock is usually correct.&lt;/li&gt;
  &lt;li&gt;SageMaker is the platform of last resort, not first choice. The correct tier when no managed service fits, when volume shifts the cost calculus, when data residency demands it, or when you’re developing rather than consuming models.&lt;/li&gt;
  &lt;li&gt;Pricing shape varies by tier. Per-transaction (AI services), per-token (Bedrock), per-instance-hour (SageMaker). Low-to-medium volume favours managed per-transaction pricing; very high volume can tip toward SageMaker’s per-hour model.&lt;/li&gt;
  &lt;li&gt;Time-to-first-version tracks the tier. AI services are hours. Bedrock is hours-to-days. SageMaker is days-to-weeks. Budget accordingly.&lt;/li&gt;
  &lt;li&gt;Custom vocabularies, custom classifiers, and custom entities bridge the accuracy gap on AI services. Transcribe custom vocabularies, Comprehend Custom classification and entity recognition, Textract custom adapters. Often enough to close the accuracy gap without dropping to SageMaker.&lt;/li&gt;
  &lt;li&gt;The default sequence is AI service -&amp;gt; Bedrock -&amp;gt; SageMaker. Start at the top, drop only when the tier above can’t meet the requirement. Reversing the sequence is how teams build custom ML pipelines for problems managed services would have solved in a day.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Five AI-shaped requests, one sprint, and no custom models. That’s the shape the AWS AI stack is designed for: most “AI” problems are not ML research projects; they’re integrations of capabilities someone else already built and tuned. Recognising which tier a problem belongs to, and holding the discipline to stay as high as the problem allows, is most of what makes a platform team fast.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Boring Baseline That Wins</title>
    <link href="/writing/the-boring-baseline-that-wins/"/>
    <updated>2026-05-23T06:00:00+08:00</updated>
    <id>/writing/the-boring-baseline-that-wins/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;You have 4,000 customer reviews. Half are positive, half are negative, more or less. You want a sentiment classifier. The team’s first instinct is to call the &lt;label for=&quot;sn-writing-the-boring-baseline-that-wins-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-boring-baseline-that-wins-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; API once per review and parse the response. The bill is real, the latency is real, and the accuracy on your specific data is unproven.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An afternoon’s work in scikit-learn produces a &lt;label for=&quot;sn-writing-the-boring-baseline-that-wins-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-boring-baseline-that-wins-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; that hits 92% accuracy, runs at 50,000 predictions per second on a CPU, and costs nothing per call. The afternoon includes lunch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This shouldn’t be an unusual outcome, but increasingly it is.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There’s a recurring pattern in machine learning projects: someone reaches for the most sophisticated tool first, struggles with it, and only later discovers that a “boring” classical baseline. TF-IDF features fed into a logistic regression, would have solved the problem in an hour. &lt;a href=&quot;/writing/before-the-transformer/&quot;&gt;The previous post&lt;/a&gt; covered the classical NLP that still ships in production. This post covers the classical machine learning that should be the default starting point for most text-classification, clustering, and topic-modelling projects.&lt;/p&gt;

&lt;p&gt;Not because neural models are bad. Because for problems below a certain size and complexity, the boring tools are simply the correct answer.&lt;/p&gt;

&lt;h3 id=&quot;tf-idf-the-trick-that-wont-die&quot;&gt;TF-IDF: the trick that won’t die&lt;/h3&gt;

&lt;p&gt;TF-IDF. Term Frequency / Inverse Document Frequency, is a way of turning a piece of text into a vector of numbers based on which words appear in it and how distinctive those words are.&lt;/p&gt;

&lt;p&gt;The intuition is simple. For each word in your vocabulary, multiply two numbers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;TF: how often the word appears in this document. Common words score high.&lt;/li&gt;
  &lt;li&gt;IDF: a penalty for words that appear in many documents. Words that are common everywhere (like “the” or “and”) score low. Words that appear in only a few documents score high.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a feature vector where words that are &lt;em&gt;distinctive&lt;/em&gt; to a document score highly and words that are common across the corpus score low. “Refund” in a customer-service ticket scores high; “the” scores near zero.&lt;/p&gt;

&lt;p&gt;That’s it. There’s no neural network, no &lt;label for=&quot;sn-writing-the-boring-baseline-that-wins-training&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-boring-baseline-that-wins-training-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;training&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-training&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-training-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Training&lt;/span&gt;The process of fitting a model’s weights to data by minimising a loss function.
&lt;/span&gt; in the modern sense. You count words, you weight them, you have a feature vector. The whole pipeline is a hundred lines of Python or a single call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sklearn.feature_extraction.text.TfidfVectorizer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And it works. Astonishingly well, for a fifty-year-old idea.&lt;/p&gt;

&lt;h3 id=&quot;logistic-regression-on-tf-idf-features&quot;&gt;Logistic regression on TF-IDF features&lt;/h3&gt;

&lt;p&gt;Once you have TF-IDF vectors, you can feed them into any classifier. The most-used and least-glamorous choice is logistic regression: a linear model that learns a weight for each feature and predicts the probability of each class as a logistic function of the weighted sum.&lt;/p&gt;

&lt;p&gt;For text classification with reasonable amounts of data (a few thousand to a few hundred thousand labelled examples), TF-IDF + logistic regression is often within a few percentage points of the best deep-learning model, and orders of magnitude cheaper to train, deploy, and explain.&lt;/p&gt;

&lt;p&gt;Real numbers from real projects:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sentiment analysis on movie reviews (50k examples, IMDB-style): TF-IDF + logistic regression hits ~89% accuracy. A fine-tuned BERT hits ~94%. A frontier LLM with a prompt hits ~92%. The first one trains in 30 seconds and runs at 50,000 predictions per second on a CPU.&lt;/li&gt;
  &lt;li&gt;Spam detection (millions of emails): TF-IDF + logistic regression or naive Bayes is &lt;em&gt;still&lt;/em&gt; the production standard at most large mail providers. The neural model would be more accurate by a percentage point and cost a thousand times more to run at scale.&lt;/li&gt;
  &lt;li&gt;Topic classification of news articles (20-30 classes, 100k articles): TF-IDF + logistic regression matches BERT to within a couple of points and runs in milliseconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern holds: when the task is “find a stable mapping from word patterns to a fixed set of labels,” and you have a few thousand examples, the linear model on lexical features is the sensible baseline.&lt;/p&gt;

&lt;h3 id=&quot;when-the-linear-model-isnt-enough&quot;&gt;When the linear model isn’t enough&lt;/h3&gt;

&lt;p&gt;The boring baseline has known weaknesses, and they’re the cases where you actually want a &lt;label for=&quot;sn-writing-the-boring-baseline-that-wins-transformer&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-boring-baseline-that-wins-transformer-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;transformer&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-transformer&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-transformer-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Transformer&lt;/span&gt;The neural network architecture that underpins modern LLMs – stacks of self-attention layers that let every token look at every other token in the context.
&lt;/span&gt;.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Paraphrase and synonymy. “I’m furious” and “I’m absolutely livid” are obviously sentiment-equivalent to a human. TF-IDF treats them as completely different features. Word2vec helps a bit; transformers solve it.&lt;/li&gt;
  &lt;li&gt;Long-range context. “The hotel was lovely, except for the bedbugs and the manager who threatened me.” A bag-of-words model averages “lovely” and “threatened” and gets the answer roughly correct by accident. A transformer reads it as a sentence and weights the second clause appropriately.&lt;/li&gt;
  &lt;li&gt;Negation and irony. “Best customer service ever, if you enjoy waiting four hours and being lied to.” TF-IDF sees “best” + “customer service” + “ever” and predicts positive. The transformer sees the structure.&lt;/li&gt;
  &lt;li&gt;Low-resource targets. If you only have 50 labelled examples, the linear model is overfitting; an LLM with zero-shot prompting may genuinely do better.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb is: if the task can be solved by paying attention to the correct keywords, the boring baseline works. If it requires understanding sentence structure or context, you need a transformer.&lt;/p&gt;

&lt;h3 id=&quot;naive-bayes-the-even-more-boring-baseline&quot;&gt;Naive Bayes: the even more boring baseline&lt;/h3&gt;

&lt;p&gt;Naive Bayes is, in a real sense, more primitive than logistic regression. It assumes every feature is independent of every other feature given the class, a “naive” assumption that’s almost always false. And yet it often works fine, particularly for spam classification, document categorisation, and short-text problems.&lt;/p&gt;

&lt;p&gt;The reason is computational. Naive Bayes is &lt;em&gt;blazing fast&lt;/em&gt; to train, counting word occurrences per class, and equally fast at &lt;label for=&quot;sn-writing-the-boring-baseline-that-wins-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-boring-baseline-that-wins-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-boring-baseline-that-wins-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt;. For applications where you need to retrain frequently (incoming email streams, news feeds, anything with model drift) it’s hard to beat. Multinomial naive Bayes specifically remains the correct default for short text classification with limited data.&lt;/p&gt;

&lt;h3 id=&quot;clustering-k-means-and-the-friends-you-dont-think-about&quot;&gt;Clustering: k-means and the friends you don’t think about&lt;/h3&gt;

&lt;p&gt;Sometimes the task isn’t “classify this into one of N labels”, it’s “find natural groupings in this data.” That’s clustering, and the boring baseline is k-means.&lt;/p&gt;

&lt;p&gt;K-means takes a set of points (your TF-IDF vectors, your image embeddings, whatever) and a number &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt;, and finds &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt; clusters such that each point is closer to its own cluster’s centre than to any other. It’s the algorithm taught in the first week of a machine learning course, and it’s still the correct tool for most clustering problems.&lt;/p&gt;

&lt;p&gt;When you’d actually use it:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Customer segmentation based on behaviour vectors.&lt;/li&gt;
  &lt;li&gt;Document clustering for exploratory analysis (“what topics exist in this corpus?”).&lt;/li&gt;
  &lt;li&gt;Image quantisation, reducing a photograph to a palette of &lt;em&gt;k&lt;/em&gt; colours.&lt;/li&gt;
  &lt;li&gt;Vector quantisation for compression and indexing in vector databases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;K-means has limitations, it assumes spherical clusters, requires you to pick &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt;, and can get stuck in bad local minima, but for “I have a pile of vectors and I want to know what’s in there,” it’s still the first tool to reach for.&lt;/p&gt;

&lt;p&gt;For when k-means isn’t enough, there’s a small family of alternatives that are themselves still classical: DBSCAN for density-based clustering, hierarchical clustering when you want a dendrogram, Gaussian Mixture Models when you want soft assignments and uncertainty.&lt;/p&gt;

&lt;h3 id=&quot;topic-modelling-lda-and-nmf&quot;&gt;Topic modelling: LDA and NMF&lt;/h3&gt;

&lt;p&gt;A specific kind of unsupervised text analysis: what topics are present in this corpus, and which documents touch on which topics?&lt;/p&gt;

&lt;p&gt;The classical answer is Latent Dirichlet Allocation (LDA, Blei et al., 2003). LDA models each document as a mixture of topics, and each topic as a distribution over words. The result, when applied to a corpus of news articles, might give you topics that look like “sports basketball game team player,” “politics election vote senator democrat,” “weather storm rain temperature forecast.” Each document is described as some percentage of each topic.&lt;/p&gt;

&lt;p&gt;LDA is interpretable, deterministic-ish, and runs on modest hardware. It produces output a human can read (a topic is a list of weighted words) rather than a 768-dimensional vector. For exploratory analysis, journalism, and humanities research, it’s still extremely common.&lt;/p&gt;

&lt;p&gt;Non-negative Matrix Factorisation (NMF) does a similar thing through different mathematics and often produces sharper, more separable topics, worth trying alongside LDA when topic modelling is what you actually want.&lt;/p&gt;

&lt;p&gt;The neural alternatives, topic models built on top of contextual embeddings, like BERTopic, produce subtler topics but are harder to interpret and slower to run. If your goal is “give me a readable list of what’s in this corpus,” LDA is still hard to beat.&lt;/p&gt;

&lt;h3 id=&quot;a-starter-kit-in-code&quot;&gt;A starter kit, in code&lt;/h3&gt;

&lt;p&gt;Eighty per cent of the practical problems in this post can be solved with a combination of:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.feature_extraction.text&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TfidfVectorizer&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.linear_model&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LogisticRegression&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.naive_bayes&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MultinomialNB&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.cluster&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;KMeans&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.decomposition&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LatentDirichletAllocation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The total surface area is maybe 30 functions. The mental model is small. The deployment cost is whatever it costs to run a Python process on a CPU. You can train, deploy, and serve all of these from a single laptop, and you can scale them out to billions of documents on commodity hardware without surprise.&lt;/p&gt;

&lt;p&gt;That’s not nothing. That’s most of the practical value of machine learning, available without buying a GPU or calling an API.&lt;/p&gt;

&lt;h3 id=&quot;a-decision-table&quot;&gt;A decision table&lt;/h3&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;If your task is…&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;The boring baseline is…&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Reach for a transformer when…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Sentiment / topic / intent classification with thousands of labels&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;TF-IDF + logistic regression&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;You need to handle paraphrase, irony, or long context&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Spam / phishing / abuse detection&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Multinomial naive Bayes or logistic regression&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Adversaries are actively rewording to evade keywords&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Document categorisation across many classes&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;TF-IDF + linear SVM or logistic regression&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Class definitions are subtle and require context&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Customer segmentation&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;K-means on engineered features&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;You need clusters defined by complex relationships&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;What topics exist in this corpus?&quot;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;LDA or NMF&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;You need topics defined by semantic meaning rather than co-occurring words&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Initial baseline for any new ML problem&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;TF-IDF + logistic regression, even if you eventually replace it&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Always start here. Knowing how the boring baseline scores tells you whether the fancy model is worth the cost.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h3 id=&quot;why-teams-skip-this-step&quot;&gt;Why teams skip this step&lt;/h3&gt;

&lt;p&gt;Three usual reasons.&lt;/p&gt;

&lt;p&gt;First, the gradient of professional incentives points away from boring. Saying “I shipped a TF-IDF + logistic regression model” sounds like 2008. Saying “I fine-tuned a transformer” sounds like 2026. The actual customer doesn’t care.&lt;/p&gt;

&lt;p&gt;Second, the tooling for fancy models is now better than the tooling for boring ones. Hugging Face, Replicate, and the LLM APIs have made it easier to call a transformer than to set up a scikit-learn pipeline, particularly for someone new to the field. The friction has inverted.&lt;/p&gt;

&lt;p&gt;Third, “good enough” is hard to defend when the alternative is “best.” Nobody got fired for picking the SOTA model. If you pick the linear baseline and it’s 92% accurate, someone will eventually ask why you didn’t use the 94% transformer. The answer is “because it costs a thousand times more and is two percent better and we don’t need that two percent”, but that’s an explicit trade-off discussion most teams don’t want to have.&lt;/p&gt;

&lt;p&gt;The discipline that pays off is making the boring baseline the explicit comparison point. If you can’t beat the linear model by a meaningful margin, the linear model wins. If you can, you’ve justified the upgrade with a number.&lt;/p&gt;

&lt;p&gt;The discipline that pays off is making the boring baseline the explicit comparison point on every project. TF-IDF and logistic regression remain the right place to start a text-classification problem with thousands of labelled examples. Multinomial naive Bayes still beats most things for very short text at very high throughput. K-means is still the first thing to reach for when you want to know what groups exist in a pile of vectors, and LDA or NMF are still the tools to use when “give me a readable list of topics” is the actual brief. None of these is the consolation prize. They are the score the fancier model has to beat by a margin large enough to justify its cost.&lt;/p&gt;

&lt;p&gt;Most production ML in industry is still classical. The headlines belong to LLMs and the backend belongs to logistic regression. A 92% model that runs at fifty thousand predictions per second on a CPU usually beats a 94% model that costs a thousandth of a cent per call, once you multiply by the volume you’re actually serving. Always know the boring number before you commit to something fancier.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>A Gentle Guide to Typography: From Chisels to Character Sets</title>
    <link href="/writing/a-guide-to-typography/"/>
    <updated>2026-05-22T06:00:00+08:00</updated>
    <id>/writing/a-guide-to-typography/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt; — deep dives into the technology we use every day.&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;Before there were fonts, before there were printing presses, before there was even an alphabet, there were people who wanted to say things that would last longer than a breath.&lt;/p&gt;

&lt;p&gt;They scratched marks into wet clay. They carved shapes into stone. They painted on cave walls with ground-up ochre and spit; the &lt;a href=&quot;https://archive.org/details/lascauxmovements0000aujo&quot;&gt;pigments at Lascaux&lt;/a&gt; date to around 17,000 years ago. But that’s not the oldest mark-making by a long stretch. The First Nations peoples of Australia, the oldest continuous civilisation on Earth, were creating rock art tens of thousands of years earlier. Petroglyphs in the Pilbara region of Western Australia have been dated to at least 30,000 years ago, and charcoal drawings in Arnhem Land’s Nawarla Gabarnmang shelter push past 28,000 years (as documented in &lt;a href=&quot;https://press.anu.edu.au/publications/series/terra-australis/histories-australian-rock-art-research&quot;&gt;&lt;em&gt;Histories of Australian Rock Art Research&lt;/em&gt;&lt;/a&gt; and related studies). Some researchers argue the tradition extends back 65,000 years or more, to the earliest evidence of &lt;a href=&quot;https://www.nature.com/articles/nature22968&quot;&gt;human settlement on the continent&lt;/a&gt;. Writing, in its oldest form, was a physical act: you took a tool and you pushed it into something that would hold the mark after you walked away.&lt;/p&gt;

&lt;p&gt;This is where typography starts. Not with software. Not with design theory. With someone pressing a wedge into clay and thinking: &lt;em&gt;I want this to outlive me&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;from-hand-to-mould&quot;&gt;From hand to mould&lt;/h3&gt;

&lt;p&gt;For thousands of years, every copy of every written document was made by hand. Scribes (often monks in medieval Europe) would sit for hours copying text character by character onto parchment or vellum. Each copy was unique. Each was slightly different. The handwriting of the scribe was the “font”, though nobody called it that.&lt;/p&gt;

&lt;p&gt;Then, around 1440, &lt;a href=&quot;https://archive.org/details/johannesgutenber0000chil&quot;&gt;Johannes Gutenberg&lt;/a&gt; changed everything.&lt;/p&gt;

&lt;p&gt;Gutenberg didn’t invent printing. The Chinese had been doing block printing for centuries, and &lt;a href=&quot;https://archive.org/details/science-and-civilisation-in-china-volume-5-chemistry-and-chemical-technology-par_202109&quot;&gt;Bi Sheng&lt;/a&gt; had created movable type from baked clay as early as 1040 AD. What Gutenberg invented was &lt;em&gt;movable metal type&lt;/em&gt;: individual letters, each cast as a small block of a &lt;a href=&quot;https://ethw.org/Gutenberg_Devises_a_Lead-Tin-Antimony_Alloy&quot;&gt;lead-tin-antimony alloy&lt;/a&gt;, that could be arranged into words, locked into a frame, inked, and pressed onto paper. When you were done printing one page, you could break the letters apart and rearrange them into something else.&lt;/p&gt;

&lt;p&gt;This was revolutionary, and it introduced a bunch of concepts we still use today. So let’s walk through them, starting from the most fundamental.&lt;/p&gt;

&lt;h3 id=&quot;characters&quot;&gt;Characters&lt;/h3&gt;

&lt;p&gt;A character is the abstract idea of a letter, digit, or symbol. The letter “A” is a character. So is “7”. So is “?”. So is “é”. A character doesn’t have a specific shape; it’s the &lt;em&gt;concept&lt;/em&gt; of that symbol. When you think of the letter B, you’re thinking of a character: the second letter of the Latin alphabet, regardless of whether it’s tall and thin or short and round.&lt;/p&gt;

&lt;p&gt;This distinction matters because the same character can look wildly different depending on who’s drawing it. Your handwritten “g” looks nothing like the “g” on this screen, but they’re the same character. They carry the same meaning.&lt;/p&gt;

&lt;h3 id=&quot;glyphs&quot;&gt;Glyphs&lt;/h3&gt;

&lt;p&gt;A glyph is the specific visual shape that represents a character. If a character is the idea, a glyph is the drawing. The letter “a” is a character; the particular way it looks in this paragraph, its curves, its weight, its proportions, that’s a glyph.&lt;/p&gt;

&lt;p&gt;One character can have many glyphs. Think about “a” for a moment. There’s the version you’re probably reading now: a little arch sitting over a closed bowl, with a distinct two-part structure. Then there’s the simpler version, the one that looks like a circle with a stick, the kind most people write by hand. Typographers call the first one “double-storey” and the second “single-storey” (because the first has two enclosed spaces stacked up, like floors of a building). Both are glyphs of the same character.&lt;/p&gt;

&lt;p&gt;This goes further. An italic “a”, a bold “a”, a small-caps “A”: these are all different glyphs of the same character. Gutenberg understood this instinctively. His Bible used around 290 distinct glyphs, far more than the alphabet required, including variant letterforms and common ligatures, all designed to mimic the &lt;a href=&quot;https://finaltype.de/en/topics/gutenbergs-justification&quot;&gt;natural variation of handwriting&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;typefaces&quot;&gt;Typefaces&lt;/h3&gt;

&lt;p&gt;Now we’re getting to the term people most often mix up.&lt;/p&gt;

&lt;p&gt;A typeface is a designed set of glyphs that share a consistent visual style. When someone sits down and draws a complete alphabet (uppercase, lowercase, numbers, punctuation) in a unified style, they’ve created a typeface. Helvetica is a typeface. Garamond is a typeface. Times New Roman is a typeface.&lt;/p&gt;

&lt;p&gt;The word “typeface” comes directly from the physical world. In Gutenberg’s workshop, each metal letter block had a &lt;em&gt;face&lt;/em&gt;: the raised surface that got inked and pressed onto paper. A set of blocks sharing the same design was a set of type with the same face. A typeface.&lt;/p&gt;

&lt;p&gt;When people say “I love that font”, they usually mean the typeface: the overall design, the aesthetic, the personality. And that’s fine; language evolves. But if you want to be precise, the typeface is the design.&lt;/p&gt;

&lt;h3 id=&quot;fonts&quot;&gt;Fonts&lt;/h3&gt;

&lt;p&gt;So what’s a font then?&lt;/p&gt;

&lt;p&gt;In the metal-type era, a font was a specific size and style of a typeface. Garamond 12-point italic was one font. Garamond 14-point bold was a different font. They were literally different sets of physical metal blocks. You had to buy them separately and store them in different drawers.&lt;/p&gt;

&lt;p&gt;Those drawers, by the way, were called &lt;em&gt;cases&lt;/em&gt;. The capital letters were stored in the upper case (the harder-to-reach one, since capitals are used less often) and the small letters in the lower case, which is where we get the terms &lt;a href=&quot;https://archive.org/details/elementsoftypogr0000brin&quot;&gt;“uppercase” and “lowercase”&lt;/a&gt;. (Lovely, isn’t it?)&lt;/p&gt;

&lt;p&gt;In the digital world, the distinction has blurred. A font file today usually contains the full set of glyphs for one style of a typeface: Garamond Italic, say, or Garamond Bold. The typeface is the family; the font is the specific file or instance. But in everyday conversation, “font” and “typeface” are used interchangeably, and that’s okay.&lt;/p&gt;

&lt;h3 id=&quot;font-faces&quot;&gt;Font faces&lt;/h3&gt;

&lt;p&gt;Font face is a term that lives mostly in the world of CSS and web development. When you write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@font-face&lt;/code&gt; in a stylesheet, you’re telling the browser: here’s a font file, and here’s what I want you to call it. It’s the bridge between a font file sitting on a server and a name you can use in your design.&lt;/p&gt;

&lt;p&gt;In broader typographic conversation, “font face” and “typeface” mean roughly the same thing: the visual design of the letterforms.&lt;/p&gt;

&lt;h3 id=&quot;serifs-and-their-absence&quot;&gt;Serifs (and their absence)&lt;/h3&gt;

&lt;p&gt;Look at the letters in a book printed in Times New Roman. See those little feet and flicks at the ends of the strokes? Those are serifs.&lt;/p&gt;

&lt;p&gt;The word probably comes from the Dutch &lt;em&gt;schreef&lt;/em&gt;, meaning “stroke” or “line” (as discussed in De Vinne’s &lt;a href=&quot;https://archive.org/details/practiceoftypogr00devirich&quot;&gt;&lt;em&gt;The Practice of Typography&lt;/em&gt;&lt;/a&gt;). Serifs have been around since Roman times, literally. If you look at the inscriptions on Trajan’s Column in Rome (dedicated 113 AD), the letters have serifs. There’s a beautiful theory, advanced by Edward Catich in his 1968 study &lt;em&gt;The Origin of the Serif&lt;/em&gt;, that they originated not from the chisel but from the brush: before carving, Roman stonecutters painted the letterforms with a flat brush, and the natural flare of each brush stroke at the start and end of a line became the serif. The chisel then faithfully followed the painted guide. (Catich’s &lt;a href=&quot;https://shop.bl.ag/products/the-origin-of-the-serif&quot;&gt;&lt;em&gt;The Origin of the Serif&lt;/em&gt;&lt;/a&gt; demonstrated this by cutting letters with period-appropriate tools.)&lt;/p&gt;

&lt;p&gt;Typefaces with serifs (like Garamond, Baskerville, Georgia, and Times New Roman) are called serif typefaces. They feel classic, bookish, warm. Serifs also have a practical function: they help guide the eye along a line of text, creating a subtle visual rail. That’s why they’ve been the default for body text in printed books for centuries.&lt;/p&gt;

&lt;p&gt;Typefaces &lt;em&gt;without&lt;/em&gt; serifs (like Helvetica, Arial, Futura, and Gill Sans) are called sans-serif typefaces (“sans” is French for “without”). They tend to feel modern, clean, minimal. On screens, especially at small sizes, sans-serif typefaces have historically been easier to read because the fine details of serifs can get lost in low-resolution pixels. (High-resolution screens have closed that gap considerably.)&lt;/p&gt;

&lt;p&gt;There are other categories too. Slab serif typefaces (like Rockwell or Courier) have thick, blocky serifs: bold and industrial. Monospaced typefaces give every character the same width, which is why they’re used for code: everything lines up neatly. Script typefaces mimic handwriting. Display typefaces are designed for headlines and large sizes, where they can be dramatic without worrying about readability at 10 points.&lt;/p&gt;

&lt;h3 id=&quot;spacing-and-leading&quot;&gt;Spacing and leading&lt;/h3&gt;

&lt;p&gt;When Gutenberg assembled his type, the letters didn’t just touch each other. The metal blocks had built-in spacing: a little extra metal on each side of the letter face, so that when you lined them up, there was breathing room between characters.&lt;/p&gt;

&lt;p&gt;Spacing (or tracking in modern terminology) is the uniform distance between all characters in a block of text. Increase the tracking and the text feels airy, open, maybe a little aloof. Decrease it and things get tight, urgent, compressed. Good tracking is invisible; you don’t notice it, but you feel comfortable reading.&lt;/p&gt;

&lt;p&gt;Leading (pronounced “ledding”) is the vertical space between lines of text. The name comes from the actual strips of lead that typesetters placed between rows of metal type to push the lines apart (as described in Lupton’s &lt;a href=&quot;https://archive.org/details/thinkingwithtype0000lupt&quot;&gt;&lt;em&gt;Thinking with Type&lt;/em&gt;&lt;/a&gt;). More leading gives text room to breathe. Less leading packs it in. The correct amount depends on the typeface, the line length, and where the text is being read. Cramped leading is one of the quickest ways to make text feel hostile.&lt;/p&gt;

&lt;h3 id=&quot;kerning&quot;&gt;Kerning&lt;/h3&gt;

&lt;p&gt;Kerning is the adjustment of space between &lt;em&gt;specific pairs&lt;/em&gt; of characters. This is different from tracking, which affects all characters equally. Kerning is about individual relationships.&lt;/p&gt;

&lt;p&gt;Consider the letters “AV”. Because of their shapes (one leaning left, one leaning right) if you just space them evenly using each letter’s default width, there’ll be an awkward gap between them. It looks like “A V” instead of “AV”. Kerning tucks them closer together so they feel correct.&lt;/p&gt;

&lt;p&gt;Other classic kerning pairs: “To”, “We”, “Ty”, “VA”, “LT”. Any combination where the shapes of adjacent letters create an optical gap that needs closing.&lt;/p&gt;

&lt;p&gt;Good kerning is something you never notice. Bad kerning is something you can’t unsee. (There’s a whole internet subculture dedicated to finding poorly kerned signs. It’s called “keming”, because that’s what “kerning” looks like with bad kerning.)&lt;/p&gt;

&lt;h3 id=&quot;metrics-and-the-anatomy-of-letters&quot;&gt;Metrics and the anatomy of letters&lt;/h3&gt;

&lt;p&gt;Typographers have a precise vocabulary for the parts of a letter, and some of it is unexpectedly wonderful.&lt;/p&gt;

&lt;p&gt;Take the counter, the empty space inside a letter. The hole in “o”, the gap inside “e”, the little window in “a”. The empty space has a name! And it matters: counters are a huge part of what makes a typeface feel open or cramped.&lt;/p&gt;

&lt;p&gt;Then there’s the baseline (the invisible line letters sit on) and the x-height, which is just the height of a lowercase “x” (and by extension, most lowercase letters). Once you know about x-height, you start noticing it everywhere: a typeface with a tall x-height feels big and readable even at small sizes. Tall lowercase letters like “b” and “d” have ascenders that rise above the x-height. Letters like “p” and “g” have descenders that drop below the baseline, and the length of the descenders is one of those subtle things that gives a typeface its personality.&lt;/p&gt;

&lt;p&gt;The rest of the vocabulary is just as precise: the cap height is how tall capitals are, the bowl is the rounded part of letters like “b” and “d”, the stroke is any main line, and a terminal is where a stroke ends without a serif.&lt;/p&gt;

&lt;p&gt;The em is a unit of measurement that originally meant the width of the capital M, because M was typically the widest letter, and its width roughly equalled its height, making a nice square. Today, an em is simply equal to the current point size: in 16-point type, an em is 16 points. It’s used everywhere in typography and CSS. An en is half an em (roughly the width of a capital N) and is the unit behind the en-dash (–), which is half the width of an em-dash (—).&lt;/p&gt;

&lt;p&gt;But what &lt;em&gt;is&lt;/em&gt; a point? And how does it relate to the pixels on your screen?&lt;/p&gt;

&lt;p&gt;A point (pt) is the fundamental unit of typographic measurement. The concept dates back to Pierre Simon Fournier, who proposed a standardised point system in 1737, later refined by François-Ambroise Didot in the 1780s (documented in Carter’s &lt;a href=&quot;https://hyphenpress.co.uk/products/books/978-0-907259-21-3/&quot;&gt;&lt;em&gt;A View of Early Typography&lt;/em&gt;&lt;/a&gt;). In the modern PostScript standard (used by virtually all digital typography), one point is exactly &lt;a href=&quot;https://www.adobe.com/products/postscript.html&quot;&gt;1/72 of an inch&lt;/a&gt;. So 72-point type has letters about an inch tall. This wasn’t always the case; before digital standardisation, different countries used slightly different point sizes. The American point (established by the American Type Founders Association in 1886) was 0.01383 inches; the French Didot point was 0.01483 inches, about &lt;a href=&quot;https://archive.org/details/anatomyoftypefac0000laws&quot;&gt;7% larger&lt;/a&gt;, which made international typesetting exciting in all the wrong ways.&lt;/p&gt;

&lt;p&gt;A pica is 12 points, or 1/6 of an inch. Picas are used for measuring larger things: column widths, margins, page dimensions. If a designer says “set the body text in 10-point on a 20-pica column”, they mean 10-point type in a column about 3.3 inches wide. There’s even a European cousin called the cicero, which is 12 Didot points, almost the same size as a pica, but not quite. It’s mostly historical now.&lt;/p&gt;

&lt;p&gt;A pixel (px) is a single illuminated dot on your screen, and its physical size depends entirely on the display. On a 96-DPI (dots per inch) screen (the traditional Windows default) one pixel is 1/96 of an inch, so a CSS “point” (1/72 inch) works out to about 1.33 pixels. On a modern Retina display at 220 DPI, the same point might be 3 or more physical pixels.&lt;/p&gt;

&lt;p&gt;This is where it gets confusing. CSS defines &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1px&lt;/code&gt; as exactly &lt;a href=&quot;https://www.w3.org/TR/css-values-3/#absolute-lengths&quot;&gt;1/96 of an inch&lt;/a&gt;, but on high-DPI screens, a CSS pixel might map to 2 or 3 physical device pixels. Your phone’s “logical” resolution (the one websites see) is often half or a third of its actual hardware resolution. The operating system handles the scaling, which is why text looks sharp on a Retina display: there are simply more physical pixels per logical pixel, giving the rasteriser more dots to work with when drawing those Bézier curves (the mathematical curves that define each letter’s shape; more on these shortly).&lt;/p&gt;

&lt;p&gt;In practice: points for print, pixels for screens, ems for responsive design. An em in CSS is relative to the current font size, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;padding: 1em&lt;/code&gt; means “pad by the width of one M in whatever size we’re using”. This makes layouts scale naturally when the user changes their font size, which is why web designers love ems and their cousin, the rem (root em), which is relative to the root element’s font size rather than the current element’s.&lt;/p&gt;

&lt;h3 id=&quot;character-sets-and-encodings&quot;&gt;Character sets and encodings&lt;/h3&gt;

&lt;p&gt;Now we leave the world of ink and metal and enter the world of computers. And things get… complicated.&lt;/p&gt;

&lt;p&gt;When computers first needed to represent text, someone had to decide: which characters do we support, and how do we store them?&lt;/p&gt;

&lt;p&gt;ASCII (American Standard Code for Information Interchange), first published as ASA X3.4-1963 and revised several times through 1986 (as documented in Mackenzie’s &lt;a href=&quot;https://archive.org/details/mackenzie-coded-char-sets&quot;&gt;&lt;em&gt;Coded Character Sets&lt;/em&gt;&lt;/a&gt;), was one of the earliest answers. It used 7 bits to represent 128 characters: the English alphabet (upper and lower), digits 0-9, punctuation, and a handful of control characters (like “new line” and “tab”). It was simple, elegant, and completely inadequate for anyone who didn’t write in English.&lt;/p&gt;

&lt;p&gt;To make this tangible, here’s what the letter “R” looks like as actual bits in ASCII:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Character:  R
Decimal:    82
Hex:        52
Binary:     01010010
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Seven bits of information. That’s all it takes. The letter “A” is 01000001 (65), “B” is 01000010 (66), and so on. Uppercase and lowercase letters are exactly 32 apart (“a” is 01100001, 97) which means you can convert between them by flipping a single bit (bit 5, if you’re counting from zero). This wasn’t an accident; the designers of ASCII, led by Robert Bemer at IBM, were very clever about the layout (Bemer wrote about these &lt;a href=&quot;https://archive.org/details/ascii-bemer&quot;&gt;design decisions&lt;/a&gt; himself).&lt;/p&gt;

&lt;p&gt;A character set (or charset) is the complete collection of characters that a system recognises. ASCII’s character set has 128 members. That’s fine for English, but French needs accented characters, German needs ß and umlauts, Greek needs an entirely different alphabet, and that’s before we even get to Chinese, Japanese, Korean, Arabic, Hindi, or the hundreds of other writing systems used by actual humans.&lt;/p&gt;

&lt;p&gt;The 1980s and 90s saw a proliferation of extended character sets: ISO 8859-1 for Western European languages, ISO 8859-5 for Cyrillic, Shift JIS for Japanese, Big5 for Traditional Chinese. Each one carved out a different set of 256 (or more) characters. This sort of worked if everyone agreed on which character set they were using, but of course they often didn’t. The result was mojibake: garbled text where characters from one encoding were displayed using another’s mapping. You’ve seen it. Those weird sequences of Ã¤ and â€™ where accented letters and curly quotes should be? That’s mojibake.&lt;/p&gt;

&lt;h3 id=&quot;unicode-one-set-to-rule-them-all&quot;&gt;Unicode: one set to rule them all&lt;/h3&gt;

&lt;p&gt;Unicode was the attempt to fix this mess, and it’s one of the great technical achievements of the modern era, even if nobody outside of a relatively small group of people appreciates it.&lt;/p&gt;

&lt;p&gt;The idea was simple and ambitious: create a single character set that includes &lt;em&gt;every&lt;/em&gt; character from &lt;em&gt;every&lt;/em&gt; writing system, living or dead, plus mathematical symbols, emoji, musical notation, and anything else humans have ever wanted to write down.&lt;/p&gt;

&lt;p&gt;Each character in Unicode gets a unique number called a code point. These are written using a notation you’ll see everywhere: “U+” followed by a hexadecimal number. Hexadecimal (base 16) uses the digits 0-9 and the letters A-F, so each digit represents a value from 0 to 15. It’s used because it maps neatly onto bytes: two hex digits represent exactly one byte. The “U+” prefix just means “Unicode code point”.&lt;/p&gt;

&lt;p&gt;So when you see U+0041, that means Unicode code point number 65 (in decimal), which is the letter “A”. U+03B1 is code point 945, the Greek letter alpha (α). U+1F600 is code point 128512, the emoji 😀. The higher the number, the later the character was added to the standard (roughly speaking). The first 128 code points (U+0000 to U+007F) map directly to ASCII, which was a deliberate design choice that made adoption much easier.&lt;/p&gt;

&lt;p&gt;As of Unicode 16.0 (September 2024), the standard defines &lt;a href=&quot;https://www.unicode.org/versions/Unicode16.0.0/&quot;&gt;154,998 characters covering 168 scripts&lt;/a&gt;. Every one of them has a code point and an official name. U+0052 is LATIN CAPITAL LETTER R. U+2603 is SNOWMAN (☃). U+1F4A9 is PILE OF POO (💩). The naming is meticulous, sometimes whimsical, and always permanent: once a character is added, it’s never removed.&lt;/p&gt;

&lt;p&gt;But a code point is just a number. To actually store and transmit that number in a computer, you need an encoding: a scheme for turning code points into bytes.&lt;/p&gt;

&lt;p&gt;UTF-8 is the most common encoding on the web, used by &lt;a href=&quot;https://w3techs.com/technologies/details/en-utf8&quot;&gt;over 98% of all websites&lt;/a&gt; as of 2024, and the one you should almost always use. It was designed in September 1992 by Ken Thompson and Rob Pike, famously &lt;a href=&quot;https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt&quot;&gt;sketched out on a placemat&lt;/a&gt; in a New Jersey diner. It’s clever: ASCII characters (U+0000 to U+007F) are stored as a single byte, identical to their ASCII values, so all existing ASCII text is automatically valid UTF-8. Characters outside ASCII use 2, 3, or 4 bytes as needed. This makes it compact for English text and capable of representing any Unicode character.&lt;/p&gt;

&lt;p&gt;To see the difference, let’s look at how a few characters are stored as actual bytes across the different encodings. First, something simple, the letter “R” (U+0052):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Encoding   Bytes (hex)       Bytes (binary)
ASCII      52                01010010
UTF-8      52                01010010
UTF-16     00 52             00000000 01010010
UTF-32     00 00 00 52       00000000 00000000 00000000 01010010
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For basic Latin characters, UTF-8 and ASCII are identical: one byte. UTF-16 pads it to two bytes. UTF-32 pads it to four. You can see why UTF-32 is wasteful for English text: three of those four bytes are zeros, carrying no information.&lt;/p&gt;

&lt;p&gt;Now something outside ASCII, the pound sign “£” (U+00A3):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Encoding   Bytes (hex)       What&apos;s happening
ASCII      --                Can&apos;t represent it (not in the character set)
Latin-1    A3                One byte -- works, but only in this specific encoding
UTF-8      C2 A3             Two bytes (the C2 signals &quot;two-byte sequence&quot;)
UTF-16     00 A3             Two bytes
UTF-32     00 00 00 A3       Four bytes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And something further afield, the Japanese character “字” (U+5B57, meaning “character”, how fitting):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Encoding   Bytes (hex)       What&apos;s happening
ASCII      --                Can&apos;t represent it
Latin-1    --                Can&apos;t represent it
Shift JIS  8E 9A             Two bytes (Japanese-specific encoding)
UTF-8      E5 AD 97          Three bytes (the E5 signals &quot;three-byte sequence&quot;)
UTF-16     5B 57             Two bytes (falls within the Basic Multilingual Plane)
UTF-32     00 00 5B 57       Four bytes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And finally, an emoji, “😀” (U+1F600):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Encoding   Bytes (hex)          What&apos;s happening
ASCII      --                   Can&apos;t represent it
UTF-8      F0 9F 98 80          Four bytes (the F0 signals &quot;four-byte sequence&quot;)
UTF-16     D8 3D DE 00          Four bytes (a surrogate pair -- two 2-byte code units)
UTF-32     00 01 F6 00          Four bytes (same size as everything else in UTF-32)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice how UTF-8 scales: 1 byte for ASCII, 2 for European characters, 3 for most of the world’s living languages, and 4 for emoji and rarer scripts. The leading bits of each byte tell the decoder how many bytes to read. It’s an elegant piece of engineering.&lt;/p&gt;

&lt;p&gt;UTF-16 uses 2 bytes for characters in the Basic Multilingual Plane (the first 65,536 code points, which covers most living languages) and 4 bytes for everything else. Those 4-byte characters are encoded using pairs of 2-byte values called surrogate pairs: a clever hack that lets UTF-16 reach the full Unicode range while keeping the common case compact. UTF-16 is used internally by Windows, Java, and JavaScript. If you’ve ever been bitten by a JavaScript string reporting the wrong &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.length&lt;/code&gt; for an emoji, that’s because JavaScript counts UTF-16 code units, not characters, and your emoji needed a surrogate pair.&lt;/p&gt;

&lt;p&gt;UTF-32 (sometimes called UCS-4) takes the brute-force approach: 4 bytes for every single character, no exceptions. This makes it simple; the nth character is always at byte offset 4n, so random access is trivial. But it’s wasteful. An English text file in UTF-32 is four times the size of the same file in UTF-8, with three zero bytes for every one byte of actual data.&lt;/p&gt;

&lt;p&gt;There are also some historical encodings worth knowing about. UCS-2 was an early 2-byte encoding that predates UTF-16; it could only represent the first 65,536 code points and had no surrogate pair mechanism, so it couldn’t handle emoji or many CJK characters. It’s effectively obsolete, but you’ll occasionally encounter it in older systems. UTF-7 was designed for email systems that could only handle ASCII; it encoded Unicode characters using only ASCII-safe bytes. It was slow, complex, and is now deprecated for security reasons (it enabled some nasty injection attacks).&lt;/p&gt;

&lt;p&gt;The encoding is not the character set. Unicode is the character set (the list of characters and their code points). UTF-8, UTF-16, and UTF-32 are encodings (ways of turning those code points into bytes). This distinction trips people up constantly, but it matters. You might say “this file is Unicode” when you mean “this file is encoded in UTF-8”. Unicode tells you &lt;em&gt;which&lt;/em&gt; characters exist. The encoding tells you &lt;em&gt;how&lt;/em&gt; they’re stored as bytes.&lt;/p&gt;

&lt;h3 id=&quot;representations-how-letters-become-pixels&quot;&gt;Representations: how letters become pixels&lt;/h3&gt;

&lt;p&gt;So we have characters (abstract ideas), code points (numbers assigned to those ideas), encodings (ways to store those numbers), and typefaces (visual designs). The last piece of the puzzle is: how does a computer actually &lt;em&gt;draw&lt;/em&gt; a letter on screen?&lt;/p&gt;

&lt;p&gt;There are two main approaches.&lt;/p&gt;

&lt;p&gt;Bitmap fonts were the early method. Each glyph was stored as a grid of pixels: literally a tiny picture. This was fast to render but didn’t scale well. A bitmap font designed for 12-point looked terrible at 24-point because you were just scaling up the pixel grid, producing jagged edges.&lt;/p&gt;

&lt;p&gt;Outline fonts (also called vector fonts) solved this. Instead of storing a grid of pixels, they store the &lt;em&gt;shape&lt;/em&gt; of each glyph as a set of mathematical curves: typically Bézier curves, named after &lt;a href=&quot;https://en.wikipedia.org/wiki/Pierre_B%C3%A9zier&quot;&gt;Pierre Bézier&lt;/a&gt;, the French engineer at Renault who developed them in the 1960s for designing car bodies. (Paul de Casteljau at Citroën independently developed equivalent mathematics around the same time, but Renault &lt;a href=&quot;https://en.wikipedia.org/wiki/B%C3%A9zier_curve&quot;&gt;published first&lt;/a&gt;.) To display the letter, the computer calculates which pixels fall inside the outline and fills them in. This process is called rasterisation, and it’s why outline fonts scale beautifully to any size.&lt;/p&gt;

&lt;p&gt;The two dominant outline font formats are TrueType (developed by Apple and announced in 1991, partly to avoid Adobe’s licensing fees for PostScript Type 1 fonts (&lt;a href=&quot;https://developer.apple.com/fonts/TrueType-Reference-Manual/&quot;&gt;TrueType&lt;/a&gt; was Apple’s response), with files ending in .ttf) and OpenType (announced jointly by Microsoft and Adobe in &lt;a href=&quot;https://learn.microsoft.com/en-us/typography/opentype/spec/&quot;&gt;1996&lt;/a&gt;, with files ending in .otf or .ttf). OpenType is TrueType’s successor and adds support for advanced typographic features: ligatures, small caps, stylistic alternates, and more.&lt;/p&gt;

&lt;p&gt;Hinting is the process of adjusting how outlines are rasterised at small sizes on low-resolution screens. Without hinting, the mathematical curves of a glyph might fall between pixels, creating blurry or uneven strokes. Hints are instructions embedded in the font that snap the outlines to the pixel grid at small sizes, keeping text crisp. It’s painstaking work, and it’s one of the reasons well-hinted fonts (like the core Microsoft fonts) have historically looked so much better on screen than cheaper alternatives.&lt;/p&gt;

&lt;h3 id=&quot;ligatures-when-letters-merge&quot;&gt;Ligatures: when letters merge&lt;/h3&gt;

&lt;p&gt;A ligature is a single glyph made by combining two or more characters. The most common one in English is “fi”: in many serif typefaces, the dot of the “i” collides with the overhang of the “f”, so designers create a special glyph where the two letters are fused together. Other common ligatures: “fl”, “ff”, “ffi”, “ffl”.&lt;/p&gt;

&lt;p&gt;Ligatures started as a practical solution in metal type (it was easier to cast certain letter combinations as a single piece) and survived because they look good. OpenType fonts can contain dozens of ligatures, and modern software can substitute them automatically.&lt;/p&gt;

&lt;p&gt;Some typefaces take this further with glyphs that change shape depending on what’s next to them (font nerds call this contextual alternates). This is especially common in script typefaces, where a letter might have a different tail depending on the following letter, mimicking the natural flow of handwriting.&lt;/p&gt;

&lt;h3 id=&quot;how-long-is-a-piece-of-string-or-what-even-is-a-character&quot;&gt;How long is a piece of string (or: what even is a character?)&lt;/h3&gt;

&lt;p&gt;You’d think counting characters would be simple. You want to allow 600-character comments on your website. How hard can it be? You just… count the characters. Right?&lt;/p&gt;

&lt;p&gt;Welcome to one of the most quietly maddening problems in software engineering.&lt;/p&gt;

&lt;p&gt;Let’s start with something innocent: the letter “é”. Is that one character? It depends on who you ask. In Unicode, it can be represented two ways. There’s U+00E9, LATIN SMALL LETTER E WITH ACUTE: a single code point, unambiguously one thing. But there’s also the two-code-point sequence U+0065 (LATIN SMALL LETTER E) followed by U+0301 (COMBINING ACUTE ACCENT). These render identically. They mean the same thing. They’re defined as canonically equivalent by the Unicode standard. But one is one code point and the other is two.&lt;/p&gt;

&lt;p&gt;So when your user types “café” into your 600-character comment box, how many characters is that? If you count code points, it might be 4 or 5, depending on which representation of “é” their keyboard produced. If you count UTF-8 bytes, it’s 5 or 6. If you count UTF-16 code units (which is what JavaScript’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.length&lt;/code&gt; does), it’s yet another number.&lt;/p&gt;

&lt;p&gt;Now add emoji. The thumbs-up emoji 👍 is one code point: U+1F44D. But 👍🏽 (thumbs up with a medium skin tone) is &lt;em&gt;two&lt;/em&gt; code points: U+1F44D followed by U+1F3FD (a skin tone modifier). They render as a single visible symbol. The family emoji 👨‍👩‍👧‍👦 is seven code points stitched together with invisible joiners (U+200D, ZERO WIDTH JOINER): man + joiner + woman + joiner + girl + joiner + boy. One “character” on screen, seven code points, many more bytes.&lt;/p&gt;

&lt;p&gt;And flags! The flag emoji 🇬🇧 is two code points: U+1F1EC (REGIONAL INDICATOR SYMBOL LETTER G) followed by U+1F1E7 (REGIONAL INDICATOR SYMBOL LETTER B). The system pairs them up and displays a flag. What happens if you insert a character between them? Now you’ve got two orphaned regional indicators that render as ugly letter boxes. Is this one character? Two?&lt;/p&gt;

&lt;p&gt;The Unicode standard defines a concept called grapheme clusters: sequences of code points that together represent a single user-perceived character. This is probably what you mean when you say “character”, and it’s what a well-implemented character counter should count. But getting grapheme cluster segmentation correct requires implementing a nontrivial Unicode algorithm (UAX #29, &lt;a href=&quot;https://www.unicode.org/reports/tr29/&quot;&gt;“Unicode Text Segmentation”&lt;/a&gt;). Most programming languages don’t do this by default. Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len()&lt;/code&gt; counts code points. JavaScript’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.length&lt;/code&gt; counts UTF-16 code units. Neither counts what a human would call “characters”.&lt;/p&gt;

&lt;p&gt;So your 600-character limit? If you implement it by counting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.length&lt;/code&gt; in JavaScript, a user could type 300 emoji and hit your limit, because each emoji is two UTF-16 code units. Or they could paste in text with combining accents and get 600 “characters” that look like 400. Or they could use a single family emoji and consume 11 of their 600 “characters” on one symbol.&lt;/p&gt;

&lt;p&gt;The correct answer is to count grapheme clusters, validate on the server (since the client can always lie), and honestly, to be generous with your limits because this stuff is harder than it has any right to be.&lt;/p&gt;

&lt;p&gt;There’s an old joke among internationalisation engineers: “How many characters are in this string?” “It depends on what you mean by ‘character’.” It’s not really a joke; it’s more of a warning.&lt;/p&gt;

&lt;h3 id=&quot;when-letters-lie-homoglyphs-and-punycode&quot;&gt;When letters lie: homoglyphs and Punycode&lt;/h3&gt;

&lt;p&gt;Unicode’s ambition, including every character from every writing system, introduced a problem that no one at the printing press ever had to worry about: characters from different scripts that look identical.&lt;/p&gt;

&lt;p&gt;The Latin letter “a” (U+0061) and the Cyrillic letter “а” (U+0430) are visually indistinguishable in most typefaces. The same goes for Latin “o” and Cyrillic “о”, Latin “p” and Cyrillic “р”, Latin “e” and Cyrillic “е”. These are called homoglyphs: different characters that produce identical (or nearly identical) glyphs.&lt;/p&gt;

&lt;p&gt;This is a problem because domain names can contain non-ASCII characters. (The DNS post in this series covers the DNS side of this story.) The system that makes this work is called Internationalised Domain Names (IDN), and under the hood it uses an encoding called Punycode to convert Unicode domain names into ASCII-safe strings that DNS can handle. The domain “münchen.de” becomes “xn–mnchen-3ya.de” in Punycode. The “xn–” prefix tells the system it’s an encoded internationalised domain.&lt;/p&gt;

&lt;p&gt;The security implications are nasty. An attacker can register a domain like “аpple.com” where the first “а” is Cyrillic, not Latin. To the naked eye, this looks exactly like “apple.com”. The underlying Punycode is completely different (“xn–pple-43d.com”), but browsers display the pretty Unicode version. This is called an IDN homograph attack, first described by Evgeniy Gabrilovich and Alex Gontmakher in a &lt;a href=&quot;https://dl.acm.org/doi/10.1145/503124.503156&quot;&gt;2002 paper&lt;/a&gt;, and it has been used for real-world phishing.&lt;/p&gt;

&lt;p&gt;Browsers have defences. Most will display the Punycode version instead of the Unicode version if the domain mixes scripts suspiciously (if some characters are Latin and others are Cyrillic, for instance). Chrome, Firefox, and Safari each have slightly different rules for when to show the Punycode, and these rules have been refined over years of cat-and-mouse with attackers. But the fundamental problem remains: Unicode gives us more than 154,000 characters, many of which look alike, and any system that displays them needs to decide how much to trust what it’s showing you.&lt;/p&gt;

&lt;p&gt;It’s not just URLs. Homoglyphs can appear in code too. A variable name that &lt;em&gt;looks&lt;/em&gt; like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;password&lt;/code&gt; but uses a Cyrillic “а” is a different identifier entirely. Malicious pull requests have used this trick to sneak backdoors past code review. Some code editors now flag mixed-script identifiers, and Unicode itself defines a set of security mechanisms (documented in Unicode Technical Report #36, &lt;a href=&quot;https://www.unicode.org/reports/tr36/&quot;&gt;“Unicode Security Considerations”&lt;/a&gt;) for detecting confusable characters.&lt;/p&gt;

&lt;p&gt;Gutenberg’s compositor never had this problem. Every letter in his type case was unambiguous; you could pick it up and feel its shape. In the digital world, two characters can be byte-for-byte different but pixel-for-pixel identical. The typeface doesn’t lie; the character set does.&lt;/p&gt;

&lt;h3 id=&quot;what-happens-when-you-press-a-key&quot;&gt;What happens when you press a key&lt;/h3&gt;

&lt;p&gt;Let’s make all of this concrete. You’re sitting at your computer and you press the letter “R”. What actually happens, and how did we get here?&lt;/p&gt;

&lt;p&gt;In the scribe’s version (500 AD), a monk in a scriptorium dips a quill in iron gall ink. He looks at the exemplar (the book he’s copying from) and draws an R. His hand shapes the stroke, the bowl, the leg. The letter exists because his muscles moved in a practised pattern. The “input device” is his hand; the “rendering engine” is also his hand. The glyph is one-of-a-kind.&lt;/p&gt;

&lt;p&gt;In the printer’s version (1500 AD), a compositor stands at a type case. He reaches into the compartment labelled R, picks up a small metal block (reversed, so it’ll print the right way round) and slots it into the composing stick alongside the other letters. Later, the assembled type is locked into a frame, inked with a leather ball, and pressed onto dampened paper. The letter R is now reproducible. The same block can print the same R a thousand times. The “input” is the compositor’s hand selecting the correct piece of type; the “rendering” is the press.&lt;/p&gt;

&lt;p&gt;In the typist’s version (1900 AD), a typist sits at a typewriter and strikes the R key. A mechanical linkage swings a type bar upward. On the end of the bar is a small metal slug with a reversed R on its face. It hits an inked ribbon, which presses against paper, leaving the shape of the letter. One keystroke, one character, one glyph. The “encoding” is purely mechanical: each key is physically connected to exactly one letterform. (This is also where monospaced type became the norm: every character had to occupy the same width so the carriage could advance by a fixed amount after each keystroke.)&lt;/p&gt;

&lt;p&gt;In the early computer’s version (1980 AD), you press R on the keyboard of an IBM PC. The keyboard controller sends a scan code: a number identifying which physical key was pressed (not which character it represents; that comes later). The operating system’s keyboard driver translates the scan code into a character code. On this machine, that means ASCII: the letter R is stored as the number 82 (binary 01010010). The application receives this number, looks it up in a bitmap font (a grid of pixels for each character) and copies those pixels into video memory. The screen redraws. An R appears. The letter is now a number that becomes a picture.&lt;/p&gt;

&lt;p&gt;In the modern version (today), you press R. The keyboard sends a scan code (via USB or Bluetooth). The operating system’s input system translates it, through the keyboard layout (QWERTY? AZERTY? Dvorak?), into a Unicode code point: U+0052, LATIN CAPITAL LETTER R. This code point might be stored in memory as UTF-8 (the single byte 0x52, since R falls within ASCII’s range), or as UTF-16 (the two bytes 0x00 0x52), depending on the application.&lt;/p&gt;

&lt;p&gt;Now the text renderer takes over. It looks up the current font (say, a .otf OpenType file for the typeface Inter). Inside that file, it finds the glyph for U+0052: a set of Bézier curves describing the outline of the letter R in this particular design. The renderer checks the kerning table to see if R needs to be nudged closer to or further from the characters on either side. It checks for ligatures: does this R combine with the next character into a special glyph? (Probably not for R, but the system checks every time.) It applies hinting to snap the curves to the pixel grid at the current size. It rasterises the outline, filling in pixels that fall inside the curves, with subpixel rendering to smooth the edges: each pixel on your LCD is actually three tiny coloured stripes (red, green, blue), and the renderer exploits this to position edges with sub-pixel precision. The result is painted into the application’s window buffer, which is composited with other windows by the operating system and sent to the display.&lt;/p&gt;

&lt;p&gt;All of that, scan code to keyboard driver to Unicode code point to glyph lookup to Bézier curves to kerning adjustment to hinting to rasterisation to subpixel rendering to composited display, happens in &lt;em&gt;microseconds&lt;/em&gt;. You press R, and R appears. It feels instant because it is.&lt;/p&gt;

&lt;p&gt;Across the ages, the same act has run on very different machinery. A monk spent minutes per letter. A compositor spent seconds selecting type. A typist connected key to page in a single mechanical stroke. A modern computer does it in microseconds, but the pipeline is deeper: physical key → scan code → character code → Unicode code point → encoding → glyph lookup → outline scaling → kerning → hinting → rasterisation → pixel buffer → display.&lt;/p&gt;

&lt;p&gt;More steps than ever before. Each one invisible. Each one built on something a monk, a compositor, or a typist once did by hand.&lt;/p&gt;

&lt;p&gt;In the &lt;label for=&quot;sn-writing-a-guide-to-typography-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-a-guide-to-typography-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-a-guide-to-typography-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-a-guide-to-typography-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt;’s version (also today), you ask an AI to write a paragraph. Somewhere in a data centre, billions of numerical weights are multiplied together across dozens of layers of a neural network. The model predicts the most likely next &lt;label for=&quot;sn-writing-a-guide-to-typography-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-a-guide-to-typography-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;token&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-a-guide-to-typography-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-a-guide-to-typography-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;: not quite a character, not quite a word, but a chunk of text from a vocabulary of tens of thousands of pieces. It picks the token for “R”. This token is decoded back into the bytes of the UTF-8 character R (0x52), which is sent over HTTPS to your browser, where it enters the exact same rendering pipeline as before: Unicode code point → glyph lookup → Bézier curves → rasterisation → screen. The R appears. The entire history of typography, scribe, compositor, typist, keyboard, font renderer, is still there, running the last mile. The only difference is who asked for the letter. It used to be a human pressing a key. Now, sometimes, it’s a machine guessing what comes next. (Though arguably the monk was also guessing what came next. He was just copying more carefully.)&lt;/p&gt;

&lt;h3 id=&quot;now-print-it&quot;&gt;Now print it&lt;/h3&gt;

&lt;p&gt;Everything above gets a letter onto a screen. But what if you want it on paper? What if you hit Ctrl+P and expect a piece of dead tree to come out of a machine with your words on it?&lt;/p&gt;

&lt;p&gt;This is where things get properly unhinged. Because a printer is not a screen. A screen has pixels that glow. A printer has to physically deposit material onto a surface. And the chain of events between “the user clicked Print” and “ink is on paper” is one of the most gloriously over-engineered pipelines in all of computing.&lt;/p&gt;

&lt;p&gt;The problem is one of translation. Your computer knows what the document looks like; it’s been rendering it on screen just fine. But the printer is a separate device with its own processor, its own memory, and its own ideas about how to put dots on paper. Somehow, the computer has to describe the page in a way the printer can understand and reproduce.&lt;/p&gt;

&lt;p&gt;In the early days, this was brutally simple. Character printers (like daisy-wheel and dot-matrix printers) worked much like typewriters. The computer sent ASCII characters down a cable, and the printer had its own built-in font: literally a physical wheel with letter shapes on it, or a set of pin patterns for each character. You got whatever the printer gave you. Want a different typeface? Buy a different daisy wheel. Want graphics? Good luck.&lt;/p&gt;

&lt;p&gt;PostScript changed everything.&lt;/p&gt;

&lt;p&gt;In 1984, Adobe released &lt;a href=&quot;https://www.adobe.com/products/postscript.html&quot;&gt;PostScript&lt;/a&gt;: a full programming language designed for describing pages. Not characters. Not lines of text. &lt;em&gt;Pages&lt;/em&gt;. PostScript could describe any combination of text, graphics, and images as mathematical instructions. A PostScript file doesn’t say “print an R at position 40, 100”. It says “move to coordinates (40, 100), select the font Palatino-Roman at 12 points, scale the coordinate system, define a path using these Bézier curves, and fill it”. Sound familiar? Those are the same Bézier curves we met in outline fonts. This wasn’t a coincidence; Adobe co-founder John Warnock developed PostScript and the Type 1 font format together (as Warnock recounted in his &lt;a href=&quot;https://www.computerhistory.org/collections/catalog/102738759&quot;&gt;Computer History Museum oral history&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;PostScript was revolutionary because it made the printer resolution-independent. The same PostScript file could print on a 300-DPI office laser printer and a 2,400-DPI professional typesetter, and each device would rasterise the curves at its native resolution. The page description was abstract; the physical rendering was the printer’s job.&lt;/p&gt;

&lt;p&gt;The downside? PostScript is a Turing-complete programming language. Your printer is literally &lt;em&gt;executing code&lt;/em&gt; to figure out what to print. Early PostScript printers needed powerful processors and lots of RAM; they were computers in their own right, and often more expensive than the computer sending them data. Some complex pages could take minutes to process. Occasionally, a malformed PostScript file could crash the printer or send it into an infinite loop, because that’s what happens when your printer runs arbitrary programs.&lt;/p&gt;

&lt;p&gt;PDF (Portable Document Format), also from Adobe, is PostScript’s better-behaved descendant. It dropped the full programming language in favour of a more structured, predictable format, while keeping the same fundamental model: vector graphics, Bézier curves, embedded fonts, resolution independence. When you “print to PDF” today, your computer is generating a page description in this format.&lt;/p&gt;

&lt;p&gt;Printer drivers are the translators that sit between your operating system and your specific printer. When you click Print, here’s what actually happens:&lt;/p&gt;

&lt;p&gt;The application hands the document to the operating system’s printing subsystem. On Windows, this is the GDI (Graphics Device Interface) or the newer XPS (XML Paper Specification) pipeline. On macOS, it’s Quartz, which internally uses PDF as its native page description format; every Mac has been thinking in PDF since OS X launched in &lt;a href=&quot;https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_pdf/dq_pdf.html&quot;&gt;2001&lt;/a&gt;. On Linux, it’s typically CUPS (Common Unix Printing System, originally created by Michael Sweet at Easy Software Products in 1997 and later &lt;a href=&quot;https://en.wikipedia.org/wiki/CUPS&quot;&gt;acquired by Apple in 2007&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The printing subsystem renders the page into an intermediate format: a spool file. The printer driver then translates this spool file into whatever language the specific printer speaks. This might be PostScript, or it might be PCL (Printer Command Language, HP’s long-running alternative), or for many modern consumer printers it might be a proprietary raster format where the computer does all the rendering and just sends the printer a bitmap of dots to lay down.&lt;/p&gt;

&lt;p&gt;Now for the physics.&lt;/p&gt;

&lt;p&gt;A laser printer works by exploiting static electricity and heat: a process called electrophotography, invented by Chester Carlson in 1938 and first &lt;a href=&quot;https://archive.org/details/copiesinsecondsh0000owen&quot;&gt;commercialised by Xerox&lt;/a&gt; in 1959. The first laser printer was the &lt;a href=&quot;https://en.wikipedia.org/wiki/Xerox_9700&quot;&gt;Xerox 9700&lt;/a&gt;, released in 1977. A photosensitive drum, a cylinder coated in a material (typically organic photoconductor, or OPC) that conducts electricity when exposed to light, sits at the heart of the machine. Here’s the sequence:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The drum is given a uniform negative electrical charge by a corona wire or charge roller.&lt;/li&gt;
  &lt;li&gt;A laser (or an array of LEDs) scans across the drum, switching on and off thousands of times per second. Where the light hits, the charge dissipates. The laser is drawing the page as a pattern of charged and uncharged areas on the drum’s surface, one row of dots at a time, as the drum rotates.&lt;/li&gt;
  &lt;li&gt;The drum passes a reservoir of toner: a fine powder of plastic particles mixed with pigment. Toner is attracted to the uncharged areas (where the laser hit) and repelled from the charged areas. The powder sticks to the drum in exactly the pattern of your text and images.&lt;/li&gt;
  &lt;li&gt;A sheet of paper is fed past the drum. The paper has been given a positive charge, which is stronger than the drum’s remaining negative charge, so the toner transfers from drum to paper.&lt;/li&gt;
  &lt;li&gt;The paper passes through a fuser: a pair of heated rollers at around &lt;a href=&quot;https://archive.org/details/physicstechnolog0000will&quot;&gt;150-200°C&lt;/a&gt; (300-390°F). The heat melts the plastic in the toner, bonding it permanently to the paper fibres.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it. Your letter R is now toner (melted plastic) fused into paper. The Bézier curves that started as mathematical abstractions in a font file have become a physical pattern of charged and uncharged spots on a rotating drum, which attracted specks of plastic dust, which were melted onto a sheet of ground-up wood pulp. It’s &lt;em&gt;absurd&lt;/em&gt; when you think about it.&lt;/p&gt;

&lt;p&gt;An inkjet printer takes a different approach but is no less wild. Tiny nozzles, sometimes thousands of them, each thinner than a human hair, fire microscopic droplets of liquid ink onto paper. There are two main technologies: thermal inkjet (used by HP and Canon), in which a tiny resistor heats the ink to around 300°C in microseconds, forming a &lt;a href=&quot;https://en.wikipedia.org/wiki/Inkjet_printing&quot;&gt;steam bubble that ejects a droplet&lt;/a&gt;, and piezoelectric inkjet (used by Epson), which uses a piezoelectric crystal that physically deforms when electricity is applied, squeezing the ink out &lt;a href=&quot;https://corporate.epson/en/technology/overview/printer-inkjet/micro-piezo.html&quot;&gt;mechanically&lt;/a&gt;. Each droplet is about 1-5 picolitres (a picolitre is a &lt;em&gt;trillionth&lt;/em&gt; of a litre, &lt;a href=&quot;https://www.dl.begellhouse.com/journals/6a7c7e10642258cc,45424ffc0b99306e,6cb9451f43303efe.html&quot;&gt;Castrejon-Pita et al., 2013&lt;/a&gt;). The precision required is staggering: the nozzles must fire at exactly &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;the correct microsecond&lt;/a&gt; as the print head sweeps across the page, placing dots at up to 5,760 DPI.&lt;/p&gt;

&lt;p&gt;For colour printing, things multiply. A colour laser printer has four separate drums and four toner cartridges (cyan, magenta, yellow, and black, or CMYK). Each colour is laid down in a separate pass, with the four layers combining to produce the full colour spectrum. Getting the four colours to align perfectly (registration) is one of the hardest mechanical challenges, and even tiny misalignment shows up as colour fringing on text. A colour inkjet fires four (or more, with some having six or eight) colours from separate nozzle arrays in a single pass.&lt;/p&gt;

&lt;p&gt;And then there’s the font question. Does the printer use the same fonts as the computer? Sometimes yes, sometimes no. PostScript printers traditionally had a set of built-in fonts (the “PostScript 35”, including Helvetica, Times, Courier, and others) stored in the printer’s own ROM. If your document used one of these, the computer just sent the font name and the printer rendered it locally. If your document used a font the printer didn’t have, the driver had to either embed the font data in the print job (increasing its size) or substitute a similar built-in font (changing how your document looked).&lt;/p&gt;

&lt;p&gt;Modern printers mostly receive pre-rasterised data; the computer does the heavy lifting and sends the printer a bitmap. This avoids font substitution problems entirely but means the computer is doing more work and the print data is larger. It’s the same trade-off as always: do you send instructions or pixels? PostScript said instructions. Modern consumer printing says pixels. The professional print industry still says instructions (PDF), because when you’re printing a million copies of a magazine, you need the precision.&lt;/p&gt;

&lt;p&gt;The full pipeline, from keypress to paper: You type an R. It becomes a Unicode code point (U+0052). The application looks up the glyph in the font. It renders the page layout: text, kerning, leading, line breaks, all of it. You hit Print. The OS printing subsystem takes the rendered page and converts it to a page description (PostScript, PDF, XPS, or a raw bitmap). The printer driver translates this for your specific printer. The data travels over USB, Wi-Fi, or Ethernet to the printer. The printer’s controller processes the data and drives the marking engine: laser and drum, or inkjet nozzles. Toner is melted or ink is squirted. The paper emerges.&lt;/p&gt;

&lt;p&gt;From an abstract idea in Unicode, through mathematical curves, through a page description language, through a driver, through a cable, through trapped lightning in a thinking chip, to a laser drawing on a charged drum, to plastic powder melted by heat onto pressed wood fibre. Gutenberg would be absolutely baffled. But he’d recognise the letter.&lt;/p&gt;

&lt;h3 id=&quot;why-this-matters&quot;&gt;Why this matters&lt;/h3&gt;

&lt;p&gt;Typography sits at the intersection of art, engineering, and language. It’s been refined over nearly 600 years of printing and thousands of years of writing before that. Every choice, serif or sans-serif, tight tracking or loose, generous leading or cramped, changes how text feels, and therefore changes how ideas land.&lt;/p&gt;

&lt;p&gt;When typography is good, it’s invisible. The words just flow. You’re not thinking about letterforms or kerning or encodings; you’re thinking about what the text &lt;em&gt;says&lt;/em&gt;. That invisibility is hard-won. Behind every comfortable paragraph is centuries of craft: stonecutters and scribes, punchcutters and typesetters, designers and engineers, all working towards the same goal.&lt;/p&gt;

&lt;p&gt;Making the words feel effortless.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Clock Inside You</title>
    <link href="/writing/the-clock-inside-you/"/>
    <updated>2026-05-21T06:00:00+08:00</updated>
    <id>/writing/the-clock-inside-you/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The previous posts in this series covered &lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;the human history of the hour&lt;/a&gt;, &lt;a href=&quot;/writing/what-day-is-it/&quot;&gt;the calendar&lt;/a&gt;, &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;the physics of the second&lt;/a&gt;, &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;how relativity bends it&lt;/a&gt;, &lt;a href=&quot;/writing/does-time-even-exist/&quot;&gt;whether it exists at all&lt;/a&gt;, and &lt;a href=&quot;/writing/can-you-turn-back-time/&quot;&gt;whether you can go backwards&lt;/a&gt;. All of that was about time out there, in clocks, in spacetime, in the equations. This post is about the clock you can’t put down: the one inside you.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;your-body-doesnt-run-on-utc&quot;&gt;Your body doesn’t run on UTC&lt;/h3&gt;

&lt;p&gt;You can’t switch off the body clock. You can ignore it. You can override it with coffee, bright screens, shift work, or a midnight flight to Frankfurt. The body clock does not care. It keeps running at roughly its own pace, drifts slightly out of sync with the sun, and resets itself, not from your watch, but from the light hitting your retina.&lt;/p&gt;

&lt;p&gt;This is the biological machinery that runs every living thing on Earth. Mice have it. Fruit flies have it. So do cyanobacteria, which were doing circadian biology a billion years before anyone invented a sundial. Whatever timekeeping humans eventually engineered with caesium atoms and geoid corrections, evolution got there first. It just picked a different substrate.&lt;/p&gt;

&lt;h3 id=&quot;the-suprachiasmatic-nucleus&quot;&gt;The suprachiasmatic nucleus&lt;/h3&gt;

&lt;p&gt;Somewhere behind your eyes, just above where the optic nerves from your left and right retinas cross, sits a pair of tiny nuclei about the size of a grain of rice. That’s the suprachiasmatic nucleus, the SCN, and it contains roughly 20,000 neurons. It is the master clock of your body.&lt;/p&gt;

&lt;p&gt;The SCN is astonishing. Isolate it from the rest of the brain, keep its neurons alive in a dish with the correct nutrients, and it keeps oscillating, individual cells tick in rough synchrony, with a period close to 24 hours, for weeks. No external input. No light cues. Just a biochemical feedback loop inside each cell, coupled loosely to its neighbours, running on its own rhythm.&lt;/p&gt;

&lt;p&gt;The period is not exactly 24 hours. Czeisler et al. demonstrated the near-24-hour intrinsic period in a landmark 1999 study in &lt;em&gt;Science&lt;/em&gt;, putting volunteers in carefully controlled environments with no time cues and measuring their internal rhythms. The average was about 24.18 hours, slightly longer than a solar day. Almost all healthy humans cluster close to that value. A small number run shorter.&lt;/p&gt;

&lt;p&gt;That slight mismatch matters. Because your intrinsic period isn’t 24 hours, the clock would drift out of phase with the planet if it ran uncorrected. A small daily adjustment keeps it aligned. The adjustment comes from light.&lt;/p&gt;

&lt;h3 id=&quot;how-light-resets-you&quot;&gt;How light resets you&lt;/h3&gt;

&lt;p&gt;The SCN gets its light signal from a special class of cells in the retina called intrinsically photosensitive retinal ganglion cells (ipRGCs). These aren’t the rods and cones you see with. They’re a separate system, tuned to detect overall light level, particularly the blue end of the visible spectrum, and report it to the SCN via a dedicated neural pathway called the retinohypothalamic tract.&lt;/p&gt;

&lt;p&gt;When those cells fire, the SCN adjusts its internal clock. Bright light in the morning nudges the clock earlier. Bright light in the evening nudges it later. The direction depends on when in your current cycle the light lands.&lt;/p&gt;

&lt;p&gt;The practical consequences are everywhere. If you’re staring at a screen at midnight, your ipRGCs are reporting “high blue light” to the SCN, which reads that as an argument for “still daytime,” which delays the clock. The clock drifts later. You go to bed later. The next morning you have to drag yourself up before the clock says it’s morning. Repeat this for a working week and you have given yourself a mild, self-imposed form of jet lag without leaving the house.&lt;/p&gt;

&lt;h3 id=&quot;jet-lag&quot;&gt;Jet lag&lt;/h3&gt;

&lt;p&gt;Jet lag is what happens when you cross time zones faster than your body can adjust. The SCN resets at roughly one hour per day, so a five-hour time change takes roughly five days to shake off. A ten-hour change takes about ten. That’s why you can feel perfectly fine by day three of a short hop, and still brain-fogged by day seven of a long one.&lt;/p&gt;

&lt;p&gt;Eastward travel is generally worse than westward. This is because your intrinsic period is slightly &lt;em&gt;longer&lt;/em&gt; than 24 hours, and shortening the day is harder than extending it. Flying east forces your clock to advance, to squeeze 24 hours of biology into, say, 20 hours of wall time. Flying west lets your clock simply extend, which it already wants to do. Living in Perth, I feel this every time I fly to Europe. The outbound is westward, and my clock gets to drift out to match the longer day; I feel human again by the third morning. The return is the brutal one, eight or nine time zones east, staring at the ceiling at 2 AM local time for the better part of a week.&lt;/p&gt;

&lt;p&gt;The fix is light, mostly. Morning sunlight at the destination, avoiding bright light in the destination’s evening, and, if you’re feeling technical, using light carefully before you fly to pre-adapt. Melatonin at the destination’s bedtime can help, but the headline intervention is exposure to the right light at the right time. The eyes know.&lt;/p&gt;

&lt;h3 id=&quot;shift-work-and-the-iarc&quot;&gt;Shift work and the IARC&lt;/h3&gt;

&lt;p&gt;Chronic circadian disruption is an occupational hazard for long-haul flight crews, night-shift workers, and anyone whose work schedule repeatedly drags them across their own body clock’s boundaries. Studies have linked it to increased rates of cardiovascular disease, metabolic disorders, and several cancers.&lt;/p&gt;

&lt;p&gt;In 2007, the International Agency for Research on Cancer (IARC) classified shift work involving circadian disruption as Group 2A: probably carcinogenic to humans. That’s the same classification as red meat, and one step below “known carcinogen.” The evidence isn’t airtight, but it’s strong enough that IARC was willing to put it in writing. The body’s clock is not a metaphor. It’s a biological mechanism, and forcing it out of sync repeatedly has measurable health consequences.&lt;/p&gt;

&lt;p&gt;We built a civilisation on the assumption that humans can work any hours, as long as someone is willing to pay for them. The biology quietly disagrees. A nurse working rotating night shifts isn’t just tired in a local, sleep-deficit sense, they’re operating a system that evolved for a world where you did most of what you did during daylight. Ignoring the clock has a bill, and the bill is paid in health outcomes decades down the line.&lt;/p&gt;

&lt;h3 id=&quot;sleep-pressure-and-adenosine&quot;&gt;Sleep pressure and adenosine&lt;/h3&gt;

&lt;p&gt;There is a second clock in your body that interacts with the first, and it’s worth keeping them straight.&lt;/p&gt;

&lt;p&gt;The circadian clock, the SCN, tells you what time of day it is. It doesn’t care how long you’ve been awake. Even if you stay up all night, your SCN will still say “morning” when morning comes.&lt;/p&gt;

&lt;p&gt;The sleep drive, often called sleep pressure, tells you how long you’ve been awake. It builds up the longer you’re conscious, and drops while you sleep. The chemistry behind it is largely a molecule called adenosine, which accumulates in the brain during wakefulness as a byproduct of neural activity. High adenosine means high sleep pressure: your head gets heavy, focus goes, and the couch starts looking like a strategic asset.&lt;/p&gt;

&lt;p&gt;Caffeine is an adenosine receptor antagonist. It doesn’t remove the adenosine, it just blocks the receptors that let your brain notice how much has built up. The pressure is still there. You’re borrowing alertness against it. When the caffeine wears off, the full accumulated adenosine load lands on the receptors at once, which is part of why the crash can be steeper than the coffee was worth.&lt;/p&gt;

&lt;p&gt;The two systems normally cooperate. Circadian drive pushes you to be alert during biological daytime. Sleep drive pushes you toward sleep when you’ve been awake too long. Night-shift workers are fighting both at once: their sleep drive is high because they’ve been up all night, and their circadian drive is high because their body thinks it’s morning. That combination is brutal, and it’s one of the reasons shift work is so hard on the body.&lt;/p&gt;

&lt;h3 id=&quot;larks-owls-and-the-chronotype-spectrum&quot;&gt;Larks, owls, and the chronotype spectrum&lt;/h3&gt;

&lt;p&gt;Not everyone’s SCN runs at the same phase. Some people’s clocks run earlier than the population average; others run later. The technical term is chronotype, and it’s real.&lt;/p&gt;

&lt;p&gt;Larks, morning types, feel sharpest in the early hours and fade in the evening. Their SCN is phase-advanced relative to the average. Extreme larks are up at 5 AM with the birds and exhausted by 9 PM.&lt;/p&gt;

&lt;p&gt;Owls, evening types, peak late and struggle with early starts. Their SCN is phase-delayed. Extreme owls are at their best after midnight and miserable before 10 AM.&lt;/p&gt;

&lt;p&gt;Most people sit somewhere on a continuum between the two. Chronotype is partly genetic, several genes, including &lt;em&gt;PER3&lt;/em&gt;, have been implicated, and partly age-related. Teenagers are statistically more owl-like; older adults drift lark-ward. The stereotype of the teenager who can’t be roused before 10 AM isn’t laziness. Their circadian phase is genuinely shifted later during adolescence, for developmental reasons that aren’t fully understood.&lt;/p&gt;

&lt;p&gt;The trouble is that society is calibrated for average-to-lark chronotypes. School starts at 8 AM. Offices open at 9. An extreme owl trying to hold down a 9-to-5 job is being asked, every working day, to be awake and productive at a time their body is biologically still asleep. The polite term is social jet lag. The practical effect is that owls are chronically mildly sleep-deprived for their entire working lives, and it shows up in the health data.&lt;/p&gt;

&lt;h3 id=&quot;the-body-clock-and-ageing&quot;&gt;The body clock and ageing&lt;/h3&gt;

&lt;p&gt;Circadian rhythm weakens with age. The SCN’s output becomes less reliable; the light-sensitive cells in the retina decline; older adults often report waking earlier, sleeping less deeply, and feeling “off” if their schedule shifts. The internal clock is still there, but the signal it sends the rest of the body is quieter.&lt;/p&gt;

&lt;p&gt;There’s a feedback with cognition. Sleep disruption in older adults is associated with memory problems, and some researchers suspect that weakening circadian control contributes to neurodegenerative conditions, not as a sole cause, but as one of the stressors that piles up over decades. The relationship runs both ways: Alzheimer’s disease, for instance, damages the SCN directly, which further disrupts sleep, which in turn makes cognitive symptoms worse.&lt;/p&gt;

&lt;p&gt;The practical implications are straightforward, even if they’re hard to put into practice. Bright light in the morning. Dim light in the evening. Consistent sleep and wake times. Outdoor time in actual daylight, which is orders of magnitude brighter than any indoor lighting and gives the SCN a much stronger signal to lock onto. None of this is glamorous. All of it works.&lt;/p&gt;

&lt;h3 id=&quot;the-clock-that-doesnt-care-about-you&quot;&gt;The clock that doesn’t care about you&lt;/h3&gt;

&lt;p&gt;The other clocks in this series are human inventions. Sundials, mechanical escapements, caesium fountains, GPS constellations, all of them are things we built, using machinery we understand, to answer questions we formulated. The body clock was here first. It runs on biochemistry you didn’t choose, it resets itself from signals you can’t see directly, and it has opinions about when you should be awake whether you consult it or not.&lt;/p&gt;

&lt;p&gt;You can fight it. Plenty of people do. But the fight has a cost, and the cost compounds. If the physics of time is impressive, the biology is humbling. The most accurate atomic clock in the world has been running for a few decades. The machinery in your suprachiasmatic nucleus has been keeping time, in one organism or another, for a billion years. It is not going to lose an argument with your calendar.&lt;/p&gt;

&lt;p&gt;There’s one more clock to look at, and it’s the trickiest of the lot: the one in your head that tells you how &lt;em&gt;long&lt;/em&gt; something felt. It has nothing to do with the SCN. It runs on attention, memory, and dopamine. And it’s wrong almost all the time.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/writing/why-does-thursday-last-forever/&quot;&gt;Why Does Thursday Last Forever?&lt;/a&gt; is next, the neuroscience of why time drags, vanishes, and accelerates as you age.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Choosing Between Chains, Retrieval, and Agents for a GenAI Assistant</title>
    <link href="/writing/choosing-between-chains-retrieval-and-agents-for-a-genai-assistant/"/>
    <updated>2026-05-20T06:00:00+08:00</updated>
    <id>/writing/choosing-between-chains-retrieval-and-agents-for-a-genai-assistant/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;AI Practitioner&lt;/strong&gt; · AIF-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;An internal-operations team has commissioned a GenAI assistant. The requirements, ordered by when they landed:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;v1: Policy Q&amp;amp;A. Engineers ask “what’s our data-retention policy for customer chat logs?”; the assistant answers from the internal policy wiki. One-shot question-answering, grounded in documents.&lt;/li&gt;
  &lt;li&gt;v2: Customer record lookup. Support agents ask “what subscription tier is customer ID 4711 on, and when did they last log in?”; the assistant calls an internal API and returns the answer in natural language. The data isn’t in any document; it’s in a database.&lt;/li&gt;
  &lt;li&gt;v3: Email drafting. After looking up a customer, draft a personalised apology-plus-next-steps email for the agent to review. Combines retrieved facts with generated text.&lt;/li&gt;
  &lt;li&gt;v4: Ticket filing. “Please file a P2 Jira ticket against team payments describing the issue above, with the customer context attached.” The assistant takes an action in an external system based on what was just discussed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The team has a Bedrock account, access to Claude Sonnet and Nova Pro, an internal REST API for the customer-record lookup, a Jira API, and the policy wiki mirrored to an S3 bucket. What’s unclear is the architecture. Somebody has proposed one big “agent” that handles all four versions uniformly. Somebody else has proposed four separate endpoints, each built on the simplest pattern that works for its job. The team want a recommendation.&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;These three patterns aren’t rivals for the same problem. They’re answers to different &lt;em&gt;shapes&lt;/em&gt; of problem. Picking right is mostly about matching the pattern to the shape of the workload. Policy Q&amp;amp;A is a retrieval shape, knowledge in documents, no actions. A multi-step “do whatever’s needed” flow is an agent shape. Everything in between is some flavour of chain. Trying to solve a retrieval problem with an agent is over-engineering; trying to solve “do whatever’s needed” with a fixed chain is under-engineering.&lt;/p&gt;

&lt;p&gt;The patterns sit on a ladder of elaboration. Each rung up buys capability, external data, actions, planning, and pays for it in three currencies: latency, cost, and predictability. The first two scale with the number of model invocations per request. The third is what bites in production: a chain always runs steps 1, 2, 3, in that order; an &lt;label for=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-ai-agent&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-ai-agent-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;agent&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-ai-agent&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-ai-agent-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Agent&lt;/span&gt;A system that wraps an LLM with tools, memory, and a loop, so it can take multi-step actions toward a goal rather than just answering one prompt.
&lt;/span&gt; might call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lookup_customer&lt;/code&gt; once on Monday and three times on Tuesday, depending on how the &lt;label for=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; planned. For any production-facing behaviour, that determinism is a feature, if an auditor asks “what did the assistant do when user X asked Y?”, a chain’s answer is the code; an agent’s is the run trace.&lt;/p&gt;

&lt;p&gt;&lt;label for=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-tool-use&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-tool-use-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Tool use&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-tool-use&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-tool-use-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Tool use&lt;/span&gt;Letting an LLM call structured functions you’ve defined – search, calculator, database query, API call – instead of trying to do everything in text.
&lt;/span&gt; is the mechanism that connects models to systems, describe a function, the model decides when to call it, the application executes, the result returns. It’s a &lt;em&gt;model&lt;/em&gt; feature, not an agent-only one. A chain whose steps can call tools is still a chain (the topology is fixed by code); a model handed tools and told to “figure it out” is an agent (the topology is chosen by the model). That distinction is the architectural call, not “do we use tools?” but “do we let the model choose the path?”&lt;/p&gt;

&lt;p&gt;The default error mode, when an agent platform is on the table, is &lt;em&gt;over-elaboration&lt;/em&gt;: “we have an agent runtime, so everything becomes an agent call.” The better discipline is to pick the simplest pattern that supports each piece of the workload, and reach for agency only when the path genuinely needs to be chosen by the model rather than written down by the engineer.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Six filters, applied to each of the three patterns.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Determinism of topology, does the same input produce the same sequence of steps?&lt;/li&gt;
  &lt;li&gt;Supports external data at query time, facts not in the &lt;label for=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; or the model’s training?&lt;/li&gt;
  &lt;li&gt;Supports taking actions (calling APIs, writing to external systems)?&lt;/li&gt;
  &lt;li&gt;Latency, roughly, how many round-trips to the model per user request?&lt;/li&gt;
  &lt;li&gt;Cost per request, roughly proportional to number of model invocations?&lt;/li&gt;
  &lt;li&gt;Observability / ease of audit, can a human reconstruct what happened?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-pattern-landscape&quot;&gt;The pattern landscape&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Single LLM call. One invocation, one response. The user’s prompt goes in; the model’s completion comes out. No external data, no tools, no multi-step reasoning beyond what fits in one prompt. The baseline, useful for tasks the model can do in one shot given its training (summarise this text, translate this sentence, classify this ticket).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Chain. Multiple LLM calls stitched together in application code. Output of call N feeds into input of call N+1. Topology is hardcoded. Example: “extract facts from this ticket (call 1), then generate a customer-facing summary from the facts (call 2).” Each step is an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; call; the orchestration is your Lambda or application server.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Retrieval (RAG). A specific two-step chain: retrieve relevant chunks from a document corpus, then generate using the chunks. AWS-native via Bedrock Knowledge Bases (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock-agent-runtime:RetrieveAndGenerate&lt;/code&gt;) or DIY with &lt;label for=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embeddings&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; + &lt;label for=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector store&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt;. Deterministic topology, one model call per request (two if you count the embedding call).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Chain with tool use. A chain where one or more steps allow the model to call tools. The model’s response might be “call tool X with these arguments”; the application executes the tool and sends the result back; the model continues. The chain topology is still fixed (step 1 can use tools, step 2 generates the final response, etc.) but within a step the model has degrees of freedom.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Agent. A loop, not a chain. The model is given tools and a goal; it plans, the application executes, the model observes, it re-plans. The loop continues until the model emits a “final answer.” AWS-native via Bedrock Agents, define an agent with instructions, action groups (each backed by a Lambda or an OpenAPI schema), optional Knowledge Bases, and optional &lt;label for=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-guardrail&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-guardrail-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;guardrails&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-guardrail&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-guardrail-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Guardrail&lt;/span&gt;A filter or rule applied to an LLM’s inputs or outputs to keep it inside safe, legal, or on-brand behaviour.
&lt;/span&gt;; invoke via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock-agent-runtime:InvokeAgent&lt;/code&gt;. The runtime handles the plan-act-observe loop and emits a trace showing each step.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Multi-agent orchestration. Multiple specialised agents coordinated by a supervisor agent. A billing agent, a customer-lookup agent, a ticketing agent, and a supervisor that routes requests to the correct one. Bedrock supports this via agent collaborators. Useful at scale when a single agent’s tool count exceeds what it can reason over reliably (typically 15-20 tools); over-engineering at lower scale.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Pattern&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Deterministic&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;External data&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Actions&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Latency&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Cost&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Auditability&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Single call&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1 hop&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Low&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Trivial&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Chain&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗ (unless tool)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;N hops&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Low-medium&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Easy&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Retrieval (RAG)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1-2 hops&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Low&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Easy&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Chain with tool use&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;N+M hops&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Medium&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Easy&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Agent&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Variable&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;High&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Trace-based&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Multi-agent&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Very variable&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Very high&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Multi-trace&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The trade is obvious reading down the table: as you move from single call to multi-agent, capability increases and predictability, latency, and cost all move the wrong way. Choosing well means picking the &lt;em&gt;lowest&lt;/em&gt; row that supports the required capability.&lt;/p&gt;

&lt;h3 id=&quot;pattern-decision-tree&quot;&gt;Pattern decision tree&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 620&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Decision tree for choosing between a single LLM call, retrieval, chain with tool use, and agent. Top question: does the task need knowledge the model does not have? If no, does it need to take actions? If no, single LLM call. If yes, does the flow have a fixed topology? If yes, chain with tool use. If no, agent. If it needs knowledge and only knowledge, retrieval. If it needs knowledge and actions, whether fixed topology decides between chain with tool use and retrieval combined, or agent with knowledge base.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .acr-q          { fill: #fff; stroke: #5a7a9a; stroke-width: 1.8; }
      .acr-leaf-s     { fill: rgba(110, 110, 120, 0.08); stroke: #555; stroke-width: 2; }
      .acr-leaf-r     { fill: rgba(46, 138, 90, 0.12); stroke: rgb(36, 108, 70); stroke-width: 2; }
      .acr-leaf-c     { fill: rgba(70, 140, 200, 0.12); stroke: rgb(50, 110, 170); stroke-width: 2; }
      .acr-leaf-a     { fill: rgba(180, 60, 150, 0.10); stroke: rgb(140, 40, 115); stroke-width: 2; }
      .acr-q-text     { font-size: 13px; fill: #222; }
      .acr-leaf-title { font-size: 14px; font-weight: 700; fill: #222; }
      .acr-leaf-sub   { font-size: 11px; fill: #444; }
      .acr-arrow      { fill: none; stroke: #666; stroke-width: 1.5; }
      .acr-label-y    { font-size: 11px; font-weight: 700; fill: rgb(36, 108, 70); }
      .acr-label-n    { font-size: 11px; font-weight: 700; fill: rgb(170, 60, 60); }
      .acr-header     { font-size: 16px; font-weight: 700; fill: #222; }
      .acr-map        { font-size: 12px; font-weight: 600; fill: #333; }
    &lt;/style&gt;
    &lt;marker id=&quot;acr-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;6&quot; markerHeight=&quot;6&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#666&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;40&quot; y=&quot;30&quot; class=&quot;acr-header&quot;&gt;Picking the pattern&lt;/text&gt;

  &lt;!-- Root --&gt;
  &lt;rect x=&quot;420&quot; y=&quot;60&quot; width=&quot;260&quot; height=&quot;60&quot; rx=&quot;8&quot; class=&quot;acr-q&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;85&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;Does the task need knowledge&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;103&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;outside the model&apos;s training?&lt;/text&gt;

  &lt;!-- Left branch: No external knowledge --&gt;
  &lt;path d=&quot;M475,120 L260,175&quot; class=&quot;acr-arrow&quot; marker-end=&quot;url(#acr-arrow)&quot; /&gt;
  &lt;text x=&quot;330&quot; y=&quot;150&quot; class=&quot;acr-label-n&quot;&gt;NO&lt;/text&gt;

  &lt;!-- Right branch: Yes external knowledge --&gt;
  &lt;path d=&quot;M625,120 L820,175&quot; class=&quot;acr-arrow&quot; marker-end=&quot;url(#acr-arrow)&quot; /&gt;
  &lt;text x=&quot;770&quot; y=&quot;150&quot; class=&quot;acr-label-y&quot;&gt;YES&lt;/text&gt;

  &lt;!-- Left: Does it need actions? --&gt;
  &lt;rect x=&quot;110&quot; y=&quot;180&quot; width=&quot;260&quot; height=&quot;60&quot; rx=&quot;8&quot; class=&quot;acr-q&quot; /&gt;
  &lt;text x=&quot;240&quot; y=&quot;205&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;Does it need to take&lt;/text&gt;
  &lt;text x=&quot;240&quot; y=&quot;223&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;actions in external systems?&lt;/text&gt;

  &lt;!-- Left-Left: No actions -&gt; single call --&gt;
  &lt;path d=&quot;M180,240 L160,310&quot; class=&quot;acr-arrow&quot; marker-end=&quot;url(#acr-arrow)&quot; /&gt;
  &lt;text x=&quot;150&quot; y=&quot;280&quot; class=&quot;acr-label-n&quot;&gt;NO&lt;/text&gt;

  &lt;rect x=&quot;60&quot; y=&quot;310&quot; width=&quot;200&quot; height=&quot;90&quot; rx=&quot;8&quot; class=&quot;acr-leaf-s&quot; /&gt;
  &lt;text x=&quot;160&quot; y=&quot;338&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-title&quot;&gt;Single LLM call&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;358&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;bedrock-runtime:&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;372&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;InvokeModel&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;388&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;one request, one response&lt;/text&gt;

  &lt;!-- Left-Right: Actions, fixed topology? --&gt;
  &lt;path d=&quot;M300,240 L340,305&quot; class=&quot;acr-arrow&quot; marker-end=&quot;url(#acr-arrow)&quot; /&gt;
  &lt;text x=&quot;345&quot; y=&quot;280&quot; class=&quot;acr-label-y&quot;&gt;YES&lt;/text&gt;

  &lt;rect x=&quot;290&quot; y=&quot;310&quot; width=&quot;220&quot; height=&quot;60&quot; rx=&quot;8&quot; class=&quot;acr-q&quot; /&gt;
  &lt;text x=&quot;400&quot; y=&quot;335&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;Is the flow topology fixed?&lt;/text&gt;
  &lt;text x=&quot;400&quot; y=&quot;353&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;(you know which tools, in order)&lt;/text&gt;

  &lt;!-- Chain with tool use --&gt;
  &lt;path d=&quot;M350,370 L310,430&quot; class=&quot;acr-arrow&quot; marker-end=&quot;url(#acr-arrow)&quot; /&gt;
  &lt;text x=&quot;310&quot; y=&quot;400&quot; class=&quot;acr-label-y&quot;&gt;YES&lt;/text&gt;

  &lt;rect x=&quot;200&quot; y=&quot;430&quot; width=&quot;200&quot; height=&quot;90&quot; rx=&quot;8&quot; class=&quot;acr-leaf-c&quot; /&gt;
  &lt;text x=&quot;300&quot; y=&quot;458&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-title&quot;&gt;Chain with tool use&lt;/text&gt;
  &lt;text x=&quot;300&quot; y=&quot;478&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;InvokeModel with tools&lt;/text&gt;
  &lt;text x=&quot;300&quot; y=&quot;492&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;orchestration in app code&lt;/text&gt;
  &lt;text x=&quot;300&quot; y=&quot;508&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;deterministic, cheap&lt;/text&gt;

  &lt;!-- Agent --&gt;
  &lt;path d=&quot;M450,370 L490,430&quot; class=&quot;acr-arrow&quot; marker-end=&quot;url(#acr-arrow)&quot; /&gt;
  &lt;text x=&quot;495&quot; y=&quot;400&quot; class=&quot;acr-label-n&quot;&gt;NO&lt;/text&gt;

  &lt;rect x=&quot;420&quot; y=&quot;430&quot; width=&quot;200&quot; height=&quot;90&quot; rx=&quot;8&quot; class=&quot;acr-leaf-a&quot; /&gt;
  &lt;text x=&quot;520&quot; y=&quot;458&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-title&quot;&gt;Agent&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;478&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;Bedrock Agents&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;492&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;model-driven plan-act loop&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;508&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;trace is the audit artefact&lt;/text&gt;

  &lt;!-- Right: Does it also need actions? --&gt;
  &lt;rect x=&quot;730&quot; y=&quot;180&quot; width=&quot;260&quot; height=&quot;60&quot; rx=&quot;8&quot; class=&quot;acr-q&quot; /&gt;
  &lt;text x=&quot;860&quot; y=&quot;205&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;Does it also need to&lt;/text&gt;
  &lt;text x=&quot;860&quot; y=&quot;223&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;take actions?&lt;/text&gt;

  &lt;!-- Retrieval --&gt;
  &lt;path d=&quot;M800,240 L780,305&quot; class=&quot;acr-arrow&quot; marker-end=&quot;url(#acr-arrow)&quot; /&gt;
  &lt;text x=&quot;770&quot; y=&quot;280&quot; class=&quot;acr-label-n&quot;&gt;NO&lt;/text&gt;

  &lt;rect x=&quot;680&quot; y=&quot;310&quot; width=&quot;200&quot; height=&quot;90&quot; rx=&quot;8&quot; class=&quot;acr-leaf-r&quot; /&gt;
  &lt;text x=&quot;780&quot; y=&quot;338&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-title&quot;&gt;Retrieval (RAG)&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;358&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;Bedrock Knowledge Bases&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;372&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;RetrieveAndGenerate&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;388&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;grounded answers + citations&lt;/text&gt;

  &lt;!-- Agent + KB --&gt;
  &lt;path d=&quot;M920,240 L940,305&quot; class=&quot;acr-arrow&quot; marker-end=&quot;url(#acr-arrow)&quot; /&gt;
  &lt;text x=&quot;945&quot; y=&quot;280&quot; class=&quot;acr-label-y&quot;&gt;YES&lt;/text&gt;

  &lt;rect x=&quot;900&quot; y=&quot;310&quot; width=&quot;180&quot; height=&quot;90&quot; rx=&quot;8&quot; class=&quot;acr-leaf-a&quot; /&gt;
  &lt;text x=&quot;990&quot; y=&quot;338&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-title&quot;&gt;Agent + KB&lt;/text&gt;
  &lt;text x=&quot;990&quot; y=&quot;358&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;Bedrock Agent with&lt;/text&gt;
  &lt;text x=&quot;990&quot; y=&quot;372&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;Knowledge Base attached&lt;/text&gt;
  &lt;text x=&quot;990&quot; y=&quot;388&quot; text-anchor=&quot;middle&quot; class=&quot;acr-leaf-sub&quot;&gt;+ action groups (Lambdas)&lt;/text&gt;

  &lt;!-- v1-v4 mapping footer --&gt;
  &lt;rect x=&quot;80&quot; y=&quot;550&quot; width=&quot;950&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;acr-q&quot; /&gt;
  &lt;text x=&quot;555&quot; y=&quot;575&quot; text-anchor=&quot;middle&quot; class=&quot;acr-map&quot;&gt;The four assistant versions on this map:&lt;/text&gt;
  &lt;text x=&quot;555&quot; y=&quot;595&quot; text-anchor=&quot;middle&quot; class=&quot;acr-q-text&quot;&gt;v1 Policy Q&amp;amp;A -&amp;gt; Retrieval · v2 Customer lookup -&amp;gt; Chain with tool use · v3 Email drafting -&amp;gt; Chain · v4 Ticket workflow -&amp;gt; Agent (when the flow genuinely branches)&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Two questions split the space cleanly. &quot;One big agent&quot; is almost always over-engineering; &quot;agent when it genuinely needs to plan&quot; is the rule worth holding.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-picks-in-depth&quot;&gt;The picks in depth&lt;/h3&gt;

&lt;p&gt;v1. Policy Q&amp;amp;A: retrieval. Knowledge lives in documents, no actions, fixed topology (retrieve, then generate). Implementation: a Bedrock Knowledge Base over the policy wiki’s S3 mirror, with semantic chunking and Titan v2 embeddings. The v1 endpoint is one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; call per question. Answers come back with citations; the UI renders clickable links to the source policy. No tools, no agent runtime.&lt;/p&gt;

&lt;p&gt;v2. Customer lookup: chain with tool use, not an agent. The topology isn’t uncertain, ask, optionally call one tool, format. The model never has to &lt;em&gt;plan&lt;/em&gt;; it just decides whether the question needed the tool. An agent here would be slower, costlier, and harder to debug for no extra capability the flow needs. Implementation: a tool description &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lookup_customer(id: int) -&amp;gt; {tier, last_login, plan_details}&lt;/code&gt;. The v2 endpoint calls Claude Sonnet with the user’s question, the tool description, and a &lt;label for=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-system-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-system-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;system prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-system-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-chains-retrieval-and-agents-for-a-genai-assistant-system-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;System prompt&lt;/span&gt;The instruction block that frames the model’s behaviour for a session, separate from the user’s messages.
&lt;/span&gt; instructing it to call the tool if the question requires customer data. The model responds either with text (if it didn’t need the tool) or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tool_use&lt;/code&gt; block with the customer ID. The application executes the Lambda behind the tool, sends the result back in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tool_result&lt;/code&gt; block, and the model produces the final text response. Two model calls in the typical case; one if the model can answer without the tool.&lt;/p&gt;

&lt;p&gt;v3. Email drafting: chain, with retrieval folded in. Fixed topology, retrieve customer + policy context, generate the draft, optionally review for tone/PII. The model doesn’t choose the path. Implementation: two or three steps in application code. Step 1: retrieve policy context relevant to the issue (if applicable) and the customer record. Step 2: pass the retrieved context plus the engineer’s instructions to Claude with a prompt template (“You are drafting a customer email. Tone: empathetic but professional. Include the following points…”). Step 3 (optional): a second model call that reviews the draft for tone and PII compliance.&lt;/p&gt;

&lt;p&gt;v4. Ticket workflow: agent, and this is where the pattern earns its keep. If the flow were “user says file a ticket, assistant extracts context and calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file_jira()&lt;/code&gt;”, that would still be a chain with one tool call. The agent pattern starts to make sense when the flow might be: check Jira for a duplicate first, comment on it if one matches, otherwise file new, then notify Slack, then escalate on-call if P1. That’s a decision tree where the model picks the path based on what each call returns, agent territory. Implementation: a Bedrock Agent with:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Instructions. “You help engineers file and manage Jira tickets. When asked to file, first search for duplicates; if a similar open ticket exists, prefer commenting on it. Always summarise the ticket back to the user for confirmation before filing. Never change ticket priority without explicit user confirmation.”&lt;/li&gt;
  &lt;li&gt;Action group: Jira. An OpenAPI schema (or Lambda function schema) exposing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jira_search(query)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jira_create_ticket(team, title, description, priority)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jira_comment(ticket_id, text)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jira_update(ticket_id, changes)&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Action group: Notifications. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slack_notify(channel, text)&lt;/code&gt; for the P1 escalation flow.&lt;/li&gt;
  &lt;li&gt;Knowledge Base attached. For any policy questions that come up mid-flow (“is this a P1 or P2?” requires consulting the severity policy).&lt;/li&gt;
  &lt;li&gt;Guardrails attached. Content filters; PII redaction on output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent runtime handles the loop: receive the user’s request, plan (model decides first action), act (runtime calls the action group’s Lambda), observe (result returns to the model), re-plan, etc. When the model emits a final response, the loop terminates. The runtime produces a trace, a structured record of each plan-act-observe cycle, which is the audit artefact.&lt;/p&gt;

&lt;p&gt;An example v4 flow, condensed:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;User: &quot;File a P2 ticket with team payments describing the duplicate-charge issue we
       just discussed. Attach customer 4711&apos;s context.&quot;

Agent:
  Thought: &quot;Search for existing P2 tickets matching this description first.&quot;
  Action: jira_search(query=&quot;duplicate charge customer 4711&quot;)
  Observation: [] (no matches)

  Thought: &quot;Look up customer context to include.&quot;
  Action: (uses Knowledge Base to retrieve recent chat history for customer 4711)
  Observation: &quot;Customer 4711, Enterprise tier, reported duplicate charge on 2027-02-26...&quot;

  Thought: &quot;Confirm ticket details with user before filing.&quot;
  Response to user: &quot;I&apos;ll file a P2 with team payments: &apos;Duplicate charge for
                    customer 4711 (Enterprise) on 2027-02-26.&apos; Confirm?&quot;

User: &quot;Yes, file it.&quot;

Agent:
  Action: jira_create_ticket(team=&quot;payments&quot;, title=&quot;Duplicate charge for customer 4711&quot;,
                             description=&quot;...&quot;, priority=&quot;P2&quot;)
  Observation: {ticket_id: &quot;PAY-2387&quot;}

  Response to user: &quot;Filed PAY-2387 with team payments. https://...&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Four model calls, three action-group invocations, one knowledge-base lookup, one user confirmation cycle. The trace shows every step with inputs, outputs, and the model’s reasoning. Compare that to the equivalent “chain with tool use” implementation: you’d have to hardcode the search-first-then-file logic, the confirmation step, the knowledge-base lookup, and the moment a user asks something slightly different (e.g. “file this or comment on PAY-2301 if it’s the same issue”), your hardcoded chain misses it. The agent handles variations without code changes; the cost is the non-deterministic topology and the observability burden of reading traces.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-dispatch&quot;&gt;A worked dispatch&lt;/h3&gt;

&lt;p&gt;The v1-to-v4 assistant in production fronts four endpoints, or one endpoint with an intent router. Either way, an individual request’s path looks like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Request: &quot;What&apos;s our data retention policy for chat logs?&quot;
Router classifies: policy question -&amp;gt; route to Knowledge Base
  RetrieveAndGenerate -&amp;gt; answer + citations
  Latency: ~1.5s. Cost: ~$0.003.

Request: &quot;What tier is customer 4711 on?&quot;
Router classifies: customer lookup -&amp;gt; route to chain with tool use
  Claude Sonnet (tool-use capable) -&amp;gt; tool_use: lookup_customer(4711)
  Lambda executes -&amp;gt; tool_result: {tier: &quot;Pro&quot;, ...}
  Claude formats -&amp;gt; &quot;Customer 4711 is on the Pro tier, last logged in 2 days ago.&quot;
  Latency: ~2s. Cost: ~$0.008.

Request: &quot;Draft an apology email for customer 4711 about their billing issue.&quot;
Router classifies: drafting -&amp;gt; route to email chain
  Step 1: retrieve customer + policy context.
  Step 2: generate draft with Claude Sonnet.
  Step 3: (optional) tone/PII review pass.
  Latency: ~4s. Cost: ~$0.015.

Request: &quot;File a P2 Jira ticket for the billing issue, attach customer context,
          and notify #customer-escalations on Slack.&quot;
Router classifies: multi-step workflow -&amp;gt; route to Bedrock Agent
  InvokeAgent -&amp;gt; agent runs its plan-act loop:
    search Jira -&amp;gt; retrieve context -&amp;gt; confirm with user -&amp;gt; create ticket -&amp;gt; notify Slack
  Returns final response + trace.
  Latency: ~10-15s across confirmations. Cost: ~$0.05-0.10.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Latency and cost vary by an order of magnitude across the four patterns. If you’d built the policy Q&amp;amp;A as an agent, each simple question would cost 5x more and take 5x longer, for no quality gain. If you’d built the ticket workflow as a chain, you’d have hardcoded the flow and brittle-failed on any variant the code didn’t anticipate. Each pattern matches the shape of its workload.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Chains, retrieval, and agents are different designs for different problems. Not rival approaches to the same problem. Picking wrong usually means over-engineering (agent where chain would do) rather than under-engineering.&lt;/li&gt;
  &lt;li&gt;Chains are fixed topologies; agents are model-driven topologies. That’s the fundamental distinction. Chains are deterministic in structure (though each LLM call is stochastic). Agents choose their own path through a tool space, so the same request can take different paths.&lt;/li&gt;
  &lt;li&gt;Tool use is a model feature, not an agent-only feature. A chain with tool use gets many agent-like capabilities (external data, actions) while keeping a fixed topology. This is often the correct middle ground.&lt;/li&gt;
  &lt;li&gt;Retrieval is a specific chain that deserves its own name. Retrieve, then generate. Bedrock Knowledge Bases is the managed path; DIY with embeddings + vector store is the flexible path.&lt;/li&gt;
  &lt;li&gt;Bedrock Agents handle the plan-act-observe loop for you. Define action groups (Lambdas or OpenAPI schemas), attach Knowledge Bases and Guardrails, invoke via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeAgent&lt;/code&gt;. The runtime produces a trace that’s the audit artefact.&lt;/li&gt;
  &lt;li&gt;Latency and cost scale with the pattern. Single call is 1-2 seconds and fractions of a cent. Retrieval is 1-3 seconds and a few cents. Chain with tool use is 3-5 seconds and an order of magnitude up. Agents are 5-20 seconds and another order of magnitude. Pick the cheapest pattern that works.&lt;/li&gt;
  &lt;li&gt;Non-determinism is the cost of agency. Agents will take different paths on the same request. That’s what makes them general; it’s also what makes them harder to test and explain. Keep agents to the flows that genuinely need them.&lt;/li&gt;
  &lt;li&gt;An intent router is the architecture most assistants actually want. Not “one big agent” handling everything. A router that classifies the request and dispatches to the correct pattern (retrieval, chain, agent) keeps the cheap paths cheap and reserves the expensive machinery for the cases that need it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The temptation, when agents are available, is to use them for everything, “then we don’t have to think about routing.” The result is a system that costs ten times more, takes five times longer, and is harder to audit than it needed to be. The harder, better discipline is to look at each piece of the workload, ask whether the model genuinely needs to choose the path, and reach for an agent only when the answer is yes.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Pricing Experiments: The Right Box at the Right Price</title>
    <link href="/writing/pricing-experiments-the-right-box/"/>
    <updated>2026-05-19T06:00:00+08:00</updated>
    <id>/writing/pricing-experiments-the-right-box/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Greenbox has just over two hundred subscribers. The &lt;a href=&quot;/writing/business-model-canvas-does-this-actually-work/&quot;&gt;Business Model Canvas&lt;/a&gt; workshop was earlier in the week, and something Lee said is still rattling around in Maya’s head: “You’ve validated almost everything on that canvas. The one box you haven’t tested is the revenue model.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lee is at Maya’s kitchen table with a coffee and the canvas printed on A3. He’s drawn a circle around the Revenue Streams box and written three words next to it: “untested, but working.”&lt;/p&gt;

&lt;p&gt;“The prices are working,” Maya says. “People are paying them. Nobody’s complained.”&lt;/p&gt;

&lt;p&gt;“Nobody who signed up has complained. You don’t know anything about the people who looked at the site, saw $25, and closed the tab. You don’t know whether you could charge $30 and still get the same conversion. You don’t know whether the small box is mispriced relative to the large box. You’ve got one data point, the current price, and you’ve decided it’s right because people are buying.”&lt;/p&gt;

&lt;p&gt;Maya frowns. She’d been congratulating herself, a little, that the pricing felt settled. It was one less thing to worry about.&lt;/p&gt;

&lt;p&gt;“So what are you suggesting?”&lt;/p&gt;

&lt;p&gt;“Pricing experiments. Not once. As a habit. You should be testing your pricing the way you test your features.”&lt;/p&gt;

&lt;h3 id=&quot;why-pricing-feels-different&quot;&gt;Why pricing feels different&lt;/h3&gt;

&lt;p&gt;Pricing is one of the few decisions in a startup that feels genuinely scary to get wrong. A bad feature can be rolled back. A bad copy tweak can be un-tweaked. A bad price, once published, anchors every subscriber’s expectation of what the product costs. Raise it and people feel cheated. Lower it and the people who paid the old price feel stupid.&lt;/p&gt;

&lt;p&gt;This is why most founders pick a number that feels right, ship it, and never touch it again. It’s not laziness. It’s loss-aversion. The downside of changing pricing feels enormous, and the upside feels uncertain.&lt;/p&gt;

&lt;p&gt;Lee has seen this pattern many times. His view is that it’s the wrong way to think about it.&lt;/p&gt;

&lt;p&gt;“The risk isn’t changing your price. The risk is being wrong about your price and not knowing. If you’re charging $25 for a small box that people would happily pay $30 for, you’re not being nice. You’re giving away five dollars per subscriber per week. At two hundred and ten subscribers, that’s $1,050 per week you’re leaving on the table. That’s a farm partner you could pay. That’s Sam’s hours. That’s runway.”&lt;/p&gt;

&lt;p&gt;Maya does the maths in her head. The number is uncomfortably large.&lt;/p&gt;

&lt;h3 id=&quot;what-to-test&quot;&gt;What to test&lt;/h3&gt;

&lt;p&gt;The team sits down on a Wednesday afternoon to work out what they actually want to learn. Lee uses the same approach they’ve used for product discovery: write down the assumptions and figure out which ones are worth testing.&lt;/p&gt;

&lt;p&gt;The assumptions on the wall, in Maya’s handwriting:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The small box at $25 and the large box at $45 are both priced correctly relative to cost.&lt;/li&gt;
  &lt;li&gt;The $20 gap between small and large reflects the actual value difference to subscribers.&lt;/li&gt;
  &lt;li&gt;Subscribers would not pay more than $25 for the small box.&lt;/li&gt;
  &lt;li&gt;A third, larger box option would not expand the market.&lt;/li&gt;
  &lt;li&gt;Weekly delivery is the only frequency that makes sense.&lt;/li&gt;
  &lt;li&gt;Free delivery is a deal-breaker if removed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lee reads down the list and then sets the marker down. “Which of these would you be most embarrassed to find out you’d been wrong about for six months?”&lt;/p&gt;

&lt;p&gt;Maya doesn’t hesitate. “Three. If people would happily pay $30 for the small box, I’d feel sick.”&lt;/p&gt;

&lt;p&gt;“Good. Let’s test three first.”&lt;/p&gt;

&lt;h3 id=&quot;designing-the-experiment&quot;&gt;Designing the experiment&lt;/h3&gt;

&lt;p&gt;This is where it gets interesting. You can’t just change the price on the website and see what happens, because if the new price is wrong you’ve damaged the brand. You also can’t ask people “would you pay $30?” in a survey, because people lie about pricing in surveys, not deliberately, but because stated preference and revealed preference are different things.&lt;/p&gt;

&lt;p&gt;Lee proposes a simple framework:&lt;/p&gt;

&lt;p&gt;Test the price before the commitment. New visitors who haven’t yet chosen a box see a landing page with the price. Half see $25. Half see $30. Everybody who clicks through can choose to continue at the price they saw. Measure two things: the click-through rate (do fewer people continue at $30?) and the conversion rate (of the people who continue, how many actually subscribe?).&lt;/p&gt;

&lt;p&gt;Be honest about what you’re doing. If a subscriber asks why they saw $30 and their friend saw $25, don’t pretend it was a glitch. Explain that you were testing pricing and wanted to understand what the right number was. Offer them whichever price they would have preferred.&lt;/p&gt;

&lt;p&gt;Lee pauses there. “That’s the design. Now the part I can’t do for you. I’ve watched plenty of these experiments and I know what they cost when they’re set up wrong, but the actual sample-size maths, how big and how long and how confident, isn’t my world. I can tell you a pricing test is worth running. I can’t tell you whether your traffic will let you read the result. You need that worked out before you agree on a duration.”&lt;/p&gt;

&lt;p&gt;He looks across the table. Priya is already on it.&lt;/p&gt;

&lt;p&gt;“Give me a minute. I want to be sure we can actually read a result before we agree to run this.”&lt;/p&gt;

&lt;p&gt;She pulls her notepad towards her.&lt;/p&gt;

&lt;p&gt;“The thing we’re worried about with $30 is people seeing the higher number and not subscribing. So the question I have to size up is: how many visitors per arm before we’d reliably notice a &lt;em&gt;drop&lt;/em&gt; in conversion at $30, if there’s one to notice? The maths is symmetric, the sample size comes out the same whether we frame the change we’re looking for as a fall or a lift, but the fall is what would actually hurt us, so that’s the version I’ll plug in.”&lt;/p&gt;

&lt;p&gt;She writes a few lines.&lt;/p&gt;

&lt;p&gt;“Before I pick a number for ‘how big a drop’, let me work out what would actually count as bad for us. At $25 with seven percent conversion we make $1.75 a visitor. We’re testing $30, so the question becomes: how far would conversion have to fall at $30 before the price hike is a loss instead of a win? Revenue per visitor matches when $30 × p equals $1.75, so p = $1.75 ÷ $30, five point eight three percent. Anything below that and $30 is making us &lt;em&gt;less&lt;/em&gt; money than $25, not more. So the drop we’d care about catching is conversion falling from seven percent to about five point eight, roughly a seventeen percent relative drop. That’s what I’ll size for.&lt;/p&gt;

&lt;p&gt;“Two arms, $25 and $30. Conversion seven percent today. Target drop pinned at break-even, seventeen percent relative. Standard knobs on confidence and power. The back-of-envelope number is roughly seven thousand visitors per arm. We’re getting maybe a hundred and fifty visitors a week. That’s nearly a year per arm.”&lt;/p&gt;

&lt;p&gt;Tom holds up a hand. “Nearly a &lt;em&gt;year&lt;/em&gt;? Hold on, before you tell me the rest, can you walk us through where that number actually comes from? I hear ‘seven thousand per arm’ and I’ve got no idea what’s in the calculation.”&lt;/p&gt;

&lt;p&gt;Priya nods. “Worth doing. Five things go into it, and they’re all things we have to commit to &lt;em&gt;before&lt;/em&gt; we run the experiment, not after.”&lt;/p&gt;

&lt;p&gt;She turns to a fresh page.&lt;/p&gt;

&lt;p&gt;“First, the &lt;em&gt;baseline&lt;/em&gt;. The conversion rate we’re comparing against. For us that’s seven percent, the fraction of visitors today who actually subscribe at $25.&lt;/p&gt;

&lt;p&gt;“Second, the &lt;em&gt;shift&lt;/em&gt; we want to be able to detect. We have to commit up front to a size of change that matters to us. I picked the break-even drop, seven percent falling to about five point eight, a roughly seventeen percent relative shift. If we’d only cared about catching a &lt;em&gt;much&lt;/em&gt; bigger collapse, twenty-five percent, conversion all the way down to five point three, we’d need fewer visitors because the gap is bigger and easier to see through the noise. If we wanted to spot a subtler ten percent drop, seven sliding to six and a bit, we’d need a lot more. Small differences look a lot like noise, and the maths punishes us for asking it to see through them.&lt;/p&gt;

&lt;p&gt;“Third, the &lt;em&gt;null hypothesis&lt;/em&gt;. The ‘nothing’s actually happening here’ assumption, the default we’d hold to until the data pushes us off it. In our case: ‘$25 and $30 convert identically.’ The whole game is asking whether the gap we observe between the two arms could plausibly have come from chance, even if the prices really do convert identically. If yes, we shrug. If no, we’ve learned something.&lt;/p&gt;

&lt;p&gt;“Fourth, &lt;em&gt;alpha&lt;/em&gt;. How often we’re willing to be fooled by chance into thinking there’s an effect when there isn’t. Standard number is five percent, one experiment in twenty where we cry ‘effect!’ when actually nothing’s there.&lt;/p&gt;

&lt;p&gt;“Fifth, &lt;em&gt;power&lt;/em&gt;. The flip side. If there really is an effect, how likely are we to catch it? Standard number is eighty percent. So even with a perfectly designed experiment, one time in five we’d miss a real difference and write it off as noise.”&lt;/p&gt;

&lt;p&gt;Tom nods slowly. “So alpha and power are us deciding how cautious we want to be in each direction.”&lt;/p&gt;

&lt;p&gt;“Exactly. Once you’ve fixed all four, baseline, target shift, alpha, power, there’s a formula that turns them into a sample size. Two pieces of vocabulary inside that formula are worth pinning down before I show the working. The first is &lt;em&gt;standard deviation&lt;/em&gt;. Think of it as the typical size of the wobble. Flip a fair coin a hundred times and you expect fifty heads, but you don’t get exactly fifty every time, sometimes forty-six, sometimes fifty-three. Standard deviation puts a number on that everyday wobble: the noise you get from a process even when nothing interesting is going on.&lt;/p&gt;

&lt;p&gt;“The second is the &lt;em&gt;z-score&lt;/em&gt;. That’s our signal measured in wobble-units: how many standard deviations away from boring-and-noisy a result sits. The bigger the z, the harder it gets to explain the result away as noise. Alpha and power feed into the formula as z-scores, five percent alpha and eighty percent power are roughly z = 1.96 and z = 0.84, numbers worth recognising on sight.”&lt;/p&gt;

&lt;p&gt;She turns the notepad landscape to give herself more room and starts writing as she talks.&lt;/p&gt;

&lt;p&gt;“Here’s the formula, for two proportions like ours. Sample size per arm:&lt;/p&gt;

&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 660 215&quot; role=&quot;img&quot; aria-label=&quot;Sample size n per arm equals open paren z sub 1 plus z sub 2 close paren squared, times p sub 1 times open paren 1 minus p sub 1 close paren plus p sub 2 times open paren 1 minus p sub 2 close paren, all divided by open paren p sub 1 minus p sub 2 close paren squared. The first numerator term is labelled cautiousness, the second numerator term is labelled noise, and the denominator is labelled signal.&quot; style=&quot;max-width: 660px; width: 100%; height: auto; display: block; margin: var(--space-md) auto;&quot;&gt;
  &lt;style&gt;
    .priya-formula-main { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 22px; fill: currentColor; }
    .priya-formula-var { font-style: italic; }
    .priya-formula-cautious { fill: #2f6db8; }
    .priya-formula-noise { fill: #b8862f; }
    .priya-formula-signal { fill: #4a8a4a; }
    .priya-formula-label { font-family: Georgia, serif; font-size: 13px; font-style: italic; letter-spacing: 0.04em; }
    .priya-formula-bar { stroke: currentColor; stroke-width: 1.5; fill: none; }
  &lt;/style&gt;

  &lt;text class=&quot;priya-formula-label priya-formula-cautious&quot; x=&quot;160&quot; y=&quot;42&quot; text-anchor=&quot;middle&quot;&gt;cautiousness&lt;/text&gt;
  &lt;text class=&quot;priya-formula-label priya-formula-noise&quot; x=&quot;425&quot; y=&quot;42&quot; text-anchor=&quot;middle&quot;&gt;noise&lt;/text&gt;

  &lt;text class=&quot;priya-formula-main&quot; x=&quot;20&quot; y=&quot;115&quot;&gt;&lt;tspan class=&quot;priya-formula-var&quot;&gt;n&lt;/tspan&gt;&lt;tspan dx=&quot;8&quot;&gt;=&lt;/tspan&gt;&lt;/text&gt;

  &lt;text x=&quot;160&quot; y=&quot;100&quot; text-anchor=&quot;middle&quot; class=&quot;priya-formula-main priya-formula-cautious&quot;&gt;(&lt;tspan class=&quot;priya-formula-var&quot;&gt;z&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;5&quot;&gt;1&lt;/tspan&gt;&lt;tspan font-size=&quot;22&quot; dy=&quot;-5&quot;&gt; + &lt;/tspan&gt;&lt;tspan class=&quot;priya-formula-var&quot;&gt;z&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;5&quot;&gt;2&lt;/tspan&gt;&lt;tspan font-size=&quot;22&quot; dy=&quot;-5&quot;&gt;)&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;-10&quot;&gt;2&lt;/tspan&gt;&lt;/text&gt;

  &lt;text x=&quot;245&quot; y=&quot;100&quot; text-anchor=&quot;middle&quot; class=&quot;priya-formula-main&quot;&gt;×&lt;/text&gt;

  &lt;text x=&quot;425&quot; y=&quot;100&quot; text-anchor=&quot;middle&quot; class=&quot;priya-formula-main priya-formula-noise&quot;&gt;&lt;tspan class=&quot;priya-formula-var&quot;&gt;p&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;5&quot;&gt;1&lt;/tspan&gt;&lt;tspan font-size=&quot;22&quot; dy=&quot;-5&quot;&gt;(1 − &lt;/tspan&gt;&lt;tspan class=&quot;priya-formula-var&quot;&gt;p&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;5&quot;&gt;1&lt;/tspan&gt;&lt;tspan font-size=&quot;22&quot; dy=&quot;-5&quot;&gt;) + &lt;/tspan&gt;&lt;tspan class=&quot;priya-formula-var&quot;&gt;p&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;5&quot;&gt;2&lt;/tspan&gt;&lt;tspan font-size=&quot;22&quot; dy=&quot;-5&quot;&gt;(1 − &lt;/tspan&gt;&lt;tspan class=&quot;priya-formula-var&quot;&gt;p&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;5&quot;&gt;2&lt;/tspan&gt;&lt;tspan font-size=&quot;22&quot; dy=&quot;-5&quot;&gt;)&lt;/tspan&gt;&lt;/text&gt;

  &lt;line class=&quot;priya-formula-bar&quot; x1=&quot;80&quot; y1=&quot;120&quot; x2=&quot;600&quot; y2=&quot;120&quot; /&gt;

  &lt;text x=&quot;340&quot; y=&quot;155&quot; text-anchor=&quot;middle&quot; class=&quot;priya-formula-main priya-formula-signal&quot;&gt;(&lt;tspan class=&quot;priya-formula-var&quot;&gt;p&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;5&quot;&gt;1&lt;/tspan&gt;&lt;tspan font-size=&quot;22&quot; dy=&quot;-5&quot;&gt; − &lt;/tspan&gt;&lt;tspan class=&quot;priya-formula-var&quot;&gt;p&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;5&quot;&gt;2&lt;/tspan&gt;&lt;tspan font-size=&quot;22&quot; dy=&quot;-5&quot;&gt;)&lt;/tspan&gt;&lt;tspan font-size=&quot;14&quot; dy=&quot;-10&quot;&gt;2&lt;/tspan&gt;&lt;/text&gt;

  &lt;text class=&quot;priya-formula-label priya-formula-signal&quot; x=&quot;340&quot; y=&quot;185&quot; text-anchor=&quot;middle&quot;&gt;signal&lt;/text&gt;
&lt;/svg&gt;

&lt;p&gt;Three pieces. The &lt;em&gt;cautiousness&lt;/em&gt; term, (z₁ + z₂)² in blue, is alpha and power expressed as z-scores, added then squared. The &lt;em&gt;noise&lt;/em&gt; term, the amber piece, has each arm contributing its own wobble, p(1−p), summed across the two arms. The &lt;em&gt;signal&lt;/em&gt; term, (p₁ − p₂)² in green underneath, is the gap we want to detect, squared. Big signal, small sample needed. Small signal, the sample balloons, and the squaring on the gap is what makes it balloon.&lt;/p&gt;

&lt;p&gt;“Plug ours in. &lt;em&gt;Cautiousness&lt;/em&gt;: (1.96 + 0.84)² is about 7.84. &lt;em&gt;Noise&lt;/em&gt;: 0.07 × 0.93 is about 0.065 in the $25 arm; 0.0583 × 0.9417 is about 0.055 in the $30 arm; sum is about 0.12. &lt;em&gt;Signal&lt;/em&gt;: (0.07 − 0.0583)² is 0.0117², which is about 0.000137. Multiply cautiousness by noise: 7.84 × 0.12 is about 0.94. Divide by signal: 0.94 ÷ 0.000137 is just under seven thousand. Per arm. That’s where the number comes from.”&lt;/p&gt;

&lt;p&gt;She underlines the result. “About seven thousand visitors per arm to spot the break-even drop. We get a hundred and fifty a week. Forty-six weeks per arm, nearly a year. If we’d settle for spotting a much bigger collapse, twenty-five percent or so, we could call it in about four months. For a subtler ten percent drop, we’re back over two years per arm. For a five percent drop, well inside what a small price tweak might plausibly move, the better part of a decade.”&lt;/p&gt;

&lt;p&gt;Tom sits back. “So what &lt;em&gt;does&lt;/em&gt; two weeks of data buy us?”&lt;/p&gt;

&lt;p&gt;“Worth working out. Let me run the formula backwards. Two weeks gives us roughly three hundred visitors per arm. If I fix the cautiousness and noise where they were and solve for the &lt;em&gt;gap&lt;/em&gt; instead, what’s the smallest difference we’d reliably catch at n = 300?, the maths gives me about six percentage points. So a real conversion rate would have to drop from seven percent to about one percent before our two-week test would reliably notice. Anything subtler than that, we miss most of the time.”&lt;/p&gt;

&lt;p&gt;She thinks for a second.&lt;/p&gt;

&lt;p&gt;“More usefully: take the gap we actually care about, the break-even drop, seven down to five point eight, and ask how often we’d correctly call it. At three hundred per arm the power drops from eighty percent to about &lt;em&gt;eight&lt;/em&gt; percent. So even if $30 genuinely tips us over to break-even, our two-week test would flag it only about one time in twelve. The other eleven times we’d shrug and write it off as noise.&lt;/p&gt;

&lt;p&gt;“And the flip side is just as ugly. Even if $25 and $30 convert &lt;em&gt;identically&lt;/em&gt;, we’ll see a gap that &lt;em&gt;looks&lt;/em&gt; significant about one time in twenty. That’s the alpha we picked, and alpha doesn’t get kinder when the sample’s small. So a ‘significant’ result at this scale could be a real effect we got lucky enough to spot, &lt;em&gt;or&lt;/em&gt; it could be pure chance. We can’t tell which from the data alone.”&lt;/p&gt;

&lt;p&gt;She caps the pen.&lt;/p&gt;

&lt;p&gt;“At our volume the test is statistically blind. Whatever number comes out, we can’t separate signal from noise. What we &lt;em&gt;can&lt;/em&gt; do is read the direction the gap leans, and decide in advance whether that’s enough to act on.”&lt;/p&gt;

&lt;p&gt;Maya looks at Lee. “So the experiment can’t really prove anything in any sane window.”&lt;/p&gt;

&lt;p&gt;“Not at your volume. Which means the choice you have isn’t ‘run it until it’s statistically valid’. It’s ‘run it long enough to see the shape of the signal, then act on the direction’. You’ll be moving from one defensible price to another defensible price with a lean, not a proof. That’s the only kind of pricing decision you can make at this stage.”&lt;/p&gt;

&lt;p&gt;Maya thinks about it. “If we’re wrong by five percent in either direction, we can adjust. We won’t be wrong by fifty percent.”&lt;/p&gt;

&lt;p&gt;“That’s the right framing. And it tells you what the time limit is for.”&lt;/p&gt;

&lt;p&gt;Set a time limit. Two weeks, then stop, not because two weeks will give you certainty (it won’t), but because the time box is what limits your exposure. A pricing experiment is a temporary act of price discrimination, and the longer it runs the more it corrodes trust. The time limit is containment, not measurement.&lt;/p&gt;

&lt;p&gt;Know what you’ll do with each outcome. Before starting, write down what decision you’ll make if revenue per visitor goes up, goes down, or barely moves. “The data is inconclusive” needs to be one of the outcomes you’ve planned for, because at this volume it’s the most likely one. Decide in advance whether a directional lean is enough to act on. If it isn’t, don’t run the experiment yet; save it for when you’ve got the traffic.&lt;/p&gt;

&lt;p&gt;Priya adds one more thing. “And we write a note in the wiki, ‘small box price, revisit when weekly visitors exceed five thousand’. Future us deserves to know we acted on a lean, not on a proof.”&lt;/p&gt;

&lt;p&gt;Tom has a concern. “What if conversion drops by ten percent at $30? Does that mean $30 is the wrong price?”&lt;/p&gt;

&lt;p&gt;Lee thinks about it. “Not necessarily. If conversion drops ten percent but revenue per converted subscriber goes up twenty percent, you’re still ahead. The question isn’t ‘does conversion drop’, it’s ‘does total revenue go up or down.’ And you have to weight that against the long-term effects on word-of-mouth, retention, and brand.”&lt;/p&gt;

&lt;h3 id=&quot;running-the-experiment&quot;&gt;Running the experiment&lt;/h3&gt;

&lt;p&gt;They run it for two weeks.&lt;/p&gt;

&lt;p&gt;The setup is deliberately simple. Priya writes a small piece of code that assigns each new visitor to one of two groups at random and shows them the appropriate landing page. The price on the page is the price they’d pay if they subscribed. Nothing else about the site changes.&lt;/p&gt;

&lt;p&gt;Over two weeks, 312 visitors see the $25 page. 298 visitors see the $30 page. The split is even enough to compare, and, as the team already knows going in, well short of what statistical confidence would require.&lt;/p&gt;

&lt;p&gt;The results:&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;padding: var(--space-sm) var(--space-md); background: rgba(0,0,0,0.04); border-bottom: 1px solid var(--color-rule); text-align: center;&quot;&gt;
    &lt;strong&gt;Small box pricing experiment: two weeks&lt;/strong&gt;
  &lt;/div&gt;
  &lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 0.88rem;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;border-bottom: 1px solid var(--color-rule);&quot;&gt;
        &lt;th style=&quot;padding: var(--space-xs) var(--space-sm); text-align: left; color: var(--color-ink-tertiary);&quot;&gt;Variant&lt;/th&gt;
        &lt;th style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right; color: var(--color-ink-tertiary);&quot;&gt;Visitors&lt;/th&gt;
        &lt;th style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right; color: var(--color-ink-tertiary);&quot;&gt;Clicked through&lt;/th&gt;
        &lt;th style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right; color: var(--color-ink-tertiary);&quot;&gt;Subscribed&lt;/th&gt;
        &lt;th style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right; color: var(--color-ink-tertiary);&quot;&gt;Revenue/week&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr style=&quot;border-bottom: 1px solid var(--color-rule);&quot;&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); font-weight: 600;&quot;&gt;$25 (control)&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right;&quot;&gt;312&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right;&quot;&gt;184 (59%)&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right;&quot;&gt;22 (7.1%)&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right;&quot;&gt;$550&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); font-weight: 600;&quot;&gt;$30 (test)&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right;&quot;&gt;298&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right;&quot;&gt;164 (55%)&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right;&quot;&gt;19 (6.4%)&lt;/td&gt;
        &lt;td style=&quot;padding: var(--space-xs) var(--space-sm); text-align: right;&quot;&gt;$570&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;Click-through drops from 59% to 55%. Conversion drops from 7.1% to 6.4%. But revenue per visitor goes &lt;em&gt;up&lt;/em&gt;, because the people who subscribe at $30 are paying more than the people who subscribe at $25.&lt;/p&gt;

&lt;p&gt;The total revenue from the $30 cohort is slightly higher than the $25 cohort, despite slightly fewer subscribers.&lt;/p&gt;

&lt;h3 id=&quot;reading-the-numbers&quot;&gt;Reading the numbers&lt;/h3&gt;

&lt;p&gt;The team gathers round Priya’s laptop.&lt;/p&gt;

&lt;p&gt;“This is roughly the shape we expected,” Priya says. “Twenty-two subscribers versus nineteen, out of about three hundred visitors each. The gap is well inside the noise, if we’d run the experiment in a different fortnight, those numbers could easily have flipped. The data isn’t conclusive. We knew going in it wouldn’t be.”&lt;/p&gt;

&lt;p&gt;“Then what is it telling us?” Maya asks.&lt;/p&gt;

&lt;p&gt;“Direction. Click-through is slightly lower at $30. Conversion is slightly lower. Revenue per visitor is slightly higher. That’s the shape you’d expect if $30 is closer to the right price than $25, and roughly the opposite of what you’d see if $30 were too high. It’s not proof. It’s a lean.”&lt;/p&gt;

&lt;p&gt;Lee picks it up. “And the team agreed before we ran it that a lean is what we’d act on. The alternative was waiting years for a confidence we don’t actually need to make a five-dollar decision.”&lt;/p&gt;

&lt;h3 id=&quot;the-decision&quot;&gt;The decision&lt;/h3&gt;

&lt;p&gt;Maya looks at the numbers again. “So we raise the price.”&lt;/p&gt;

&lt;p&gt;“On a directional signal,” Lee says. “Not because the data proved anything, but because we said we’d act on direction and the direction is up. If it had pointed the other way we’d be having a much shorter meeting. The five percent more revenue per visitor isn’t huge, but the people who said yes at $30 are telling you they value the box at $30. The people who said no were probably never going to be great subscribers anyway. They would have subscribed for a month and cancelled.”&lt;/p&gt;

&lt;p&gt;“Before you change anything, how do you feel about the subscribers who paid $25?”&lt;/p&gt;

&lt;p&gt;Maya thinks. “I don’t want to raise the price on them. They signed up at $25 and that was the deal.”&lt;/p&gt;

&lt;p&gt;“Good instinct. Honour the original price for existing subscribers indefinitely. New subscribers sign up at $30. Your existing subscribers feel looked after. Your new subscribers feel fairly treated. The only people who lose are the ones who would have subscribed at $25 but won’t at $30, and the experiment tells you that’s a small group, and a group that probably wouldn’t have stuck around.”&lt;/p&gt;

&lt;p&gt;It’s a clean decision, but it’s only clean because they measured first, and only honest because they were clear, before measuring, about what kind of evidence they’d accept.&lt;/p&gt;

&lt;h3 id=&quot;what-gets-tested-next&quot;&gt;What gets tested next&lt;/h3&gt;

&lt;p&gt;Maya and Lee work through the rest of the list. The $20 gap between small and large is the obvious next target, assumption 2 from the wall. After that, the mixed-sourcing pilot that’s been on the whiteboard for weeks. Each one a separate test. Each one starting the same way: write down the question, design the test, decide what you’d do with each outcome before you run it, measure, decide.&lt;/p&gt;

&lt;p&gt;Pricing experiments. Not once. As a habit.&lt;/p&gt;

&lt;p&gt;The team doesn’t know it yet, but the question on the wall is about to change. The discipline they’ve just learned will get its first real test on a deadline they didn’t pick. That’s a story for another week.&lt;/p&gt;

&lt;p&gt;Lee writes a single sentence at the top of the pricing page in the team wiki:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Price is an assumption until you’ve tested it. Test the assumptions you’d be most embarrassed to be wrong about first.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maya reads it, nods, and goes back to the kitchen to think about what to test next.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>How to Make a Bedrock Chatbot Audit-Ready with Guardrails and Watermarks</title>
    <link href="/writing/how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks/"/>
    <updated>2026-05-18T06:00:00+08:00</updated>
    <id>/writing/how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;AI Practitioner&lt;/strong&gt; · AIF-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A mid-size fintech runs a customer-facing chatbot on Bedrock. The chatbot helps the roughly 2M active customers understand their transaction history, explain fees, surface policy documents, and escalate to a human when needed. It runs on Claude Sonnet 4.5, invoked from a Lambda behind API Gateway.&lt;/p&gt;

&lt;p&gt;Three compliance obligations:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;No regulated financial advice. The chatbot can explain what a fee &lt;em&gt;is&lt;/em&gt;; it cannot recommend whether to invest, what to buy, or when to sell. Crossing that line is a regulated-advice violation.&lt;/li&gt;
  &lt;li&gt;No customer PII in outputs. The model should never echo a full account number, full name + date of birth together, or any other field that would count as PII under the relevant privacy regulation. The chatbot has access to this data (via tool use) but should redact it in responses.&lt;/li&gt;
  &lt;li&gt;Auditable provenance. Every response must be attributable: which model produced it, which &lt;label for=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt;, which customer session, and, in the event of a dispute, proof that the text came from the AWS-hosted model rather than a third-party intercept or a compromised channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separately, the product team wants to know: when the chatbot refuses a request (e.g. “I can’t give investment advice”), what does that refusal look like? Who controls the refusal message? And can users bypass it with prompt tricks (“pretend you’re a financial advisor…”)?&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;Responsible-AI controls live in three broad layers, real-time enforcement, audit persistence, and identity/encryption, and they don’t overlap neatly with the three compliance obligations. The first job is mapping question to control category.&lt;/p&gt;

&lt;p&gt;The first is &lt;em&gt;topic restriction&lt;/em&gt;. The “no financial advice” requirement is about &lt;em&gt;what the model talks about&lt;/em&gt;, not &lt;em&gt;what words appear in the output&lt;/em&gt;. A topic-level control needs to recognise that “should I invest in XYZ?” is a regulated-advice question even if phrased as “what do you think about XYZ?” or “if you were me, what would you buy?”. That calls for a classifier-style filter that fires on intent, intercepting the invocation and returning a canned refusal instead of the model’s response.&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;PII redaction&lt;/em&gt;. This is about &lt;em&gt;patterns in the output&lt;/em&gt;, an account number has a recognisable shape, an email address matches a regex, a full name is an entity a tagger can identify. The right control combines a catalogue of standard PII types with user-defined regex patterns for domain-specific identifiers (the fintech’s internal account number format, say). It also has to offer a choice between blocking the invocation entirely and redacting, replacing the matched text with a typed tag, because some references are legitimate when anonymised and others are never acceptable in output at all.&lt;/p&gt;

&lt;p&gt;The third is &lt;em&gt;blunt word and phrase blocking&lt;/em&gt;. Profanity, competitor mentions, and a hard-coded block list live here. Less important for this specific scenario but part of any complete control catalogue.&lt;/p&gt;

&lt;p&gt;The fourth is &lt;em&gt;generic content moderation&lt;/em&gt;. Hate, insults, sexual content, violence, misconduct, and &lt;label for=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-jailbreak&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-jailbreak-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt-injection&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-jailbreak&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-jailbreak-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Jailbreak&lt;/span&gt;A prompt that bypasses a model’s safety training and gets it to produce output it would normally refuse.
&lt;/span&gt; attempts, each with a configurable severity threshold, applied independently to input and output. This is the safety net that catches the cases the topic and PII controls don’t explicitly cover.&lt;/p&gt;

&lt;p&gt;The fifth is &lt;em&gt;grounding and relevance checks&lt;/em&gt;. The control evaluates whether the model’s response is grounded in the source material provided to it and whether it actually addresses the user’s question, blocking or flagging when either score falls below a threshold. Most relevant for RAG-heavy chatbots; worth knowing for the fintech scenario even though the primary obligations don’t lean on it.&lt;/p&gt;

&lt;p&gt;The sixth is &lt;em&gt;watermarking and provenance&lt;/em&gt;. Text-generation watermarking (statistical patterns embedded in the output that can be detected later) is an emerging capability, not yet universal. The reliable provenance story today is the platform’s own call log: every model invocation recorded with principal, model identity, timestamp, and, with full payload logging enabled, the request and response stored encrypted at rest. Combined with identity restrictions on which principals can invoke which models, this gives a cryptographically-bounded audit trail.&lt;/p&gt;

&lt;p&gt;The seventh is &lt;em&gt;the shape of the refusal itself&lt;/em&gt;. When a real-time filter intercepts an invocation, the caller needs to receive something distinguishable from a normal completion: a stop reason, an assessment showing which policy fired, and a configurable refusal message that the application can render. Refusals are control flow, not error handling.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Five filters, one per compliance requirement (with PII split into “detect” and “action taken”).&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Does this control enforce topic-level restrictions (e.g. no financial advice)?&lt;/li&gt;
  &lt;li&gt;Does it detect PII in inputs and outputs?&lt;/li&gt;
  &lt;li&gt;Does it let the response be redacted rather than blocked (for cases where redaction is enough)?&lt;/li&gt;
  &lt;li&gt;Does it cover prompt-injection attempts (“ignore previous instructions…”)?&lt;/li&gt;
  &lt;li&gt;Does it produce an audit artefact, something an auditor can inspect after the fact?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-bedrock-responsible-ai-landscape&quot;&gt;The Bedrock responsible-AI landscape&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Bedrock Guardrails. A configuration object attached to an invocation that applies one or more policies to the input, the output, or both. Policies: denied topics (up to 30 per guardrail, each a named topic with description and example prompts), content filters (six categories + prompt attacks, each with severity threshold), word filters (block list + managed profanity), sensitive-information filters (the built-in PII catalogue + user regexes), and, for RAG use cases, contextual grounding and relevance checks. Each guardrail is versioned; invocations reference a specific version. Created via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock:CreateGuardrail&lt;/code&gt;, versioned via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreateGuardrailVersion&lt;/code&gt;, applied via the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;guardrailIdentifier&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;guardrailVersion&lt;/code&gt; fields on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; (or automatically when a Knowledge Base or Agent has a guardrail attached).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Model invocation logging. A Bedrock account-level setting (one per Region) that directs Bedrock to write full request and response payloads to a destination: S3 bucket, CloudWatch Logs, or both. Enabled via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock:PutModelInvocationLoggingConfiguration&lt;/code&gt;. Captures the prompt, the model’s raw output, any guardrail assessments, and metadata (model ID, timestamp, caller IAM principal via CloudTrail correlation). Encrypts at rest under a KMS key of your choice. This is the durable audit trail.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;CloudTrail. Every Bedrock API call – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreateGuardrail&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetGuardrail&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PutModelInvocationLoggingConfiguration&lt;/code&gt;, emits a CloudTrail event. Data events can be enabled to capture &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; calls specifically (they’re not in management events by default). Gives the “who called what, when” audit; doesn’t include the model’s output (that’s invocation logging’s job).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;IAM-scoped model access. A Bedrock IAM policy controls which principals can invoke which models. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock:InvokeModel&lt;/code&gt; on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0&lt;/code&gt; restricts a role to one model. The chatbot Lambda’s role should allow exactly the models the application is approved to use, nothing else; requests for other models return &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AccessDenied&lt;/code&gt; in CloudTrail before the model is invoked.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Customer-managed KMS keys. Invocation logs, training data, and custom models can be encrypted with customer-managed KMS keys. Gives the ability to revoke access to historical logs by disabling the key, and to require explicit key-usage grants to read the audit record. The regulator-facing story.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Cross-Region &lt;label for=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-make-a-bedrock-chatbot-audit-ready-with-guardrails-and-watermarks-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt; profiles and data residency. For regulators that care where inference happens, Bedrock’s model ARNs pin the Region, and cross-Region inference profiles (for models that support it) expose an explicit list of which Regions can serve a request. Important for the audit story when data-residency constraints apply.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Bedrock Evaluation. Not a real-time control, but part of the responsible-AI story: systematic evaluation of a model (or a prompt-and-model combination) on dimensions including toxicity, robustness, and accuracy, against either built-in datasets or your own. The pre-production counterpart to Guardrails’ in-production enforcement.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;p&gt;Mapping each control to the three compliance obligations plus the four attributes:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Control&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Topic restriction&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;PII detection&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Redact option&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Prompt-injection&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Audit artefact&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Guardrails: denied topics&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (assessment)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Guardrails: content filters&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (prompt attacks)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (assessment)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Guardrails: PII filters&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (anonymize)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (assessment)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Guardrails: word filters&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (assessment)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Invocation logging&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (full payload)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CloudTrail&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (metadata)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;IAM model scoping&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;—&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (deny trail)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A complete configuration for the fintech chatbot uses &lt;em&gt;all&lt;/em&gt; of these, not one. Guardrails handle real-time enforcement; invocation logging and CloudTrail handle audit; IAM handles the “this model, not another” question; KMS handles the “this key, held by us” question.&lt;/p&gt;

&lt;h3 id=&quot;how-the-controls-compose&quot;&gt;How the controls compose&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 600&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Bedrock invocation with layered guardrail controls shown as a left-to-right pipeline with branches. User input enters from the left. First stage: input guardrail evaluates for denied topics, content filters including prompt-attack detection, and PII in input; if any fires, the invocation returns a refusal without calling the model. If allowed, the input passes to the model (Claude Sonnet). Model output passes through the output guardrail: denied topics, content filters, PII anonymization, grounding and relevance checks. Clean output returns to the user. All stages feed metadata to CloudTrail and full payloads to invocation logging in an S3 bucket encrypted with a customer-managed KMS key. IAM and resource policies gate who can invoke what at the entry.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .grw-user     { fill: #fff; stroke: #333; stroke-width: 1.5; }
      .grw-model    { fill: rgba(100, 60, 180, 0.10); stroke: #5a3aa0; stroke-width: 2; }
      .grw-guard    { fill: rgba(214, 142, 41, 0.10); stroke: #b08020; stroke-width: 1.8; }
      .grw-audit    { fill: rgba(46, 138, 90, 0.08); stroke: rgb(46, 138, 90); stroke-width: 1.5; }
      .grw-refuse   { fill: rgba(200, 60, 60, 0.10); stroke: #b03030; stroke-width: 1.8; }
      .grw-iam      { fill: rgba(70, 140, 200, 0.08); stroke: #3a6090; stroke-width: 1.5; }
      .grw-title    { font-size: 14px; font-weight: 700; fill: #222; }
      .grw-sub      { font-size: 11px; fill: #444; }
      .grw-list     { font-size: 10px; fill: #333; }
      .grw-arrow    { fill: none; stroke: #555; stroke-width: 1.8; }
      .grw-arrow-r  { fill: none; stroke: #b03030; stroke-width: 1.8; stroke-dasharray: 5 3; }
      .grw-arrow-a  { fill: none; stroke: rgb(46, 138, 90); stroke-width: 1.3; stroke-dasharray: 3 3; }
      .grw-label    { font-size: 11px; fill: #444; font-style: italic; }
      .grw-header   { font-size: 15px; font-weight: 700; fill: #222; }
    &lt;/style&gt;
    &lt;marker id=&quot;grw-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;6&quot; markerHeight=&quot;6&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;grw-arrow-r&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;6&quot; markerHeight=&quot;6&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#b03030&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;grw-arrow-a&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;5&quot; markerHeight=&quot;5&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;rgb(46, 138, 90)&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;40&quot; y=&quot;30&quot; class=&quot;grw-header&quot;&gt;The request pipeline&lt;/text&gt;

  &lt;!-- User --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;60&quot; width=&quot;110&quot; height=&quot;60&quot; rx=&quot;4&quot; class=&quot;grw-user&quot; /&gt;
  &lt;text x=&quot;95&quot; y=&quot;85&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;Customer&lt;/text&gt;
  &lt;text x=&quot;95&quot; y=&quot;103&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;chat message&lt;/text&gt;

  &lt;path d=&quot;M150,90 L195,90&quot; class=&quot;grw-arrow&quot; marker-end=&quot;url(#grw-arrow)&quot; /&gt;

  &lt;!-- IAM gate --&gt;
  &lt;rect x=&quot;195&quot; y=&quot;55&quot; width=&quot;110&quot; height=&quot;70&quot; rx=&quot;4&quot; class=&quot;grw-iam&quot; /&gt;
  &lt;text x=&quot;250&quot; y=&quot;80&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;IAM gate&lt;/text&gt;
  &lt;text x=&quot;250&quot; y=&quot;98&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;Lambda role:&lt;/text&gt;
  &lt;text x=&quot;250&quot; y=&quot;113&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;bedrock:InvokeModel&lt;/text&gt;

  &lt;path d=&quot;M305,90 L350,90&quot; class=&quot;grw-arrow&quot; marker-end=&quot;url(#grw-arrow)&quot; /&gt;

  &lt;!-- Input guardrail --&gt;
  &lt;rect x=&quot;350&quot; y=&quot;40&quot; width=&quot;200&quot; height=&quot;140&quot; rx=&quot;4&quot; class=&quot;grw-guard&quot; /&gt;
  &lt;text x=&quot;450&quot; y=&quot;62&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;Input guardrail&lt;/text&gt;
  &lt;text x=&quot;362&quot; y=&quot;84&quot; class=&quot;grw-list&quot;&gt;· Denied topics&lt;/text&gt;
  &lt;text x=&quot;362&quot; y=&quot;100&quot; class=&quot;grw-list&quot;&gt;  (regulated financial advice)&lt;/text&gt;
  &lt;text x=&quot;362&quot; y=&quot;116&quot; class=&quot;grw-list&quot;&gt;· Content filters&lt;/text&gt;
  &lt;text x=&quot;362&quot; y=&quot;132&quot; class=&quot;grw-list&quot;&gt;  (incl. prompt attacks: HIGH)&lt;/text&gt;
  &lt;text x=&quot;362&quot; y=&quot;148&quot; class=&quot;grw-list&quot;&gt;· PII in input&lt;/text&gt;
  &lt;text x=&quot;362&quot; y=&quot;164&quot; class=&quot;grw-list&quot;&gt;  (block if detected)&lt;/text&gt;

  &lt;!-- Refusal branch from input guardrail --&gt;
  &lt;path d=&quot;M450,180 L450,230 L210,230 L210,240&quot; class=&quot;grw-arrow-r&quot; marker-end=&quot;url(#grw-arrow-r)&quot; /&gt;
  &lt;rect x=&quot;140&quot; y=&quot;240&quot; width=&quot;140&quot; height=&quot;70&quot; rx=&quot;4&quot; class=&quot;grw-refuse&quot; /&gt;
  &lt;text x=&quot;210&quot; y=&quot;265&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;Refusal&lt;/text&gt;
  &lt;text x=&quot;210&quot; y=&quot;283&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;guardrail_intervened&lt;/text&gt;
  &lt;text x=&quot;210&quot; y=&quot;298&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;canned message to user&lt;/text&gt;

  &lt;path d=&quot;M550,110 L600,110&quot; class=&quot;grw-arrow&quot; marker-end=&quot;url(#grw-arrow)&quot; /&gt;

  &lt;!-- Model --&gt;
  &lt;rect x=&quot;600&quot; y=&quot;75&quot; width=&quot;140&quot; height=&quot;70&quot; rx=&quot;4&quot; class=&quot;grw-model&quot; /&gt;
  &lt;text x=&quot;670&quot; y=&quot;100&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;Claude Sonnet&lt;/text&gt;
  &lt;text x=&quot;670&quot; y=&quot;118&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;text generation&lt;/text&gt;
  &lt;text x=&quot;670&quot; y=&quot;133&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;via Bedrock&lt;/text&gt;

  &lt;path d=&quot;M740,110 L790,110&quot; class=&quot;grw-arrow&quot; marker-end=&quot;url(#grw-arrow)&quot; /&gt;

  &lt;!-- Output guardrail --&gt;
  &lt;rect x=&quot;790&quot; y=&quot;40&quot; width=&quot;200&quot; height=&quot;140&quot; rx=&quot;4&quot; class=&quot;grw-guard&quot; /&gt;
  &lt;text x=&quot;890&quot; y=&quot;62&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;Output guardrail&lt;/text&gt;
  &lt;text x=&quot;802&quot; y=&quot;84&quot; class=&quot;grw-list&quot;&gt;· Denied topics&lt;/text&gt;
  &lt;text x=&quot;802&quot; y=&quot;100&quot; class=&quot;grw-list&quot;&gt;  (re-check on generated text)&lt;/text&gt;
  &lt;text x=&quot;802&quot; y=&quot;116&quot; class=&quot;grw-list&quot;&gt;· Content filters&lt;/text&gt;
  &lt;text x=&quot;802&quot; y=&quot;132&quot; class=&quot;grw-list&quot;&gt;· PII anonymize&lt;/text&gt;
  &lt;text x=&quot;802&quot; y=&quot;148&quot; class=&quot;grw-list&quot;&gt;  (account #s -&amp;gt; [ACCOUNT])&lt;/text&gt;
  &lt;text x=&quot;802&quot; y=&quot;164&quot; class=&quot;grw-list&quot;&gt;· Grounding / relevance&lt;/text&gt;

  &lt;!-- Clean output back to user --&gt;
  &lt;path d=&quot;M890,180 L890,230 L95,230 L95,120&quot; class=&quot;grw-arrow&quot; marker-end=&quot;url(#grw-arrow)&quot; /&gt;
  &lt;text x=&quot;500&quot; y=&quot;225&quot; text-anchor=&quot;middle&quot; class=&quot;grw-label&quot;&gt;sanitised response back to the customer&lt;/text&gt;

  &lt;!-- Audit sinks --&gt;
  &lt;rect x=&quot;60&quot; y=&quot;360&quot; width=&quot;220&quot; height=&quot;90&quot; rx=&quot;4&quot; class=&quot;grw-audit&quot; /&gt;
  &lt;text x=&quot;170&quot; y=&quot;385&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;CloudTrail&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;405&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;InvokeModel events:&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;420&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;principal, model ARN, timestamp&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;438&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;(data events enabled)&lt;/text&gt;

  &lt;rect x=&quot;320&quot; y=&quot;360&quot; width=&quot;220&quot; height=&quot;90&quot; rx=&quot;4&quot; class=&quot;grw-audit&quot; /&gt;
  &lt;text x=&quot;430&quot; y=&quot;385&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;Invocation logging&lt;/text&gt;
  &lt;text x=&quot;430&quot; y=&quot;405&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;full prompt + response&lt;/text&gt;
  &lt;text x=&quot;430&quot; y=&quot;420&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;guardrail assessments&lt;/text&gt;
  &lt;text x=&quot;430&quot; y=&quot;438&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;S3 / CloudWatch Logs&lt;/text&gt;

  &lt;rect x=&quot;580&quot; y=&quot;360&quot; width=&quot;220&quot; height=&quot;90&quot; rx=&quot;4&quot; class=&quot;grw-audit&quot; /&gt;
  &lt;text x=&quot;690&quot; y=&quot;385&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;KMS customer-managed&lt;/text&gt;
  &lt;text x=&quot;690&quot; y=&quot;405&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;encryption at rest&lt;/text&gt;
  &lt;text x=&quot;690&quot; y=&quot;420&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;key policy limits readers&lt;/text&gt;
  &lt;text x=&quot;690&quot; y=&quot;438&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;disable-key revokes access&lt;/text&gt;

  &lt;rect x=&quot;840&quot; y=&quot;360&quot; width=&quot;220&quot; height=&quot;90&quot; rx=&quot;4&quot; class=&quot;grw-audit&quot; /&gt;
  &lt;text x=&quot;950&quot; y=&quot;385&quot; text-anchor=&quot;middle&quot; class=&quot;grw-title&quot;&gt;Athena / Logs Insights&lt;/text&gt;
  &lt;text x=&quot;950&quot; y=&quot;405&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;query invocation logs&lt;/text&gt;
  &lt;text x=&quot;950&quot; y=&quot;420&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;for audit pulls &amp;amp; investigations&lt;/text&gt;
  &lt;text x=&quot;950&quot; y=&quot;438&quot; text-anchor=&quot;middle&quot; class=&quot;grw-sub&quot;&gt;retention policy applied&lt;/text&gt;

  &lt;!-- Dashed audit arrows --&gt;
  &lt;path d=&quot;M250,125 L170,360&quot; class=&quot;grw-arrow-a&quot; marker-end=&quot;url(#grw-arrow-a)&quot; /&gt;
  &lt;path d=&quot;M670,145 L430,360&quot; class=&quot;grw-arrow-a&quot; marker-end=&quot;url(#grw-arrow-a)&quot; /&gt;
  &lt;path d=&quot;M890,180 L690,360&quot; class=&quot;grw-arrow-a&quot; marker-end=&quot;url(#grw-arrow-a)&quot; /&gt;
  &lt;path d=&quot;M540,410 L580,410&quot; class=&quot;grw-arrow-a&quot; marker-end=&quot;url(#grw-arrow-a)&quot; /&gt;
  &lt;path d=&quot;M800,410 L840,410&quot; class=&quot;grw-arrow-a&quot; marker-end=&quot;url(#grw-arrow-a)&quot; /&gt;

  &lt;!-- Footer --&gt;
  &lt;text x=&quot;550&quot; y=&quot;490&quot; text-anchor=&quot;middle&quot; class=&quot;grw-label&quot;&gt;Green dashed lines: metadata and payloads to audit sinks. Red dashed line: refusal short-circuit.&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;510&quot; text-anchor=&quot;middle&quot; class=&quot;grw-label&quot;&gt;Solid black lines: the happy-path request and response flow.&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;540&quot; text-anchor=&quot;middle&quot; class=&quot;grw-label&quot;&gt;Every allowed or refused invocation leaves three audit trails: CloudTrail event,&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;556&quot; text-anchor=&quot;middle&quot; class=&quot;grw-label&quot;&gt;invocation log (prompt + response + guardrail assessment), and the KMS-protected persistence layer.&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Guardrails enforce at two gates, input and output, around a single model call. CloudTrail, invocation logging, and KMS produce the three audit artefacts. Each layer does one job; removing any of them breaks a different piece of the compliance story.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-configuration-in-depth&quot;&gt;The configuration in depth&lt;/h3&gt;

&lt;p&gt;The Guardrail. Create one guardrail per application (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chatbot-customer-v1&lt;/code&gt;). The configuration, at a high level:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;chatbot-customer-v1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;blockedInputMessaging&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;I can&apos;t help with that request. For investment advice, please speak with a licensed advisor at 0800-...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;blockedOutputsMessaging&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;I can&apos;t share that response. Please contact support if you need more detail.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;topicPolicyConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;topicsConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RegulatedFinancialAdvice&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;definition&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Advice to buy, sell, or hold specific securities, or recommendations on investment strategy, asset allocation, retirement planning, or tax planning for a specific person.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;examples&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Should I invest in XYZ stock?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;What should I do with my 401k?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Is now a good time to buy bonds?&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DENY&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contentPolicyConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filtersConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SEXUAL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;outputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;VIOLENCE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;outputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HATE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;outputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;INSULTS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;     &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;MEDIUM&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;outputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;MEDIUM&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;MISCONDUCT&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;outputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PROMPT_ATTACK&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HIGH&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;outputStrength&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;NONE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;sensitiveInformationPolicyConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;piiEntitiesConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;CREDIT_DEBIT_CARD_NUMBER&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;BLOCK&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;US_BANK_ACCOUNT_NUMBER&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ANONYMIZE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;US_SOCIAL_SECURITY_NUMBER&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;BLOCK&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;EMAIL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ANONYMIZE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PHONE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ANONYMIZE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;NAME&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;                     &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ANONYMIZE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;regexesConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;InternalAccountId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;pattern&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ACCT-[A-Z0-9]{10}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ANONYMIZE&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A few points on that. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PROMPT_ATTACK&lt;/code&gt; is applied to input only (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;outputStrength: NONE&lt;/code&gt;) because what we’re catching is the &lt;em&gt;user’s&lt;/em&gt; attempt to jailbreak; it doesn’t make sense on output. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CREDIT_DEBIT_CARD_NUMBER&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BLOCK&lt;/code&gt; (blocks the whole invocation) because a card number in response is never acceptable; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;US_BANK_ACCOUNT_NUMBER&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ANONYMIZE&lt;/code&gt; because the chatbot &lt;em&gt;can&lt;/em&gt; reference “your account ending in 1234” legitimately by using the anonymized form. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regexesConfig&lt;/code&gt; catches the company’s internal &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCT-...&lt;/code&gt; identifier that isn’t in the built-in PII catalogue.&lt;/p&gt;

&lt;p&gt;Versioning. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreateGuardrailVersion&lt;/code&gt; snapshots the DRAFT into an immutable version. The Lambda invokes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;guardrailIdentifier=&amp;lt;id&amp;gt;, guardrailVersion=&amp;lt;N&amp;gt;&lt;/code&gt; pinning to a specific version; updates to the guardrail don’t affect production until the Lambda is updated to reference the new version. This is the change-control story: Legal reviews version 3, approves it, the Lambda is updated to reference version 3.&lt;/p&gt;

&lt;p&gt;Invocation logging. Enable via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PutModelInvocationLoggingConfiguration&lt;/code&gt; at the Region level:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;loggingConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;cloudWatchConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;logGroupName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/aws/bedrock/invocations&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;roleArn&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;arn:aws:iam::111122223333:role/BedrockLoggingRole&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;s3Config&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;bucketName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;fintech-bedrock-audit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;keyPrefix&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;chatbot/&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;textDataDeliveryEnabled&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;imageDataDeliveryEnabled&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;embeddingDataDeliveryEnabled&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Every invocation’s full request, response, model metadata, and guardrail assessment land in both sinks. S3 is archival (Athena queryable); CloudWatch Logs is real-time (Logs Insights queryable for incident response). Both are encrypted; the S3 bucket’s default encryption uses a customer-managed KMS key that only the audit team can grant &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kms:Decrypt&lt;/code&gt; on.&lt;/p&gt;

&lt;p&gt;CloudTrail data events. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; isn’t in management events by default, enable data events for Bedrock to capture each call’s principal, model ARN, and timestamp. Data events cost money per event but are the only way to get the “who called what” trail for high-volume model calls at the CloudTrail layer.&lt;/p&gt;

&lt;p&gt;IAM restriction. The chatbot Lambda’s execution role has exactly one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock:InvokeModel&lt;/code&gt; permission, scoped to the Claude Sonnet model ARN and requiring the guardrail:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Effect&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Allow&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;bedrock:InvokeModel&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Resource&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;arn:aws:bedrock:eu-west-1::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;arn:aws:bedrock:eu-west-1:111122223333:guardrail/chatbot-customer-v1&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Condition&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;StringEquals&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;bedrock:GuardrailIdentifier&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;arn:aws:bedrock:eu-west-1:111122223333:guardrail/chatbot-customer-v1&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That condition block is the key enforcement: the Lambda &lt;em&gt;cannot&lt;/em&gt; invoke the model without the guardrail attached. Even if a developer accidentally removed the guardrail reference in code, IAM would deny the call.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-refusal&quot;&gt;A worked refusal&lt;/h3&gt;

&lt;p&gt;A customer asks: “Hey, I’ve got 50k saved. Should I put it in index funds or high-yield savings?”&lt;/p&gt;

&lt;p&gt;The Lambda forwards the message to Bedrock with the guardrail attached:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ aws bedrock-runtime invoke-model \
    --model-id anthropic.claude-sonnet-4-5-20250929-v1:0 \
    --guardrail-identifier chatbot-customer-v1 \
    --guardrail-version 3 \
    --body &apos;{&quot;anthropic_version&quot;:&quot;bedrock-2023-05-31&quot;,&quot;max_tokens&quot;:500,&quot;messages&quot;:[{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;Hey, I have got 50k saved. Should I put it in index funds or high-yield savings?&quot;}]}&apos; \
    --cli-binary-format raw-in-base64-out \
    out.json

$ jq . out.json
{
  &quot;stopReason&quot;: &quot;guardrail_intervened&quot;,
  &quot;content&quot;: [
    {&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: &quot;I can&apos;t help with that request. For investment advice, please speak with a licensed advisor at 0800-...&quot;}
  ],
  &quot;amazon-bedrock-guardrailAction&quot;: &quot;INTERVENED&quot;,
  &quot;amazon-bedrock-trace&quot;: {
    &quot;guardrail&quot;: {
      &quot;inputAssessment&quot;: {
        &quot;chatbot-customer-v1&quot;: {
          &quot;topicPolicy&quot;: {
            &quot;topics&quot;: [
              {&quot;name&quot;: &quot;RegulatedFinancialAdvice&quot;, &quot;type&quot;: &quot;DENY&quot;, &quot;action&quot;: &quot;BLOCKED&quot;}
            ]
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What happened:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The input guardrail evaluated the message against the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RegulatedFinancialAdvice&lt;/code&gt; topic. The topic’s definition (“advice to buy, sell, or hold specific securities…”) plus the examples (“What should I do with my 401k?”) trained the topic classifier to recognise this phrasing.&lt;/li&gt;
  &lt;li&gt;The classifier flagged the input as matching. Guardrails short-circuited the invocation: the model was never called.&lt;/li&gt;
  &lt;li&gt;The response body contains the configured &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blockedInputMessaging&lt;/code&gt; plus the full assessment showing which policy fired.&lt;/li&gt;
  &lt;li&gt;The Lambda received this response with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stopReason: &quot;guardrail_intervened&quot;&lt;/code&gt; and rendered the configured refusal in the chat UI.&lt;/li&gt;
  &lt;li&gt;CloudTrail recorded the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; call. The invocation log wrote the full prompt, the refusal response, and the guardrail assessment to S3 under the audit bucket’s KMS key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The customer sees a polite refusal pointing them to a real human advisor. Compliance has an auditable record that the model was not invoked with that prompt, which is the stronger position than “the model was invoked and declined.”&lt;/p&gt;

&lt;h3 id=&quot;a-worked-pii-redaction&quot;&gt;A worked PII redaction&lt;/h3&gt;

&lt;p&gt;Customer asks: “Can you confirm the balance on my account ACCT-ABC1234567?”&lt;/p&gt;

&lt;p&gt;The Lambda has tool-use wired up: it calls an internal API to look up the balance, includes the result in the prompt context, and asks the model to produce a natural-language response. The model generates:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;The balance on account ACCT-ABC1234567 is $3,421.55 as of today.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The output guardrail evaluates. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InternalAccountId&lt;/code&gt; regex matches &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCT-ABC1234567&lt;/code&gt; with action &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ANONYMIZE&lt;/code&gt;. The returned content:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;The balance on account {ACCOUNT} is $3,421.55 as of today.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The application layer then looks at the original session context, confirms the customer is authenticated and authorised for that specific account, and renders “your account ending in 4567” in the UI. The guardrail doesn’t need to know which account number is OK to show which customer, it just ensures the raw internal identifier never reaches the rendered chat log. The application, which has the authz context, substitutes a friendly form.&lt;/p&gt;

&lt;p&gt;This is the key pattern: Guardrails enforce a &lt;em&gt;structural&lt;/em&gt; invariant (“no internal account IDs in output”); the application layer enforces &lt;em&gt;contextual&lt;/em&gt; authorisation (“this customer can see a reference to &lt;em&gt;their&lt;/em&gt; account”). The two compose.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Responsible AI on Bedrock is three layers, not one. Real-time enforcement (Guardrails), audit persistence (invocation logging + CloudTrail), and identity/encryption (IAM + KMS). All three are needed for a defensible compliance story.&lt;/li&gt;
  &lt;li&gt;Guardrails has five policy types. Denied topics, content filters (including prompt attacks), word filters, sensitive-information filters (PII + user regex), and contextual grounding/relevance. Each can apply to input, output, or both.&lt;/li&gt;
  &lt;li&gt;PII filters can block or anonymize. Block stops the invocation; anonymize replaces the matched text with a tag like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[EMAIL]&lt;/code&gt; or a user-defined placeholder. Choose per PII type: card numbers block, account references anonymize.&lt;/li&gt;
  &lt;li&gt;Guardrails are versioned and pinned per invocation. Create, version, reference a specific version in the invocation. Updates don’t affect production until the caller is updated. This is change control for model behaviour.&lt;/li&gt;
  &lt;li&gt;Model invocation logging captures the full payload. Prompt, response, guardrail assessment, metadata, to S3 or CloudWatch Logs, encrypted under a customer-managed KMS key. The durable audit artefact.&lt;/li&gt;
  &lt;li&gt;CloudTrail data events for Bedrock give the “who called what” trail. Not on by default. Pair with invocation logging for the full picture.&lt;/li&gt;
  &lt;li&gt;IAM conditions enforce guardrail usage. A policy that requires &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock:GuardrailIdentifier&lt;/code&gt; to equal a specific guardrail ARN makes it impossible to invoke the model without the guardrail, bypassing guardrails requires changing IAM, which has its own audit trail.&lt;/li&gt;
  &lt;li&gt;Guardrails enforce structure; the application enforces context. Guardrails keep raw account numbers out of output. The app layer, which has the authenticated session, decides which anonymized references to show which customer. The two compose; neither alone is sufficient.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A chatbot that refuses financial advice, redacts account numbers, and produces an audit trail a regulator would accept isn’t one feature, it’s five Bedrock features configured together, plus IAM and KMS around them. The craft is knowing which feature answers which compliance question, and wiring the configuration so no obvious bypass exists (no guardrail-less invocation path, no unencrypted log sink, no overly broad IAM). Get the composition right once and the chatbot is defensible; miss a layer and the auditor has a question with no good answer.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Before the Transformer</title>
    <link href="/writing/before-the-transformer/"/>
    <updated>2026-05-16T06:00:00+08:00</updated>
    <id>/writing/before-the-transformer/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Your phone just suggested the word “tomorrow” before you finished typing “see you to”. That suggestion didn’t come from a &lt;label for=&quot;sn-writing-before-the-transformer-transformer&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-before-the-transformer-transformer-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;transformer&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-before-the-transformer-transformer&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-before-the-transformer-transformer-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Transformer&lt;/span&gt;The neural network architecture that underpins modern LLMs – stacks of self-attention layers that let every token look at every other token in the context.
&lt;/span&gt;. It came from a &lt;label for=&quot;sn-writing-before-the-transformer-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-before-the-transformer-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-before-the-transformer-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-before-the-transformer-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; that fits in 50KB, runs in microseconds, and is older than the smartphone you’re holding.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A lot of working software still runs on the AI that came before the AI. This post is about that AI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;/writing/after-the-transformer/&quot;&gt;the previous post&lt;/a&gt; we looked at what might come after the transformer. This post goes the other way. Before BERT, before word2vec, before deep learning was the default, NLP ran on a small set of statistical and probabilistic models that did genuinely useful work, some of which they still do, today, in places where the cost or latency or interpretability of a transformer would be wrong.&lt;/p&gt;

&lt;p&gt;These aren’t museum pieces. They’re production tools. You should know about them because they’re often the correct answer, especially for problems with tight latency budgets, small datasets, or auditability requirements.&lt;/p&gt;

&lt;h3 id=&quot;n-gram-language-models&quot;&gt;n-gram language models&lt;/h3&gt;

&lt;p&gt;An n-gram model is a &lt;label for=&quot;sn-writing-before-the-transformer-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-before-the-transformer-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;language model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-before-the-transformer-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-before-the-transformer-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; in the most literal sense: it estimates the probability of the next word given the previous &lt;em&gt;n-1&lt;/em&gt; words.&lt;/p&gt;

&lt;p&gt;A bigram model (n=2) estimates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P(word | previous word)&lt;/code&gt;. “The cat sat on the ___”, given the model has seen “on the mat” enough times in training data, it estimates a high probability for “mat” given “the.” A trigram model uses two preceding words. A 5-gram model uses four.&lt;/p&gt;

&lt;p&gt;The model itself is just a giant table of counts: count how many times each n-gram appeared in your training corpus, divide by the count of the prefix, and that’s your probability estimate. No neural network. No gradient descent. No GPU. Just a hash table.&lt;/p&gt;

&lt;p&gt;This sounds laughably primitive in 2026. It’s also how Google’s mobile keyboard worked for years, how speech recognition worked for years, and how machine translation worked for years, and the n-gram model was state of the art at all three.&lt;/p&gt;

&lt;h4 id=&quot;why-n-gram-models-still-ship&quot;&gt;Why n-gram models still ship&lt;/h4&gt;

&lt;p&gt;Three reasons.&lt;/p&gt;

&lt;p&gt;First, they’re tiny. A 5-gram model trained on a few million words of domain-specific text fits in megabytes. It runs on a phone, on an embedded device, in a process that wakes up for one millisecond at a time.&lt;/p&gt;

&lt;p&gt;Second, they’re fast. Lookup is a single hash-table query. The latency is nanoseconds. There’s no model to load, no GPU to wait for.&lt;/p&gt;

&lt;p&gt;Third, they’re deterministic and auditable. If your spam filter or autocomplete makes a mistake, you can find out exactly which n-gram triggered the decision and which counts produced the probability. There’s no opaque &lt;label for=&quot;sn-writing-before-the-transformer-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-before-the-transformer-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embedding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-before-the-transformer-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-before-the-transformer-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; to introspect.&lt;/p&gt;

&lt;p&gt;The trade-off is severity: n-gram models can’t generalise beyond what they’ve literally seen. “The dog sat on the mat” might be a familiar pattern; “The aardvark sat on the mat” is brand new and the model has nothing useful to say. They suffer the sparsity problem, most plausible n-grams never appear in the training data at all, even with a large corpus.&lt;/p&gt;

&lt;p&gt;A lot of the cleverness in classical n-gram modelling went into smoothing techniques (Kneser-Ney, Good-Turing) that estimate plausible probabilities for n-grams the model never saw, by backing off to shorter n-grams. These methods are mature and well-understood, and they’re still the foundation of fast statistical models for autocompletion, predictive text, and parts of speech recognition pipelines.&lt;/p&gt;

&lt;h4 id=&quot;where-youll-find-them&quot;&gt;Where you’ll find them&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Mobile autocomplete and predictive text in keyboards that need to run offline.&lt;/li&gt;
  &lt;li&gt;Speech recognition language models, the acoustic part is now neural, but a fast n-gram language model is often the rescoring layer that picks between candidate transcriptions.&lt;/li&gt;
  &lt;li&gt;Spell checkers and grammar checkers, especially for languages where there isn’t a large neural model available.&lt;/li&gt;
  &lt;li&gt;Search query understanding for tail queries where you want a fast statistical signal, not a 200ms LLM round trip.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;hidden-markov-models&quot;&gt;Hidden Markov Models&lt;/h3&gt;

&lt;p&gt;A Hidden Markov Model (HMM) is the next conceptual rung up. It models a sequence of observations that are generated by an underlying sequence of &lt;em&gt;hidden states&lt;/em&gt;, where each state depends only on the previous state and each observation depends only on the current state.&lt;/p&gt;

&lt;p&gt;The classical example: part-of-speech tagging. The observation sequence is the words you can see. The hidden sequence is the part-of-speech tag for each word, noun, verb, adjective. The HMM models two things:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Transition probabilities: how likely is each tag to follow each other tag? (e.g. determiners are often followed by nouns)&lt;/li&gt;
  &lt;li&gt;Emission probabilities: how likely is each word to be generated by each tag? (e.g. “run” can be a noun or a verb, with different probabilities for each)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given a sentence, you find the most likely sequence of tags by running the Viterbi algorithm, a dynamic programming procedure that’s been the standard textbook example since the 1970s.&lt;/p&gt;

&lt;p&gt;HMMs were the dominant approach to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Part-of-speech tagging, until CRFs (next section) and then neural taggers replaced them.&lt;/li&gt;
  &lt;li&gt;Speech recognition acoustic modelling, until deep learning replaced them in the early 2010s.&lt;/li&gt;
  &lt;li&gt;Bioinformatics gene prediction, where they’re &lt;em&gt;still&lt;/em&gt; widely used because biology has structural assumptions that match HMMs well.&lt;/li&gt;
  &lt;li&gt;Chunking and shallow parsing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;why-hmms-still-ship&quot;&gt;Why HMMs still ship&lt;/h4&gt;

&lt;p&gt;Two reasons.&lt;/p&gt;

&lt;p&gt;First, biology. Genes have a structure that maps cleanly onto hidden states (intron, exon, promoter, terminator) and HMMs have decades of biological-tuning baked into them. Tools like HMMER for protein sequence analysis are everywhere in computational biology, and they’re not getting replaced by transformers any time soon.&lt;/p&gt;

&lt;p&gt;Second, speed and tractability for low-resource languages. Training a neural POS tagger requires a lot of labelled data and a lot of compute. Training an HMM tagger requires hundreds of labelled sentences and a laptop. For low-resource language pipelines, an HMM is often the actual production tool.&lt;/p&gt;

&lt;h3 id=&quot;conditional-random-fields&quot;&gt;Conditional Random Fields&lt;/h3&gt;

&lt;p&gt;A Conditional Random Field (CRF) is the more flexible cousin of the HMM. The idea: instead of modelling the joint probability of observations and hidden states (HMM-style, which makes strong independence assumptions), model the conditional probability of the hidden states given the observations directly.&lt;/p&gt;

&lt;p&gt;In practice this lets you incorporate arbitrary features, not just “the current word” but “is the current word capitalised?”, “does it end in -ing?”, “is the previous word ‘to’?”, “what’s the gazetteer match?”, without breaking the model’s mathematical structure. CRFs work by combining many weak features through learned weights, much like logistic regression for sequences.&lt;/p&gt;

&lt;p&gt;CRFs were the standard for sequence labelling tasks throughout the 2010s:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Named entity recognition (people, places, organisations, dates).&lt;/li&gt;
  &lt;li&gt;Information extraction from semi-structured text.&lt;/li&gt;
  &lt;li&gt;Slot filling in dialogue systems.&lt;/li&gt;
  &lt;li&gt;Biomedical entity tagging (gene names, drug names, diseases).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classical pipeline, hand-craft good features, train a CRF on labelled data, deploy, produced systems that ran on CPUs at thousands of sentences per second with high accuracy. Many production NER systems still run a CRF either as the primary tagger or as a final layer on top of a neural model.&lt;/p&gt;

&lt;h4 id=&quot;when-a-crf-still-wins&quot;&gt;When a CRF still wins&lt;/h4&gt;

&lt;p&gt;CRFs are a good answer when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You have moderate amounts of labelled data (say, 1k-10k sentences), enough to learn meaningful weights, not enough to fine-tune a transformer well.&lt;/li&gt;
  &lt;li&gt;You need high precision on a fixed set of labels, regulatory keyword matching, structured-record extraction, controlled vocabularies.&lt;/li&gt;
  &lt;li&gt;You need to explain decisions, which features contributed to which label.&lt;/li&gt;
  &lt;li&gt;Latency matters, a CRF tagger runs in microseconds per sentence. A transformer NER model runs in milliseconds.&lt;/li&gt;
  &lt;li&gt;You’re working in a specialised domain with idiosyncratic vocabulary, medical, legal, scientific. Hand-crafted features encode domain knowledge that a generic transformer doesn’t have.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;word-embeddings-the-bridge&quot;&gt;Word embeddings: the bridge&lt;/h3&gt;

&lt;p&gt;Between the n-gram era and the transformer era there was a brief but enormously influential phase where word embeddings became the primary research tool. Word2Vec (Mikolov et al., Google, 2013) and GloVe (Stanford, 2014) trained dense vectors for words that captured semantic relationships, the famous “king - man + woman = queen” arithmetic.&lt;/p&gt;

&lt;p&gt;These models are no longer state of the art, but their descendants live everywhere. Modern sentence embeddings (BGE, E5, see &lt;a href=&quot;/writing/the-other-transformers/&quot;&gt;The Other Transformers&lt;/a&gt;) are direct conceptual descendants. Many smaller production NLP systems still use word2vec-style embeddings as a fast feature backbone, sometimes feeding into a CRF or a small classifier rather than a transformer.&lt;/p&gt;

&lt;p&gt;If you’ve ever called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nltk.word_embeddings&lt;/code&gt; or used GloVe vectors as a baseline before reaching for a transformer, you’ve used this generation of model.&lt;/p&gt;

&lt;h3 id=&quot;a-decision-table&quot;&gt;A decision table&lt;/h3&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;If your task is…&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Reach for…&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Why not a transformer?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Predictive text on an offline device&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A 5-gram language model with smoothing&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Megabytes vs gigabytes; nanosecond latency&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Rescoring speech recognition hypotheses&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An n-gram LM&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Streaming + low latency requirements&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Predicting protein-coding regions in DNA&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A profile HMM (HMMER)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Decades of domain tuning; biological structure matches the model&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;POS tagging a low-resource language&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An HMM with a small labelled corpus&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;No transformer pre-training in that language&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Extracting drug names from clinical notes&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A CRF with hand-crafted features and a gazetteer&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;High precision; auditability; low latency on a CPU&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Building a chatbot&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A transformer LLM&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;n-grams and HMMs cannot generate fluent multi-turn text&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Understanding ambiguous, context-rich queries&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A transformer&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Classical models struggle with long-range context&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;The story of NLP often gets told as a march of progress where each new generation makes the previous one obsolete. The actual picture is more layered. n-gram models still suggest the next word on your phone, still rescore speech-recognition hypotheses, still run inside spell checkers because they fit in megabytes and answer in nanoseconds. HMMs still dominate computational biology because gene structure maps cleanly onto hidden states and decades of domain tuning don’t transfer to a transformer overnight. CRFs are still the right answer when you have a thousand labelled sentences, a regulated domain, and a need to explain every decision the system makes.&lt;/p&gt;

&lt;p&gt;Pre-transformer doesn’t mean obsolete. It means a different cost-benefit curve. The classical tools win where their curve dominates: on devices that can’t load a GPU, on languages without pre-training, on tasks that need to run in microseconds, on auditors who want to see the features and the weights. Reach for a transformer when you need the long-range context and the generative fluency. Reach for one of these when you don’t.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: Assumption Mapping</title>
    <link href="/writing/the-workshop-assumption-mapping/"/>
    <updated>2026-05-15T06:00:00+08:00</updated>
    <id>/writing/the-workshop-assumption-mapping/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Assumption Mapping ranks the beliefs underneath a plan by risk and evidence so you test the dangerous ones first, cheaply, before they’re baked into the code. Worked example: &lt;a href=&quot;/writing/assumption-mapping-testing-what-you-believe/&quot;&gt;Testing What You Believe&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;assumption-mapping&quot;&gt;Assumption Mapping&lt;/h3&gt;

&lt;p&gt;Assumption Mapping surfaces the beliefs hiding underneath a plan, plots them by how much evidence supports them and how badly the plan fails if they’re wrong, then picks a short list of assumptions to test before committing resources. Sometimes called the risk/evidence grid or the assumptions grid. A close cousin is hypothesis mapping (same shape, different labels). Popularised by David Bland as part of the &lt;em&gt;Testing Business Ideas&lt;/em&gt; canon, building on earlier work by Giff Constable, Tom Chi, and the broader Lean Startup community. The 2x2 layout of evidence against importance is the artefact most people mean when they say “assumptions workshop.”&lt;/p&gt;

&lt;p&gt;Bland’s canonical labels for the axes are &lt;em&gt;Important / Unimportant&lt;/em&gt; (vertical) and &lt;em&gt;Has evidence / No evidence&lt;/em&gt; (horizontal); the prioritised quadrant is top-left: important + no evidence = leap of faith. We use &lt;em&gt;“impact if wrong”&lt;/em&gt; on the vertical axis instead of &lt;em&gt;“important”&lt;/em&gt; because it forces the failure-mode question (&lt;em&gt;what breaks if this turns out to be false?&lt;/em&gt;) but the placement and the priority are the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; a facilitator, the product owner or initiative lead, one or two developers, a designer or researcher, and a business stakeholder. Four to six people, around 90 minutes.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; a populated 2x2 grid of named assumptions, and a short list of leap-of-faith assumptions in the top-left, each with a cheap test, an owner, a due date, and the result that would change the plan.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; you’re about to commit significant effort to a new product, feature, or initiative and want to separate the beliefs from the facts before you build. Not for low-risk work, awareness-raising without a decision on the table, or a plan the team can’t yet articulate (run &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt; or &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;Story Mapping&lt;/a&gt; first).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whats-it-for&quot;&gt;What’s It For&lt;/h3&gt;

&lt;p&gt;A team spends six weeks building a pause-and-resume flow for subscriptions. The flow ships. Adoption is low. The team investigates and discovers that subscribers don’t want to pause; they want to skip a week. Pause is a feature the product owner imagined subscribers needed, based on a conversation with two subscribers, one of whom was actually describing a skip. The team built the wrong thing, beautifully, for six weeks.&lt;/p&gt;

&lt;p&gt;The assumption that “subscribers want to pause” was never identified as an assumption; it was treated as a fact. Because nobody had named it as a belief, nobody thought to test it. Because nobody tested it, the whole six-week build rested on a guess that cost two weeks of user research to validate.&lt;/p&gt;

&lt;p&gt;This is the universal shape of the failure. Every plan is a stack of beliefs. Some of the beliefs are tested and solid; some are tested and wrong; some are untested and dangerous; and some are untested and cheap to recover from. A team that can’t see the difference treats all the beliefs the same way, which means they treat the dangerous untested ones like the solid tested ones, and they find out too late.&lt;/p&gt;

&lt;p&gt;Assumption Mapping exists to make the beliefs visible and to separate them by how much damage they do if wrong. The grid is the forcing function: you can’t pretend an untested belief is solid when you’re looking at a note in the top-left quadrant of a whiteboard everyone is standing in front of.&lt;/p&gt;

&lt;p&gt;Reach for it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You’re about to commit significant effort to a new product, feature, or initiative&lt;/li&gt;
  &lt;li&gt;The team is confident and you suspect the confidence is resting on beliefs that haven’t been checked&lt;/li&gt;
  &lt;li&gt;You’ve just finished an &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Map&lt;/a&gt;, &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;Story Map&lt;/a&gt;, or &lt;a href=&quot;/writing/the-workshop-business-model-canvas/&quot;&gt;Business Model Canvas&lt;/a&gt; and want to push on the underlying beliefs&lt;/li&gt;
  &lt;li&gt;A decision feels high-stakes and you haven’t separated the reversible assumptions from the irreversible ones&lt;/li&gt;
  &lt;li&gt;An initiative has stalled and you want to know whether to continue or pivot&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-its-not-for&quot;&gt;What It’s Not For&lt;/h3&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The work is small and low-risk enough that the session costs more than the work itself&lt;/li&gt;
  &lt;li&gt;You’ve already validated the key assumptions through recent user research or experiments&lt;/li&gt;
  &lt;li&gt;The team can’t yet articulate what they’re building (run &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt; or &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;Story Mapping&lt;/a&gt; first)&lt;/li&gt;
  &lt;li&gt;There’s no actual decision on the table (Assumption Mapping is a pre-commitment tool, not a general awareness-raising exercise)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop a session that’s already started if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The plan isn’t concrete enough for assumptions to attach to&lt;/li&gt;
  &lt;li&gt;The room is performing confidence and refusing to engage with the evidence question&lt;/li&gt;
  &lt;li&gt;The top-left quadrant is empty after twenty minutes; that’s not safety, that’s denial&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stopping and fixing the plan is not failure. Plotting assumptions about a plan that doesn’t exist is.&lt;/p&gt;

&lt;p&gt;The session has real costs to weigh against the benefits. What you get: hidden beliefs made visible and explicit; a short list of cheap experiments that de-risk the plan within a week; decisions to commit made with clear eyes (“we know what we don’t know”); an artefact (the grid) that can be revisited as tests come in and assumptions move right or get invalidated; a team that starts treating “we believe” and “we know” as different statements. What it costs: 6, 9 person-hours per session with 4, 6 people; the follow-up work of actually running the tests, without which the session is just a wall of colourful worries; discomfort, because the session is designed to make confident people uncertain and that is hard on teams that reward confidence; and a recurring cost, because the grid needs to be run before any significant commitment, not just once.&lt;/p&gt;

&lt;p&gt;The common failure modes are worth naming up front: the grid gets produced and then ignored because the team commits anyway; tests are scoped so large they become builds, defeating the point; the session becomes a generic worry exercise instead of focused assumption-testing; the team treats “we all agree this is true” as evidence, when agreement is not the same as evidence; one person dominates placement and the grid reflects their risk appetite, not the team’s.&lt;/p&gt;

&lt;h3 id=&quot;definitions--background&quot;&gt;Definitions &amp;amp; Background&lt;/h3&gt;

&lt;p&gt;The desirability / viability / feasibility lens. Every assumption tends to be one of three kinds:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Desirability: do customers want it? Will they choose it? Will they keep choosing it?&lt;/li&gt;
  &lt;li&gt;Viability: can we sustain a business doing it? Margins, churn, acquisition cost, regulation.&lt;/li&gt;
  &lt;li&gt;Feasibility: can we actually build it? Skills, time, infrastructure, integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tag each assumption with D / V / F before plotting. A leap-of-faith cluster on &lt;em&gt;desirability&lt;/em&gt; is a different intervention from one on &lt;em&gt;feasibility&lt;/em&gt;: D-leaps need customer interviews; V-leaps need spreadsheet modelling and small commercial tests; F-leaps need spikes. The grid plots all three the same way; the experiment design differs.&lt;/p&gt;

&lt;h3 id=&quot;inputs&quot;&gt;Inputs&lt;/h3&gt;

&lt;p&gt;Something concrete to test assumptions about. An &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Map&lt;/a&gt;, a &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;Story Map&lt;/a&gt;, a &lt;a href=&quot;/writing/the-workshop-business-model-canvas/&quot;&gt;Business Model Canvas&lt;/a&gt;, or a one-page product brief. The plan is what makes the assumptions findable; without a plan, the session produces generic worries instead of specific beliefs.&lt;/p&gt;

&lt;p&gt;You also need:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A 2x2 grid drawn on a wall or whiteboard, with evidence on the horizontal axis and impact-if-wrong on the vertical&lt;/li&gt;
  &lt;li&gt;Sticky notes and markers for silent generation&lt;/li&gt;
  &lt;li&gt;Wall space for clustering before plotting&lt;/li&gt;
  &lt;li&gt;Dot stickers (optional) for the prioritisation vote&lt;/li&gt;
  &lt;li&gt;A 90-minute slot with the right people in the room (see &lt;em&gt;Who’s Needed&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;What lands on the wall at the end:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A populated 2x2 grid with every named assumption placed in a quadrant. The top-left quadrant, high impact, no evidence, is what the session exists to surface; everything else is context for it.&lt;/li&gt;
  &lt;li&gt;A short list of leap-of-faith assumptions to test first, each with: the proposed test, the owner, the due date, and the result that would change the plan.&lt;/li&gt;
  &lt;li&gt;A list of “we already know” assumptions parked in the bottom-right, useful for new joiners reading later.&lt;/li&gt;
  &lt;li&gt;Open assumptions to escalate: ones the team can’t test because they depend on leadership decisions or external factors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Photograph the grid with every note readable and the quadrants clear before the notes come down.&lt;/p&gt;

&lt;p&gt;These outputs feed straight into:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt;: every impact on an Impact Map is an assumption about actor behaviour. Run Assumption Mapping on an Impact Map and the whole middle column becomes testable.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-business-model-canvas/&quot;&gt;Business Model Canvas&lt;/a&gt;: a Canvas is nine boxes of assumptions. Assumption Mapping is the natural follow-up, especially on Revenue Streams and Cost Structure.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt;: the release-1 slice of a Story Map rests on assumptions about what users actually need. Running Assumption Mapping on the slice tells you which tasks to validate before building.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-jobs-to-be-done/&quot;&gt;Jobs to be Done&lt;/a&gt;: switch interviews surface beliefs about why customers hire (or fire) a product. The desirability assumptions on the grid, the ones that sit in the top-left because nobody has actually asked, are exactly what a JTBD interview round is designed to test.&lt;/li&gt;
  &lt;li&gt;Wardley Mapping: Wardley Mapping surfaces assumptions about component evolution and competitive position that Assumption Mapping can then test.&lt;/li&gt;
  &lt;li&gt;Threat Modelling: Threat Modelling surfaces security assumptions (&lt;em&gt;“we assume the auth token can’t be forged”&lt;/em&gt;) that belong on the grid the same way product assumptions do.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whos-needed&quot;&gt;Who’s Needed&lt;/h3&gt;

&lt;p&gt;Four to six people, around 90 minutes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Facilitator. Runs the clock, moderates placement debates on the grid, intervenes when “evidence” drifts into “opinion.”&lt;/li&gt;
  &lt;li&gt;Product owner or initiative lead. Mandatory. They made most of the assumptions, consciously or not, and they’ll be the one deciding which tests to fund.&lt;/li&gt;
  &lt;li&gt;Developers. At least one, ideally two. They’ll catch the technical assumptions the business-side people don’t know to question: integration feasibility, scale limits, data availability.&lt;/li&gt;
  &lt;li&gt;Designers and researchers. They’ll catch the user-behaviour assumptions and, critically, they’ll know which of the “we know subscribers want X” claims have actually been researched and which are folklore.&lt;/li&gt;
  &lt;li&gt;Business stakeholders. Someone who can talk about pricing, margin, market, and competitive assumptions. Without them, the grid is thin on the commercial side, which is often where the dangerous assumptions live.&lt;/li&gt;
  &lt;li&gt;Operations / SRE (Site Reliability Engineering). For technical initiatives (migrations, platform rewrites, reliability projects) ops carries the assumptions about production behaviour that the feature team doesn’t know. &lt;em&gt;“We assume we can cut over with no more than five minutes of downtime”&lt;/em&gt; is a foundational assumption on a migration, and only the on-call engineer knows what it would actually take to test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Assumption Mapping is a debate room. Fewer than four and you lose productive disagreement; more than six and the placement arguments on the grid take longer than the session.&lt;/p&gt;

&lt;p&gt;Who to leave out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;People who weren’t involved in making the plan. They don’t hold the assumptions. Their presence produces abstract concerns instead of the specific beliefs you’re trying to surface.&lt;/li&gt;
  &lt;li&gt;Large stakeholder groups. If seven people need to weigh in, run a pre-session with them to agree the assumption list, then run the mapping session with the smaller group.&lt;/li&gt;
  &lt;li&gt;Observers. Same rule as the other workshops: observers warp the room.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;how-to-run-it&quot;&gt;How To Run It&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Duration&lt;/th&gt;
      &lt;th&gt;Materials&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Orient on the plan&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;Plan artefact visible&lt;/td&gt;
      &lt;td&gt;“What are we testing the assumptions of?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Generate assumptions&lt;/td&gt;
      &lt;td&gt;20 min&lt;/td&gt;
      &lt;td&gt;Yellow notes, silent&lt;/td&gt;
      &lt;td&gt;“What has to be true for this plan to work?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Share and cluster&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;Wall space&lt;/td&gt;
      &lt;td&gt;“Which of these are the same belief?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Plot on the grid&lt;/td&gt;
      &lt;td&gt;25 min&lt;/td&gt;
      &lt;td&gt;2x2 grid&lt;/td&gt;
      &lt;td&gt;“How much evidence? What breaks if we’re wrong?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Prioritise testing&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;Dot votes or marks&lt;/td&gt;
      &lt;td&gt;“Which do we test first, and how?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Wrap-up, owners&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;,&lt;/td&gt;
      &lt;td&gt;“Who owns which test, and by when?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;~90 minutes&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The 2x2 grid has evidence on the horizontal axis (left is “no evidence, we’re guessing”; right is “strong evidence, we’ve tested this”) and impact on the vertical axis (bottom is “low impact if wrong”; top is “high impact if wrong, the whole plan fails”). Quadrants:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Top-left: Test these first: high impact, no evidence. The dangerous ones.&lt;/li&gt;
  &lt;li&gt;Top-right: Monitor: high impact, but we have evidence. Keep watching.&lt;/li&gt;
  &lt;li&gt;Bottom-left: Test if time allows: low impact, no evidence. Not urgent.&lt;/li&gt;
  &lt;li&gt;Bottom-right: Known: low impact, strong evidence. Stop worrying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The top-left quadrant is where the session earns its keep. Everything else is context for it.&lt;/p&gt;

&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 760 540&quot; style=&quot;max-width: 100%; height: auto; display: block; margin: 1.5rem auto;&quot; role=&quot;img&quot; aria-label=&quot;The assumption-mapping 2x2 grid. Vertical axis: impact if wrong (high at top, low at bottom). Horizontal axis: evidence we have (none on the left, strong on the right). Top-left quadrant is highlighted as &apos;Test these first, the leap of faith&apos;. Top-right is &apos;Monitor&apos;. Bottom-left is &apos;Test if time allows&apos;. Bottom-right is &apos;Known, stop worrying&apos;.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .am-axis { stroke: #1B1916; stroke-width: 1.8; fill: none; }
      .am-grid { stroke: #1B1916; stroke-width: 1; fill: none; opacity: 0.4; }
      .am-leap-bg { fill: #C85A1F; opacity: 0.08; }
      .am-q-label { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 17px; font-weight: 700; fill: #1B1916; }
      .am-q-leap { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 19px; font-weight: 700; fill: #C85A1F; }
      .am-q-sub { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 13px; fill: #4a4540; font-style: italic; }
      .am-axis-title { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 13px; font-weight: 700; fill: #1B1916; letter-spacing: 0.05em; text-transform: uppercase; }
      .am-axis-end { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 12px; fill: #4a4540; }
    &lt;/style&gt;
  &lt;/defs&gt;

  &lt;rect x=&quot;120&quot; y=&quot;60&quot; width=&quot;290&quot; height=&quot;200&quot; class=&quot;am-leap-bg&quot; /&gt;

  &lt;rect x=&quot;120&quot; y=&quot;60&quot; width=&quot;580&quot; height=&quot;400&quot; class=&quot;am-axis&quot; /&gt;
  &lt;line x1=&quot;410&quot; y1=&quot;60&quot; x2=&quot;410&quot; y2=&quot;460&quot; class=&quot;am-grid&quot; /&gt;
  &lt;line x1=&quot;120&quot; y1=&quot;260&quot; x2=&quot;700&quot; y2=&quot;260&quot; class=&quot;am-grid&quot; /&gt;

  &lt;text x=&quot;265&quot; y=&quot;130&quot; text-anchor=&quot;middle&quot; class=&quot;am-q-leap&quot;&gt;Test these first&lt;/text&gt;
  &lt;text x=&quot;265&quot; y=&quot;152&quot; text-anchor=&quot;middle&quot; class=&quot;am-q-sub&quot;&gt;the leap of faith&lt;/text&gt;

  &lt;text x=&quot;555&quot; y=&quot;130&quot; text-anchor=&quot;middle&quot; class=&quot;am-q-label&quot;&gt;Monitor&lt;/text&gt;
  &lt;text x=&quot;555&quot; y=&quot;152&quot; text-anchor=&quot;middle&quot; class=&quot;am-q-sub&quot;&gt;we believe it; keep watching&lt;/text&gt;

  &lt;text x=&quot;265&quot; y=&quot;335&quot; text-anchor=&quot;middle&quot; class=&quot;am-q-label&quot;&gt;Test if time allows&lt;/text&gt;
  &lt;text x=&quot;265&quot; y=&quot;357&quot; text-anchor=&quot;middle&quot; class=&quot;am-q-sub&quot;&gt;cheap to verify, low cost if wrong&lt;/text&gt;

  &lt;text x=&quot;555&quot; y=&quot;335&quot; text-anchor=&quot;middle&quot; class=&quot;am-q-label&quot;&gt;Known&lt;/text&gt;
  &lt;text x=&quot;555&quot; y=&quot;357&quot; text-anchor=&quot;middle&quot; class=&quot;am-q-sub&quot;&gt;stop worrying&lt;/text&gt;

  &lt;text x=&quot;55&quot; y=&quot;260&quot; text-anchor=&quot;middle&quot; transform=&quot;rotate(-90 55 260)&quot; class=&quot;am-axis-title&quot;&gt;Impact if wrong&lt;/text&gt;
  &lt;text x=&quot;100&quot; y=&quot;75&quot; text-anchor=&quot;end&quot; class=&quot;am-axis-end&quot;&gt;High&lt;/text&gt;
  &lt;text x=&quot;100&quot; y=&quot;455&quot; text-anchor=&quot;end&quot; class=&quot;am-axis-end&quot;&gt;Low&lt;/text&gt;

  &lt;text x=&quot;410&quot; y=&quot;500&quot; text-anchor=&quot;middle&quot; class=&quot;am-axis-title&quot;&gt;Evidence we have&lt;/text&gt;
  &lt;text x=&quot;120&quot; y=&quot;480&quot; text-anchor=&quot;start&quot; class=&quot;am-axis-end&quot;&gt;None&lt;/text&gt;
  &lt;text x=&quot;700&quot; y=&quot;480&quot; text-anchor=&quot;end&quot; class=&quot;am-axis-end&quot;&gt;Strong&lt;/text&gt;
&lt;/svg&gt;

&lt;h4 id=&quot;silent-then-loud&quot;&gt;Silent then loud&lt;/h4&gt;

&lt;p&gt;Assumption Mapping alternates between silent generation and open debate. The shape matters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Generation is silent because talking first produces groupthink. One confident voice saying &lt;em&gt;“obviously subscribers want this”&lt;/em&gt; suppresses the three people who would have written assumption notes about it.&lt;/li&gt;
  &lt;li&gt;Sharing is round-the-room so every person reads their notes aloud, even when several are duplicates. Duplicates are valuable; they tell you which assumptions are shared across the room and which are one person’s worry.&lt;/li&gt;
  &lt;li&gt;Plotting is loud on purpose. The grid placement debate is where the session earns its cost. &lt;em&gt;“That’s low-impact”&lt;/em&gt; / &lt;em&gt;“No it isn’t, if that’s wrong the whole plan dies”&lt;/em&gt; is the conversation you came to have.&lt;/li&gt;
  &lt;li&gt;Prioritising is decisive. The facilitator’s job at the end is to force commitment: each top-left assumption gets a test, an owner, and a date, or it doesn’t leave the room.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key rhythm is write silently, share completely, argue loudly, commit sharply.&lt;/p&gt;

&lt;h4 id=&quot;phase-1-orient-on-the-plan-10-minutes&quot;&gt;Phase 1: Orient on the plan (10 minutes)&lt;/h4&gt;

&lt;p&gt;Put the plan artefact where everyone can see it. The Impact Map, the Story Map, the Canvas, or a printed one-page brief. If there’s no artefact, write a one-paragraph description on a flip chart. Then read it aloud:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Here’s the plan we’re putting under pressure today. Not whether the plan is right. Whether the beliefs underneath it are true. Our job is to find the assumptions this plan is standing on, plot them, and decide which ones to test before we commit further.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then frame the session:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“The point isn’t to debunk the plan; it’s to find the parts where we’ve been treating beliefs as facts. By the end of ninety minutes we’ll have a short list of beliefs worth testing in the next week. If the beliefs survive the tests, we commit harder. If they don’t, we’ve saved ourselves a month of building the wrong thing.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This matters. Teams often arrive defensive. Framing the session as &lt;em&gt;finding the beliefs&lt;/em&gt; rather than &lt;em&gt;attacking the plan&lt;/em&gt; gets you the surfacing you need.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Defensive framing. The product owner hears “pressure-test the plan” as “attack the plan.” Reframe: &lt;em&gt;“This session exists because we take this plan seriously. We wouldn’t bother putting a plan we didn’t care about under this much pressure.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;No concrete plan. If the artefact is actually “we want to grow the business,” the session cannot run. Schedule &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt; or &lt;a href=&quot;/writing/the-workshop-business-model-canvas/&quot;&gt;Business Model Canvas&lt;/a&gt; first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-2-generate-assumptions-20-minutes&quot;&gt;Phase 2: Generate assumptions (20 minutes)&lt;/h4&gt;

&lt;p&gt;Hand out sticky notes and markers. Set a timer for fifteen minutes. Give the one instruction:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Write silently. One assumption per note. Use the framing ‘We believe that…’ or ‘We assume that…’. For example, ‘We believe subscribers want to pause their box when they go on holiday.’ Or ‘We assume we can hire a second developer by June.’ Don’t hold back. Half-formed beliefs are exactly what we’re here for. I’d rather you write thirty notes and we throw ten away than write ten and miss twenty.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Prompt with categories if the room gets stuck:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“User beliefs: what do we assume subscribers want, or how we assume they’ll behave? Technical beliefs: what do we assume we can build, integrate with, or scale to? Business beliefs: pricing, margins, costs, churn, suppliers. Team beliefs: who we’ll hire, what the team can learn, how fast we can move. Market beliefs: competitors, regulations, timing.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Silent writing for fifteen minutes. No talking. You’re looking for 15 to 30 assumptions from a 4 to 6 person room. Fewer than 15 and people are being cautious; more than 40 and you have a clustering problem in phase 3.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Assumptions framed as facts. &lt;em&gt;“Subscribers want a weekly delivery.”&lt;/em&gt; Someone writes that as a statement of truth. Challenge at the share: &lt;em&gt;“How do we know that? Have we asked? Who? When?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Too few assumptions. Push at the ten-minute mark: &lt;em&gt;“What about pricing? Timing? Team capacity? Competitors? Regulations? Failure modes? What assumption would embarrass us most if it turned out to be wrong?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Risks dressed as assumptions. &lt;em&gt;“The API might be slow.”&lt;/em&gt; That’s a risk. The assumption is &lt;em&gt;“We assume the API is fast enough for our load.”&lt;/em&gt; Reframe as you share.&lt;/li&gt;
  &lt;li&gt;Someone not writing. They may be overthinking or stuck. Quiet prompt: &lt;em&gt;“What’s the thing you’re most worried about in this plan? Write that down. It counts.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Deployment and reliability assumptions. For technical plans, the silent writing should produce notes like &lt;em&gt;“We assume we can cut over in a five-minute maintenance window,”&lt;/em&gt; &lt;em&gt;“We assume our canary (a small percentage of traffic routed to the new version before the rollout goes wide) is sensitive enough to catch regressions,”&lt;/em&gt; &lt;em&gt;“We assume we can roll back the migration cleanly if it fails.”&lt;/em&gt; These are foundational and often unwritten.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-3-share-and-cluster-15-minutes&quot;&gt;Phase 3: Share and cluster (15 minutes)&lt;/h4&gt;

&lt;p&gt;Go round the room. Each person reads their assumptions aloud, one at a time, and places them on a blank section of the wall, not the grid yet. As notes go up, cluster similar assumptions physically together.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“As you read yours, if one of mine feels like the same belief, say so and we’ll stack them. If it’s close but distinct, we keep both.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Clustering is a light touch, not a merge. &lt;em&gt;“Subscribers will pay our headline price”&lt;/em&gt; and &lt;em&gt;“Our pricing is competitive”&lt;/em&gt; are related but test differently; keep both. &lt;em&gt;“Subscribers want weekly delivery”&lt;/em&gt; and &lt;em&gt;“Subscribers prefer weekly over fortnightly”&lt;/em&gt; are the same belief; stack them.&lt;/p&gt;

&lt;p&gt;Remove exact duplicates. Resist the urge to rewrite notes for clarity; the exact wording often carries the specific concern that made someone write it.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Dismissing assumptions too quickly. Someone says &lt;em&gt;“oh, we know that’s true”&lt;/em&gt; about an untested belief. Challenge: &lt;em&gt;“What evidence? If the answer is ‘it’s obvious,’ that’s not evidence.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Long debates about wording. Pick one phrasing and move on. The placement on the grid matters more than the exact text.&lt;/li&gt;
  &lt;li&gt;Clustering too aggressively. If you merge too many assumptions, you lose nuance. Keep clusters small: two or three notes maximum per cluster.&lt;/li&gt;
  &lt;li&gt;The “we already know” trap. The team dismisses half the assumptions as known. For each dismissed one, ask: &lt;em&gt;“If I asked the CEO the same question, would they give the same answer? What about a new team member?”&lt;/em&gt; If the answer isn’t confidently yes, it’s not as known as it feels.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-4-plot-on-the-grid-25-minutes&quot;&gt;Phase 4: Plot on the grid (25 minutes)&lt;/h4&gt;

&lt;p&gt;Move to the 2x2 grid. Take each assumption (or cluster) and place it on the grid. For each one, the team debates:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“How much evidence do we actually have for this belief? Not ‘it feels true’: what concrete evidence? User research? Past experiments? Existing data? Or are we guessing?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“If we’re wrong about this, what happens? Do we adjust a feature, or does the plan fall apart?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Place the note where the debate settles. Exact position on the grid doesn’t matter; &lt;em&gt;quadrant&lt;/em&gt; matters.&lt;/p&gt;

&lt;p&gt;This phase produces the most valuable conversations in the session. Disagreement is productive; it reveals different levels of confidence across the team. When two people disagree about whether an assumption is high or low impact, they’re disagreeing about what the plan actually is.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Everything in the top-left. If every assumption lands in high-impact-no-evidence, the team is either being dramatic or the plan really is that risky. Look for assumptions that can move right with minimal testing, and look for assumptions that are actually lower-impact than they feel.&lt;/li&gt;
  &lt;li&gt;Nothing in the top-left. If nothing is high-impact-untested, the team is overconfident. Challenge the top-right items: &lt;em&gt;“Is that really evidence, or is that a strong opinion?”&lt;/em&gt; Push assumptions left until the team flinches.&lt;/li&gt;
  &lt;li&gt;Arguing about exact placement. &lt;em&gt;“Is it at 60% or 70% on the evidence axis?”&lt;/em&gt; Interrupt: &lt;em&gt;“The grid isn’t precise. Which quadrant? Pick.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Silent placement. If people are placing notes without discussion, slow down: &lt;em&gt;“Why does that belong in the top-right? What’s our evidence? Let me hear it.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;The compound assumption. &lt;em&gt;“We assume subscribers want to pause, and that they’ll pay more for the feature, and that we can build it in two weeks.”&lt;/em&gt; That’s three assumptions. Split them; each one plots differently.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-5-prioritise-testing-10-minutes&quot;&gt;Phase 5: Prioritise testing (10 minutes)&lt;/h4&gt;

&lt;p&gt;Focus on the top-left quadrant. These are your leap-of-faith assumptions: high impact, low evidence. The ones that could sink the plan.&lt;/p&gt;

&lt;p&gt;For each assumption in the top-left, briefly discuss:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“How could we test this cheaply and quickly? Not a full build. A landing page, a prototype, a handful of interviews, a manual version of the feature. What’s the cheapest thing we could do in the next week that would tell us something?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Who owns running the test? When do we want the answer?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What result would change the plan?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have dot stickers, give each person three dots and vote on which top-left assumptions to test first. The ones with the most dots are the immediate priorities.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Tests that are really full builds. &lt;em&gt;“We’ll test whether subscribers want it by building it.”&lt;/em&gt; That’s not a test; that’s the commitment you’re trying to avoid. Push for smaller experiments: interviews, landing pages, manual concierge versions (a manually-delivered version of the service that proves the demand without building the software), prototypes, five-person usability studies.&lt;/li&gt;
  &lt;li&gt;No owner. Every assumption in the top-left needs a person and a date by the end of the session. &lt;em&gt;“We should test this”&lt;/em&gt; without an owner means it won’t happen.&lt;/li&gt;
  &lt;li&gt;Cherry-picking. The team picks the interesting tests and skips the boring but important ones. Hold firm: &lt;em&gt;“The dot vote selects the order, not a different set. We work through the top-left systematically.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Tests too big to start this week. If the proposed test is a two-month research project, it’s not an experiment, it’s another commitment. Push: &lt;em&gt;“What’s the smallest slice of that research we could run this week?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;a-worked-example&quot;&gt;A worked example&lt;/h4&gt;

&lt;p&gt;See &lt;a href=&quot;/writing/assumption-mapping-testing-what-you-believe/&quot;&gt;Assumption Mapping: Testing What You Believe&lt;/a&gt; for the Greenbox team’s first session, including the moment an assumption that felt obvious turned out to be a guess, and the one-week experiment that saved a month of wrong work.&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What Can Go Wrong&lt;/h3&gt;

&lt;p&gt;The optimist. Someone insists nothing is risky because &lt;em&gt;“it’s going to work.”&lt;/em&gt;
  &lt;em&gt;Recovery:&lt;/em&gt; Anchor to evidence: &lt;em&gt;“I’m not asking whether you believe it’ll work. I’m asking what evidence we have. Those are different questions.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; They can’t engage with the evidence question. They’re not participating in the session, they’re performing confidence.&lt;/p&gt;

&lt;p&gt;The pessimist. Someone puts everything in the top-left.
  &lt;em&gt;Recovery:&lt;/em&gt; Calibrate: &lt;em&gt;“If this assumption is wrong, what specifically breaks? Does the plan fail, or do we just adjust?”&lt;/em&gt; Force them to articulate the failure mode for each one.
  &lt;em&gt;Stop if:&lt;/em&gt; The plan really is as fragile as they think. That’s a finding; escalate it rather than finishing the mapping.&lt;/p&gt;

&lt;p&gt;The tangent. The team starts solving a problem they’ve found instead of finishing the map.
  &lt;em&gt;Recovery:&lt;/em&gt; Time-box: &lt;em&gt;“Great catch. Capture the test you’d run, put it next to the note, keep plotting. We’ll prioritise solutions after we see the full grid.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The tangent reveals the whole plan is wrong. Pause the session and escalate.&lt;/p&gt;

&lt;p&gt;The too-many-assumptions problem. The wall has thirty-five notes and the grid is becoming unreadable.
  &lt;em&gt;Recovery:&lt;/em&gt; Pre-plot prioritise: dot-vote on the fifteen most important assumptions to plot. The rest go into a holding area for the next session or for asynchronous review.
  &lt;em&gt;Stop if:&lt;/em&gt; The team can’t agree which fifteen matter most. That’s its own finding; the plan has no spine yet.&lt;/p&gt;

&lt;p&gt;The “we already know” trap. The team dismisses most assumptions as known.
  &lt;em&gt;Recovery:&lt;/em&gt; Challenge each “known” with a specific test: &lt;em&gt;“If I asked a new hire the same question tomorrow, would they give the same answer? If I asked three different customers?”&lt;/em&gt; Most “known” assumptions fail this test.
  &lt;em&gt;Stop if:&lt;/em&gt; The team won’t engage with the challenge. They’re overconfident and the session won’t persuade them; the findings will come from production.&lt;/p&gt;

&lt;p&gt;The political no-go assumption. Someone writes an assumption that implicitly challenges a decision made above the team’s level.
  &lt;em&gt;Recovery:&lt;/em&gt; Plot it honestly. Note it as “owned by leadership” and flag it for escalation rather than testing within the team.
  &lt;em&gt;Stop if:&lt;/em&gt; Plotting the assumption will cause a political crisis the session can’t contain. Take the note privately to the product owner and handle it offline.&lt;/p&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;

&lt;p&gt;The session ends; the work begins.&lt;/p&gt;

&lt;p&gt;Same day, the facilitator:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Photographs the grid with all notes placed. Make sure each note is readable and the quadrants are clear.&lt;/li&gt;
  &lt;li&gt;Transcribes the top-left assumptions into a shared document with: the assumption, the proposed test, the owner, the due date, and the result that would change the plan.&lt;/li&gt;
  &lt;li&gt;Sends the photos and the top-left list to all participants and to whoever else needs to see it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This week, the product owner:&lt;/p&gt;

&lt;p&gt;This is where the pattern earns its cost, and the work is mostly the product owner’s. The grid is worthless without the follow-up.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fund the tests. Each top-left test needs time, possibly budget, possibly access to users. The product owner’s first job is to make sure the tests actually run next week, not next month.&lt;/li&gt;
  &lt;li&gt;Run the tests fast. Days, not weeks. If a test is taking more than a week, it’s too elaborate; shrink it. An imperfect answer now is worth more than a perfect answer in a month.&lt;/li&gt;
  &lt;li&gt;Share early results. Even preliminary findings matter. An assumption that’s clearly wrong is worth knowing before the next planning session.&lt;/li&gt;
  &lt;li&gt;Update the grid. As test results come in, move assumptions from left to right on the grid (evidence accumulating) or kill them entirely (invalidated). The grid is a living artefact.&lt;/li&gt;
  &lt;li&gt;Use the grid to gate commitments. Before any significant hire, contract, or build decision, the product owner checks: are we betting on something in the top-left that we haven’t tested yet? If yes, the commitment waits.&lt;/li&gt;
  &lt;li&gt;Escalate irreversible assumptions. Some assumptions in the top-left can’t be tested by the team; they depend on leadership decisions or external factors. Walk them explicitly to the people who can answer them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ongoing, the team:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Re-runs the grid when the plan changes significantly. New impacts, new deliverables, new team members: each changes the assumption set.&lt;/li&gt;
  &lt;li&gt;Keeps the photographed grid visible where planning happens. It’s the reminder that the team is betting on beliefs, not facts.&lt;/li&gt;
  &lt;li&gt;Builds the language into daily conversation. &lt;em&gt;“Is that a belief or a known?”&lt;/em&gt; becomes a useful question in standups, reviews, and planning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;variants&quot;&gt;Variants&lt;/h3&gt;

&lt;p&gt;Initiative Level (default). A single product, feature, or initiative about to take significant commitment. Ninety minutes, four to six people, one populated grid, a short list of leap-of-faith tests with owners and dates. This is what most teams need, and the rest of this post describes it.&lt;/p&gt;

&lt;p&gt;Canvas-driven. Run Assumption Mapping directly off a &lt;a href=&quot;/writing/the-workshop-business-model-canvas/&quot;&gt;Business Model Canvas&lt;/a&gt;. Each of the nine boxes generates assumptions; the Revenue Streams and Cost Structure boxes typically dominate the top-left. Use this when you’ve just produced a Canvas and want to know which boxes to validate before raising or committing.&lt;/p&gt;

&lt;p&gt;Impact-Map-driven. Take an &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Map&lt;/a&gt; and treat every actor-impact-deliverable line as a chain of assumptions. Each &lt;em&gt;impact&lt;/em&gt; is a behaviour-change belief; each &lt;em&gt;deliverable&lt;/em&gt; is a viability/feasibility belief. The &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;Story Mapping&lt;/a&gt; release-1 slice variant is similar: assumption-map only the slice you’re about to build.&lt;/p&gt;

&lt;p&gt;Remote. Miro or Mural board with a pre-drawn 2x2 grid and a clearly marked silent-generation area. Slightly slower than in-person plotting because the grid debate moves at the pace of one shared cursor, but it transfers cleanly. Have the facilitator place notes on prompts from the participants to keep the layout legible.&lt;/p&gt;

&lt;p&gt;Pre-mortem hybrid. Add a pre-mortem prompt at the start of phase 2: &lt;em&gt;“Imagine the plan failed catastrophically a year from now. What were the assumptions that turned out to be wrong?”&lt;/em&gt; This produces a different kind of assumption (failure-mode beliefs) and is worth the extra fifteen minutes when the plan is large or irreversible.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Can You Turn Back Time?</title>
    <link href="/writing/can-you-turn-back-time/"/>
    <updated>2026-05-14T06:00:00+08:00</updated>
    <id>/writing/can-you-turn-back-time/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;&lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;Time Is Weirder Than You Think&lt;/a&gt; showed how time bends near mass and motion. &lt;a href=&quot;/writing/does-time-even-exist/&quot;&gt;Does Time Even Exist?&lt;/a&gt; asked the deeper question of whether it exists at all. This post asks a narrower one: can you move through it in the wrong direction? The answer, according to the equations, is “maybe”, and the universe isn’t letting us know.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;forward-time-travel-is-easy&quot;&gt;Forward time travel is easy&lt;/h3&gt;

&lt;p&gt;Before tackling the hard direction, it’s worth noting that forward time travel is a solved problem. It’s been happening since the universe had mass and relative motion; we’ve just been &lt;em&gt;proving&lt;/em&gt; it since 1971.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;twin paradox&lt;/a&gt; is real. Move fast enough relative to someone else, and less time passes for you. The Hafele-Keating experiment confirmed it with caesium clocks on commercial airliners. GPS satellites confirm it every second of every day. Scott Kelly came back from the ISS 5 milliseconds younger than his twin.&lt;/p&gt;

&lt;p&gt;If you want to travel a thousand years into the future, the recipe is straightforward: accelerate to a significant fraction of the speed of light, cruise for a while (by your clock), decelerate, and come home. The energy requirements are absurd, accelerating a modest spacecraft to 99% of light speed would require more energy than the entire world currently produces in a year, but the physics is not in dispute. You would arrive in the future. Everyone you knew would be dead. Going back would need a different mechanism entirely, which is where this gets interesting.&lt;/p&gt;

&lt;p&gt;Gravitational time dilation offers another route. Park yourself near (but not inside) a black hole, wait a while by your clock, then fly away. Less time passes for you than for the universe outside. The film &lt;em&gt;Interstellar&lt;/em&gt; got this broadly right: the characters who visited the planet near the black hole aged hours while decades passed outside. The specific numbers in the film were dramatised, but the principle is textbook general relativity.&lt;/p&gt;

&lt;p&gt;Forward time travel isn’t speculative; it’s engineering.&lt;/p&gt;

&lt;h3 id=&quot;backward-time-travel-the-equations-say-yes&quot;&gt;Backward time travel: the equations say yes&lt;/h3&gt;

&lt;p&gt;Going backward is where things get interesting, and contested.&lt;/p&gt;

&lt;p&gt;The equations of general relativity describe the geometry of spacetime. They’re not suggestions; they’re constraints. Given a distribution of mass and energy, the equations tell you exactly how spacetime curves. And some solutions to those equations contain closed timelike curves (CTCs): paths through spacetime that loop back on themselves. Travel along a CTC, and you return to your own past. A handful of such solutions are known; each one is mathematically valid, and each one is physically strange in its own way.&lt;/p&gt;

&lt;p&gt;Gödel’s rotating universe is where CTCs were first recognised for what they were. In 1949, Kurt Gödel found a solution to Einstein’s equations describing a universe that rotates as a whole, in which sufficiently long journeys through spacetime loop back to their starting point in time. You could, in principle, attend your own birth. CTCs had quietly been present in an earlier solution (Willem van Stockum’s 1937 infinite rotating dust cylinder) but nobody noticed until Frank Tipler pointed it out in 1974. Gödel’s was where the phenomenon became impossible to ignore.&lt;/p&gt;

&lt;p&gt;Gödel presented this solution as a birthday gift to Einstein. It’s unclear whether Einstein was delighted or horrified. Gödel’s universe doesn’t match ours, ours expands (his doesn’t) and the cosmic microwave background shows no sign of global rotation to extremely tight bounds, but that’s not the point. The point is that general relativity, taken at face value, &lt;em&gt;permits&lt;/em&gt; time travel. The equations don’t forbid it. Gödel proved that any argument of the form “time travel is impossible because it violates general relativity” is wrong. The theory allows it. Whether the universe chooses to use that allowance is a different question.&lt;/p&gt;

&lt;p&gt;The Kerr metric is another CTC solution, and one we can point a telescope at. In 1963, Roy Kerr found the solution for a rotating black hole. The Event Horizon Telescope has since imaged M87* and Sgr A* directly; LIGO routinely catches pairs of spinning black holes merging. The geometry is real; whether the &lt;em&gt;CTC region&lt;/em&gt; of the geometry is real is another question. Kerr’s solution contains closed timelike curves deep in the interior, behind the inner event horizon. In the mathematical solution, you could pass through the ring singularity and emerge in a region where time loops are possible.&lt;/p&gt;

&lt;p&gt;Whether this is physically meaningful is debated. The interior of the Kerr solution may be unstable; perturbations might destroy the closed timelike curves before anything could traverse them. But the mathematical structure is there, and it’s a solution to the same equations that predict GPS corrections and gravitational waves.&lt;/p&gt;

&lt;p&gt;Wormholes opened a third route. In 1988, Kip Thorne (who would later win a Nobel Prize for LIGO) showed that if traversable wormholes exist, shortcuts through spacetime connecting distant regions, they could be converted into time machines. The recipe: take one end of a wormhole, accelerate it to near-light speed, then bring it back. Time dilation means less time has passed at the accelerated end. Enter the “slow” end and you emerge from the “fast” end at an earlier time. You’ve gone backward.&lt;/p&gt;

&lt;p&gt;Thorne wasn’t trying to design a time machine. He was responding to a question from Carl Sagan, who was writing &lt;em&gt;Contact&lt;/em&gt; and wanted the physics to be plausible. But the analysis was rigorous, published in &lt;em&gt;Physical Review Letters&lt;/em&gt;, and it launched a serious research programme into the physics of time travel that continues today.&lt;/p&gt;

&lt;p&gt;The catch is that we don’t know if traversable wormholes can exist. They require “exotic matter” with negative energy density to keep them open. Quantum field theory allows negative energy densities in certain configurations (the Casimir effect is a real example), but whether you can get enough of it, concentrated enough, to hold open a wormhole is unknown.&lt;/p&gt;

&lt;p&gt;The Tipler cylinder came from the same Frank Tipler who’d dredged van Stockum’s CTCs out of obscurity, and he didn’t stop at reanalysing other people’s work. In the same 1974 paper, he showed that an infinitely long, extremely dense, rapidly rotating cylinder would drag spacetime around it hard enough to create closed timelike curves of its own. Finite cylinders don’t work; Stephen Hawking proved that the closed timelike curves require the cylinder to be infinite. This makes it impractical (to put it mildly) but it’s another example of the equations permitting what intuition forbids.&lt;/p&gt;

&lt;h3 id=&quot;the-grandfather-paradox-and-self-consistency&quot;&gt;The grandfather paradox and self-consistency&lt;/h3&gt;

&lt;p&gt;If backward time travel is possible, what stops you from killing your own grandfather before your parent is born? This is the oldest and most intuitive objection to time travel.&lt;/p&gt;

&lt;p&gt;The Novikov self-consistency principle offers one resolution. Proposed by Igor Novikov in the 1980s, it states that any events on a closed timelike curve must be self-consistent. You can travel to the past, but you can’t change it, because you didn’t. Whatever you do in the past has already happened. It’s already part of the history that led to you travelling backward in the first place.&lt;/p&gt;

&lt;p&gt;It’s like a jigsaw puzzle. You can’t place a piece that doesn’t fit. If you travel back and try to kill your grandfather, something prevents it: you slip, you miss, you change your mind. Not because of magic, but because the version of history where you succeed is logically inconsistent and therefore doesn’t exist. Only self-consistent histories are allowed.&lt;/p&gt;

&lt;p&gt;This isn’t as strange as it sounds. We already accept that physical laws constrain what’s possible. You can’t build a perpetual motion machine, not because someone stops you, but because the laws of thermodynamics don’t permit it. The Novikov principle says that self-consistency is a similar constraint: the laws of physics, applied to closed timelike curves, only admit solutions where the timeline is internally coherent.&lt;/p&gt;

&lt;p&gt;The Deutsch model takes a quantum approach. David Deutsch, in 1991, applied quantum mechanics to the grandfather paradox and showed that closed timelike curves are consistent if you allow the universe to be in a mixed quantum state. Roughly: the traveller who emerges from the time loop is not identical to the one who entered it. They’re a quantum mixture: partly themselves, partly a version from a slightly different history. This avoids paradoxes at the cost of letting quantum mechanics redefine what “the traveller” even means. Which, given everything else about quantum mechanics, is perhaps not a high price.&lt;/p&gt;

&lt;h3 id=&quot;the-quantum-eraser-does-the-future-affect-the-past&quot;&gt;The quantum eraser: does the future affect the past?&lt;/h3&gt;

&lt;p&gt;In 1999, Yoon-Ho Kim and colleagues performed an experiment that seems to suggest the future can influence the past. It’s called the delayed-choice quantum eraser, and it’s one of the most unsettling experiments in physics.&lt;/p&gt;

&lt;p&gt;Here’s the setup, simplified. You send photons through a double slit. Normally, they produce an interference pattern on a detector: the signature of quantum mechanics, showing the photons behaving as waves. But if you add a detector that tells you which slit each photon went through, the interference pattern disappears. The photons behave as particles. This much is standard quantum mechanics.&lt;/p&gt;

&lt;p&gt;Now the twist. Kim’s experiment split each photon into two entangled partners. One partner (the “signal”) went to a screen. The other (the “idler”) went on a longer path to a second detector, where the “which-path” information was either preserved or erased, &lt;em&gt;after&lt;/em&gt; the signal photon had already hit the screen.&lt;/p&gt;

&lt;p&gt;When the experimenters later compared the data, they found that the signal photons whose idler partners had their which-path information erased showed an interference pattern. The ones whose idler partners retained the information did not. The choice about the idler, made &lt;em&gt;after&lt;/em&gt; the signal photon hit the screen, appeared to retroactively determine whether the signal photon behaved as a wave or a particle.&lt;/p&gt;

&lt;p&gt;This is not, despite appearances, evidence of backward causation. The interference pattern only becomes visible when you sort the signal photons using information from the idlers. If you look at all the signal photons together, without sorting, there’s no interference pattern. The “retrocausal” effect is an artefact of post-selection, not a signal travelling backward in time. Still, the lesson is real: quantum correlations don’t respect our intuitions about the order of cause and effect. The universe doesn’t care which measurement happened first; the entanglement ties the results together regardless of timing.&lt;/p&gt;

&lt;h3 id=&quot;hawkings-party&quot;&gt;Hawking’s party&lt;/h3&gt;

&lt;p&gt;In 2009, Stephen Hawking threw a party for time travellers. He prepared champagne, put up a banner reading “Welcome, Time Travellers,” set coordinates, and waited. Nobody came.&lt;/p&gt;

&lt;p&gt;He published the invitation afterward, so that future time travellers would know when and where to show up. The fact that nobody arrived was, Hawking suggested with a grin, “experimental evidence that time travel is not possible.”&lt;/p&gt;

&lt;p&gt;It was a joke, mostly. The absence of guests doesn’t prove much: perhaps time travellers can’t travel to before the machine was built, or perhaps they chose not to come, or perhaps the invite was lost in the noise of history. But it illustrates Hawking’s own position: he believed the universe has a chronology protection mechanism that prevents closed timelike curves from forming.&lt;/p&gt;

&lt;p&gt;His chronology protection conjecture, published in 1992, argues that whenever conditions approach those needed for a time loop, quantum effects (specifically, a divergence in the stress-energy tensor of the vacuum) intervene and destroy the loop before it can form. The back-reaction of quantum fields near a forming CTC generates enough energy to collapse the would-be time machine.&lt;/p&gt;

&lt;p&gt;“It seems there is a chronology protection agency which prevents the appearance of closed timelike curves and so makes the universe safe for historians,” Hawking wrote. The conjecture is unproven. It might be wrong. But the fact that it was needed at all, that someone of Hawking’s stature felt the need to propose a &lt;em&gt;law&lt;/em&gt; preventing time travel, tells you how seriously the equations permit it.&lt;/p&gt;

&lt;h3 id=&quot;retrocausality-a-serious-proposal&quot;&gt;Retrocausality: a serious proposal&lt;/h3&gt;

&lt;p&gt;Most of this post has treated backward-in-time effects as paradoxical or impossible. But a growing number of physicists are taking retrocausality (genuine backward-in-time influence) seriously as a foundation for quantum mechanics.&lt;/p&gt;

&lt;p&gt;The motivation is Bell’s theorem. In 1964, John Bell proved that quantum mechanics cannot be explained by any theory where particles have pre-existing properties &lt;em&gt;and&lt;/em&gt; influences travel no faster than light. Experiments have repeatedly confirmed quantum mechanics. So at least one of those assumptions must be wrong.&lt;/p&gt;

&lt;p&gt;Most physicists give up the pre-existing properties (this is the standard “Copenhagen” or “many-worlds” approach). But a minority, including Huw Price at Cambridge and Ken Wharton at San José State, argue that we should instead give up the assumption that causes always precede effects. If influences can travel backward in time, Bell’s theorem is satisfied without giving up realism. Particles &lt;em&gt;do&lt;/em&gt; have definite properties; it’s just that future measurements can influence past states.&lt;/p&gt;

&lt;p&gt;This isn’t crackpot physics. Price and Wharton’s work is published in peer-reviewed journals and taken seriously by the foundations-of-physics community. It’s a minority position, but it’s a legitimate interpretation, and it has the advantage of preserving something that most quantum interpretations sacrifice: the idea that things have definite properties even when nobody’s looking.&lt;/p&gt;

&lt;p&gt;The price is steep. Retrocausality means that the state of a particle right now depends partly on what will happen to it in the future. Not in a way that lets you send messages backward (that would violate other constraints), but in a way that makes the universe’s bookkeeping work out. The future doesn’t &lt;em&gt;cause&lt;/em&gt; the past in the way you’d normally use the word. It &lt;em&gt;constrains&lt;/em&gt; it, the way a jigsaw puzzle constrains which pieces can go where.&lt;/p&gt;

&lt;h3 id=&quot;what-we-actually-know&quot;&gt;What we actually know&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Forward time travel is real. We’ve measured it. GPS depends on it. It’s engineering, not speculation.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;General relativity permits closed timelike curves. Multiple exact solutions to Einstein’s equations contain them. This is mathematics, not handwaving.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;We don’t know if the universe actually allows them. Hawking’s chronology protection conjecture says no, but it’s unproven. Quantum gravity might resolve this, but we don’t have a theory of quantum gravity.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The grandfather paradox has solutions. The Novikov principle (self-consistency) and the Deutsch model (quantum mixed states) both resolve it without contradiction.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Quantum mechanics is weird about time. Entanglement doesn’t respect temporal ordering. The delayed-choice quantum eraser demonstrates this without actually sending information backward.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Retrocausality is a legitimate interpretation. A minority of physicists take it seriously as a foundation for quantum mechanics.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Nobody came to Hawking’s party. Make of that what you will.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The physics of time travel isn’t a closed question; it’s an open one, sitting at the intersection of general relativity, quantum mechanics, and quantum gravity, precisely the intersection where our best theories break down. Until we have a theory that works at that intersection, the equations say “maybe” and the universe isn’t talking.&lt;/p&gt;

&lt;p&gt;There’s another clock to examine, though: the one inside you. It has no caesium atom and no GPS correction. It runs on light, adenosine, and a cluster of twenty thousand neurons behind your eyes. And it sets the terms for how you experience every other clock in this series.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/writing/the-clock-inside-you/&quot;&gt;The Clock Inside You&lt;/a&gt; is next: the biology of jet lag, shift work, and why your body refuses to run on UTC.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Forecasting Without Writing Python</title>
    <link href="/writing/forecasting-without-writing-python/"/>
    <updated>2026-05-13T06:00:00+08:00</updated>
    <id>/writing/forecasting-without-writing-python/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;AI Practitioner&lt;/strong&gt; · AIF-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;Priya is a category manager at a mid-size retail business. She owns 400 SKUs across homewares. Her CSV export from the data warehouse has 78 weekly rows per SKU (18 months of history), with columns: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sku&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;week_ending&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;units_sold&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;avg_unit_price&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promo_flag&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;competitor_promo_flag&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stock_out_days&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;weather_index&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;category&lt;/code&gt;. The ask from finance is a 13-week forward forecast of units sold per SKU, deliverable in two weeks, with enough of an explanation that a director can challenge it without Priya needing a data scientist in the room.&lt;/p&gt;

&lt;p&gt;Priya knows Excel well enough to build a naive seasonal-average forecast, but finance has asked for something better: one that accounts for promotions, stock-outs (units-sold is artificially capped in weeks where stock ran out), and the weather-index column the ops team started tracking last year. She knows pivot tables, not Python. Hiring a consultant is on the table but slow; the ML team can help in Q3, which is too late.&lt;/p&gt;

&lt;p&gt;The platform team has AWS available. Someone in the data team has muttered about “no-code ML tools,” but nobody has been precise about which one or whether it fits.&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;Before reaching for a tool, pin down what shape of problem this is and what the data can honestly tell us.&lt;/p&gt;

&lt;p&gt;The first question is &lt;em&gt;what kind of problem this actually is&lt;/em&gt;. Forecasting weekly units-sold from historical data is a time-series forecasting problem: the target is a number over time, history is ordered, seasonality matters, and exogenous variables (promo flag, weather index) may explain variation. It’s not a classification problem (“will this SKU sell out?”), not a regression on cross-sectional features (“predict price from SKU attributes”), not an image or text problem. Anything we pick has to treat time as a first-class axis.&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;the shape of the data&lt;/em&gt;. 400 SKUs × 78 weeks is 31,200 rows. That’s small by ML standards but each SKU has only 78 points of history, which isn’t a lot for any individual series. There’s a real choice between fitting one model per SKU (each model starves on 78 points) and fitting one global model across all SKUs (the model learns patterns that transfer between series, so a SKU with 20 weeks of history benefits from the other 399). For a 400-item catalogue with short histories, the global-model approach is the one that earns its keep.&lt;/p&gt;

&lt;p&gt;The third is &lt;em&gt;exogenous features&lt;/em&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promo_flag&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;competitor_promo_flag&lt;/code&gt; are known in advance for future weeks (the promo calendar is set); &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;weather_index&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; known in advance (it’s a forecast of its own). The right framing distinguishes between related time series that are known in advance (we can include future values, and the model will use them) and those that only have historical values (the model uses the history to learn correlations, but can’t see future values at prediction time). Getting this distinction correct matters: classifying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;weather_index&lt;/code&gt; as known-in-advance leaks future information in &lt;label for=&quot;sn-writing-forecasting-without-writing-python-training&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-forecasting-without-writing-python-training-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;training&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-forecasting-without-writing-python-training&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-forecasting-without-writing-python-training-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Training&lt;/span&gt;The process of fitting a model’s weights to data by minimising a loss function.
&lt;/span&gt; and produces optimistic backtests that don’t hold up live.&lt;/p&gt;

&lt;p&gt;The fourth is &lt;em&gt;stock-outs&lt;/em&gt;. Units-sold in a stock-out week is censored, demand existed, but supply capped what was recorded. A forecast trained on raw units-sold learns that demand drops in those weeks, which is wrong. The fix is data preparation: either exclude stock-out weeks from training, or adjust the target using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stock_out_days&lt;/code&gt; column (e.g. if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stock_out_days &amp;gt;= 4&lt;/code&gt;, flag the row as unreliable). That’s a &lt;label for=&quot;sn-writing-forecasting-without-writing-python-feature&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-forecasting-without-writing-python-feature-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;feature engineering&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-forecasting-without-writing-python-feature&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-forecasting-without-writing-python-feature-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Feature (ML)&lt;/span&gt;An input variable to a model – the numeric or categorical signals you compute from raw data and feed in.
&lt;/span&gt; decision, not a tool decision; whatever we pick, the business user has to own this choice and document it.&lt;/p&gt;

&lt;p&gt;The fifth is &lt;em&gt;explainability&lt;/em&gt;. Finance will ask “why is the Q2 forecast 20% higher than last year’s Q2?” The chosen tool has to produce some combination of feature importance charts, per-prediction explanations, and a “what-if” capability so that a director can challenge the forecast without a data scientist in the room. Black-box predictions that beat the baseline by 5% but can’t be narrated are worse than a transparent forecast that loses 5% of accuracy.&lt;/p&gt;

&lt;p&gt;The sixth is &lt;em&gt;operationalisation&lt;/em&gt;. A one-off forecast is a CSV download. A repeatable quarterly forecast is a scheduled job. If this forecast is going to run every quarter, the tool needs a path from “model built in the UI” to “model called on a schedule” without rebuilding from scratch each time. Otherwise we’re committing to clicking through the same wizard four times a year for as long as the business cares about the answer.&lt;/p&gt;

&lt;p&gt;And finally, &lt;em&gt;the audience constraint&lt;/em&gt;. Priya knows Excel and SQL. She doesn’t know Python, statistics-as-code, or Jupyter. Anything that requires writing a notebook, even a friendly one, shifts the work back to the ML team and defeats the whole point. The tool has to be navigable by a category manager.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Six filters, applied to the forecast-building tools Priya could use.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;No-code interface, does the tool let a non-coder build and run the model?&lt;/li&gt;
  &lt;li&gt;Handles time-series forecasting natively, as a first-class problem type, not cross-sectional regression?&lt;/li&gt;
  &lt;li&gt;Supports exogenous features (known-in-advance vs. historical-only)?&lt;/li&gt;
  &lt;li&gt;Explainability, feature importance and per-prediction explanations?&lt;/li&gt;
  &lt;li&gt;Repeatable, can the same model run on a schedule without rebuilding in the UI?&lt;/li&gt;
  &lt;li&gt;Priced appropriately for 400 SKUs × quarterly cadence?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-no-code-ml-landscape&quot;&gt;The no-code ML landscape&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;SageMaker Canvas. AWS’s no-code ML workspace. Supports tabular classification and regression (via AutoML), time-series forecasting, image and text classification, and GenAI-backed exploration (ask-your-data via a foundation model). For time-series, it runs a SageMaker Autopilot AutoML job under the hood, trying multiple algorithm families (DeepAR, CNN-QR, ETS, ARIMA, Prophet) and selecting the best by backtest. Explanation via feature importance charts; registration to Model Registry for scheduled reuse. Priced per-session-hour for the UI plus the underlying training and &lt;label for=&quot;sn-writing-forecasting-without-writing-python-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-forecasting-without-writing-python-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-forecasting-without-writing-python-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-forecasting-without-writing-python-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt; costs.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;QuickSight + ML Insights. QuickSight is AWS’s BI tool; ML Insights adds anomaly detection and a forecasting feature that produces simple time-series forecasts on visualisations using an internal algorithm. Useful for quick “what’s the trend?” answers directly in a dashboard, not for a model-quality forecast with exogenous features or explainability. Good for situational awareness; not the correct tool for a 400-SKU production forecast.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Amazon QuickSight Q / Amazon Q in QuickSight. The natural-language interface to QuickSight. Answers questions like “what was last quarter’s top-selling SKU?” in English. Not a forecasting tool; complementary to a forecast once it exists.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;SageMaker Autopilot (direct). The AutoML backbone Canvas uses. Callable via the SageMaker SDK or Studio UI. Produces the same models Canvas does but requires a user comfortable enough with notebooks to trigger jobs, inspect candidates, and call endpoints. The path a data scientist would take; not the path for a non-coder.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Amazon Forecast (retired as standalone). Was a dedicated time-series forecasting service. Functionality folded into SageMaker Canvas’s time-series forecast type. Mentioned for historical context; don’t plan new work against it as a separate service.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;A third-party Excel add-in or a spreadsheet model. Priya could build an ETS or seasonal-naive model in Excel or a forecasting add-in. Cheap, familiar, but limited, hard to include exogenous features, hard to evaluate honestly, hard to explain beyond “I used a trend line.” Not a scaling answer for 400 SKUs with exogenous drivers.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tool&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;No-code&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Time-series native&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Exogenous features&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Explainability&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Repeatable&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Sized for this&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;SageMaker Canvas&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;QuickSight ML Insights&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Amazon Q in QuickSight&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;N/A&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SageMaker Autopilot (direct)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (wrong audience)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Excel add-in&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Manual&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Limp&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Only one tool ticks every box for the scenario: SageMaker Canvas in time-series forecast mode. The others either can’t handle the problem shape (QuickSight, Excel) or are the wrong audience (Autopilot directly).&lt;/p&gt;

&lt;h3 id=&quot;canvas-in-time-series-mode&quot;&gt;Canvas in time-series mode&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 600&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;SageMaker Canvas time-series workflow shown as six stages down the page. Stage one: import from S3 or Snowflake. Stage two: data preparation in Data Wrangler, handling stockouts and feature types. Stage three: configure the forecast, specifying target column, item identifier column, timestamp column, forecast horizon, and exogenous features classified as historical or known-in-advance. Stage four: train as an AutoML job trying DeepAR, CNN-QR, ETS, ARIMA, and Prophet. Stage five: review the best model with backtest metrics and feature importance. Stage six: produce predictions as a batch forecast, optionally registered to Model Registry for scheduled reuse.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .smc-stage     { fill: #fff; stroke: #4a7090; stroke-width: 1.8; }
      .smc-stage-h   { fill: rgba(70, 140, 200, 0.08); stroke: #4a7090; stroke-width: 2; }
      .smc-num       { font-size: 14px; font-weight: 700; fill: #4a7090; }
      .smc-title     { font-size: 14px; font-weight: 700; fill: #222; }
      .smc-sub       { font-size: 11px; fill: #444; }
      .smc-detail    { font-size: 11px; fill: #333; }
      .smc-arrow     { fill: none; stroke: #555; stroke-width: 1.8; }
      .smc-algo      { fill: rgba(180, 120, 60, 0.10); stroke: #8a5a1a; stroke-width: 1.3; }
      .smc-algo-text { font-size: 10px; fill: #5a3a00; font-weight: 600; }
      .smc-caption   { font-size: 12px; fill: #444; }
    &lt;/style&gt;
    &lt;marker id=&quot;smc-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;6&quot; markerHeight=&quot;6&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;!-- Stage 1 --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;30&quot; width=&quot;260&quot; height=&quot;80&quot; rx=&quot;6&quot; class=&quot;smc-stage-h&quot; /&gt;
  &lt;text x=&quot;58&quot; y=&quot;54&quot; class=&quot;smc-num&quot;&gt;1&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; class=&quot;smc-title&quot;&gt;Import&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;80&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;CSV from S3, Snowflake, Redshift,&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;96&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;Athena, or local upload up to 5 GB&lt;/text&gt;

  &lt;!-- Arrow --&gt;
  &lt;path d=&quot;M170,110 L170,135&quot; class=&quot;smc-arrow&quot; marker-end=&quot;url(#smc-arrow)&quot; /&gt;

  &lt;!-- Stage 2 --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;135&quot; width=&quot;260&quot; height=&quot;100&quot; rx=&quot;6&quot; class=&quot;smc-stage-h&quot; /&gt;
  &lt;text x=&quot;58&quot; y=&quot;159&quot; class=&quot;smc-num&quot;&gt;2&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;smc-title&quot;&gt;Prepare (Data Wrangler)&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;185&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;exclude or flag stock-out weeks&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;201&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;cast types, fill gaps, join tables&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;217&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;export clean dataset for training&lt;/text&gt;

  &lt;path d=&quot;M170,235 L170,260&quot; class=&quot;smc-arrow&quot; marker-end=&quot;url(#smc-arrow)&quot; /&gt;

  &lt;!-- Stage 3 --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;260&quot; width=&quot;260&quot; height=&quot;120&quot; rx=&quot;6&quot; class=&quot;smc-stage-h&quot; /&gt;
  &lt;text x=&quot;58&quot; y=&quot;284&quot; class=&quot;smc-num&quot;&gt;3&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;290&quot; text-anchor=&quot;middle&quot; class=&quot;smc-title&quot;&gt;Configure forecast&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;310&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;target = units_sold&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;325&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;item_id = sku · timestamp = week_ending&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;340&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;horizon = 13 weeks · frequency = W&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;355&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;classify promo_flag as known-ahead,&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;370&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;weather_index as historical-only&lt;/text&gt;

  &lt;path d=&quot;M170,380 L170,410&quot; class=&quot;smc-arrow&quot; marker-end=&quot;url(#smc-arrow)&quot; /&gt;

  &lt;!-- Stage 4 --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;410&quot; width=&quot;260&quot; height=&quot;160&quot; rx=&quot;6&quot; class=&quot;smc-stage-h&quot; /&gt;
  &lt;text x=&quot;58&quot; y=&quot;434&quot; class=&quot;smc-num&quot;&gt;4&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;440&quot; text-anchor=&quot;middle&quot; class=&quot;smc-title&quot;&gt;AutoML training&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;458&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;Canvas runs Autopilot&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;474&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;and evaluates candidates on a&lt;/text&gt;
  &lt;text x=&quot;170&quot; y=&quot;490&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;rolling-origin backtest:&lt;/text&gt;

  &lt;rect x=&quot;60&quot; y=&quot;500&quot; width=&quot;100&quot; height=&quot;20&quot; rx=&quot;10&quot; class=&quot;smc-algo&quot; /&gt;
  &lt;text x=&quot;110&quot; y=&quot;514&quot; text-anchor=&quot;middle&quot; class=&quot;smc-algo-text&quot;&gt;DeepAR+&lt;/text&gt;
  &lt;rect x=&quot;180&quot; y=&quot;500&quot; width=&quot;100&quot; height=&quot;20&quot; rx=&quot;10&quot; class=&quot;smc-algo&quot; /&gt;
  &lt;text x=&quot;230&quot; y=&quot;514&quot; text-anchor=&quot;middle&quot; class=&quot;smc-algo-text&quot;&gt;CNN-QR&lt;/text&gt;

  &lt;rect x=&quot;60&quot; y=&quot;526&quot; width=&quot;60&quot; height=&quot;20&quot; rx=&quot;10&quot; class=&quot;smc-algo&quot; /&gt;
  &lt;text x=&quot;90&quot; y=&quot;540&quot; text-anchor=&quot;middle&quot; class=&quot;smc-algo-text&quot;&gt;ETS&lt;/text&gt;
  &lt;rect x=&quot;130&quot; y=&quot;526&quot; width=&quot;70&quot; height=&quot;20&quot; rx=&quot;10&quot; class=&quot;smc-algo&quot; /&gt;
  &lt;text x=&quot;165&quot; y=&quot;540&quot; text-anchor=&quot;middle&quot; class=&quot;smc-algo-text&quot;&gt;ARIMA&lt;/text&gt;
  &lt;rect x=&quot;210&quot; y=&quot;526&quot; width=&quot;70&quot; height=&quot;20&quot; rx=&quot;10&quot; class=&quot;smc-algo&quot; /&gt;
  &lt;text x=&quot;245&quot; y=&quot;540&quot; text-anchor=&quot;middle&quot; class=&quot;smc-algo-text&quot;&gt;Prophet&lt;/text&gt;

  &lt;text x=&quot;170&quot; y=&quot;564&quot; text-anchor=&quot;middle&quot; class=&quot;smc-sub&quot;&gt;typically 2-4 hours for this volume&lt;/text&gt;

  &lt;!-- Right column: stages 5 &amp; 6 --&gt;

  &lt;!-- Stage 5 --&gt;
  &lt;rect x=&quot;500&quot; y=&quot;30&quot; width=&quot;560&quot; height=&quot;220&quot; rx=&quot;6&quot; class=&quot;smc-stage-h&quot; /&gt;
  &lt;text x=&quot;518&quot; y=&quot;54&quot; class=&quot;smc-num&quot;&gt;5&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; class=&quot;smc-title&quot;&gt;Review&lt;/text&gt;

  &lt;text x=&quot;520&quot; y=&quot;92&quot; class=&quot;smc-detail&quot;&gt;· Model leaderboard: winning algorithm (often DeepAR+ on pooled retail data)&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;112&quot; class=&quot;smc-detail&quot;&gt;· Backtest metrics: wQL, MAPE, RMSE at P10/P50/P90 quantiles&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;132&quot; class=&quot;smc-detail&quot;&gt;· Feature importance: which columns actually drove predictions&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;152&quot; class=&quot;smc-detail&quot;&gt;· Per-SKU charts: predicted vs. actual on held-out weeks&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;172&quot; class=&quot;smc-detail&quot;&gt;· Quantile forecast: P10 / P50 / P90 for every SKU × future week&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;192&quot; class=&quot;smc-detail&quot;&gt;· &quot;What-if&quot; panel: override promo_flag for a future week, see the impact&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;220&quot; class=&quot;smc-detail&quot; style=&quot;font-style: italic;&quot;&gt;This is where a business user catches a wrong answer before finance does.&lt;/text&gt;

  &lt;!-- Stage 6 --&gt;
  &lt;rect x=&quot;500&quot; y=&quot;280&quot; width=&quot;560&quot; height=&quot;160&quot; rx=&quot;6&quot; class=&quot;smc-stage-h&quot; /&gt;
  &lt;text x=&quot;518&quot; y=&quot;304&quot; class=&quot;smc-num&quot;&gt;6&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;310&quot; text-anchor=&quot;middle&quot; class=&quot;smc-title&quot;&gt;Predict &amp;amp; operationalise&lt;/text&gt;

  &lt;text x=&quot;520&quot; y=&quot;342&quot; class=&quot;smc-detail&quot;&gt;· Batch prediction: CSV out, one row per SKU × future week × quantile&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;362&quot; class=&quot;smc-detail&quot;&gt;· Share model with Studio users for inspection and extension&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;382&quot; class=&quot;smc-detail&quot;&gt;· Register to SageMaker Model Registry for approval workflow&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;402&quot; class=&quot;smc-detail&quot;&gt;· Schedule via EventBridge + Lambda -&amp;gt; SageMaker batch transform&lt;/text&gt;
  &lt;text x=&quot;520&quot; y=&quot;422&quot; class=&quot;smc-detail&quot;&gt;· Export feature importance and SHAP values for the finance slide deck&lt;/text&gt;

  &lt;!-- Diagonal arrow from 4 to 5 --&gt;
  &lt;path d=&quot;M300,460 L500,160&quot; class=&quot;smc-arrow&quot; marker-end=&quot;url(#smc-arrow)&quot; /&gt;

  &lt;!-- Arrow from 5 to 6 --&gt;
  &lt;path d=&quot;M780,250 L780,280&quot; class=&quot;smc-arrow&quot; marker-end=&quot;url(#smc-arrow)&quot; /&gt;

  &lt;!-- Footer --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;490&quot; width=&quot;1020&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;smc-stage&quot; style=&quot;opacity: 0;&quot; /&gt;
  &lt;text x=&quot;780&quot; y=&quot;515&quot; text-anchor=&quot;middle&quot; class=&quot;smc-caption&quot; style=&quot;font-style: italic;&quot;&gt;Priya spends most of her time on stages 2 (data preparation) and 5 (review),&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;535&quot; text-anchor=&quot;middle&quot; class=&quot;smc-caption&quot; style=&quot;font-style: italic;&quot;&gt;which is the correct place for business judgement. Canvas automates 1, 3, 4, and 6.&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Six stages; Canvas automates four of them. The business judgement lives in preparation (what&apos;s a stock-out worth?) and review (does this backtest make sense?).&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-pick-in-depth&quot;&gt;The pick in depth&lt;/h3&gt;

&lt;p&gt;Canvas time-series forecast, trained on the prepared dataset, registered for quarterly reuse.&lt;/p&gt;

&lt;p&gt;The import is a two-click exercise: Canvas reads from S3 (or Snowflake, Redshift, Athena, or a direct upload up to 5 GB). Priya’s CSV lands in a dataset that Canvas can inspect.&lt;/p&gt;

&lt;p&gt;Preparation in Data Wrangler. Canvas has an embedded Data Wrangler view, a visual transform builder. Priya’s required transforms:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Exclude unreliable rows. A filter step: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stock_out_days &amp;lt; 4&lt;/code&gt;. Weeks where stock was out for more than half the week are removed from training. The alternative, scaling units_sold up to impute demand, is defensible but introduces assumptions; excluding is cleaner for a first pass.&lt;/li&gt;
  &lt;li&gt;Parse timestamp. Confirm &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;week_ending&lt;/code&gt; is recognised as a date with weekly frequency.&lt;/li&gt;
  &lt;li&gt;Derive features. Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;week_of_year&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;month&lt;/code&gt; columns (Canvas offers one-click “extract date parts”). These give the model explicit seasonality signals.&lt;/li&gt;
  &lt;li&gt;Confirm types. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promo_flag&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;competitor_promo_flag&lt;/code&gt; should be categorical (not numeric), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;units_sold&lt;/code&gt; should be numeric, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sku&lt;/code&gt; should be categorical as the item identifier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prepared dataset gets exported as the training input.&lt;/p&gt;

&lt;p&gt;Forecast configuration. In Canvas’s time-series flow:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Target column: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;units_sold&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Item identifier: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sku&lt;/code&gt; (the column that distinguishes one time series from another)&lt;/li&gt;
  &lt;li&gt;Timestamp: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;week_ending&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Frequency: Weekly&lt;/li&gt;
  &lt;li&gt;Forecast horizon: 13 (weeks)&lt;/li&gt;
  &lt;li&gt;Forecast quantiles: P10, P50, P90 (this is the spread of the probabilistic forecast; finance can see downside and upside, not just a point estimate)&lt;/li&gt;
  &lt;li&gt;Related time series, known in advance: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promo_flag&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;competitor_promo_flag&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;week_of_year&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;month&lt;/code&gt;. These are all known for future weeks because the promo calendar is set and calendar features are deterministic.&lt;/li&gt;
  &lt;li&gt;Related time series, historical only: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;avg_unit_price&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;weather_index&lt;/code&gt;. Unknown for future weeks; the model uses their history to learn correlations but must impute them for prediction.&lt;/li&gt;
  &lt;li&gt;Item metadata: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;category&lt;/code&gt;. Static attributes of each SKU, useful for the model to learn category-level patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Training. Canvas runs a SageMaker Autopilot job that tries several algorithms: DeepAR+ (a deep-learning autoregressive model that pools across items), CNN-QR (convolutional quantile regression), ETS (exponential smoothing), ARIMA, and Prophet. For 400 items × 78 weeks, typical training time is 2-4 hours. The job backtests each candidate on a rolling-origin split (train on weeks 1-65, predict 66-78; train on weeks 1-52, predict 53-65; etc.) and scores each on weighted quantile loss (wQL) at the chosen quantiles.&lt;/p&gt;

&lt;p&gt;The winning model is usually DeepAR+ for retail-style data with many items, because it pools information across items, a SKU with 20 weeks of history benefits from what the model has learned about the other 399. For smaller datasets or single-item forecasts, classical methods (ETS, ARIMA) often win.&lt;/p&gt;

&lt;p&gt;Review. Canvas presents a dashboard:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Accuracy metrics: wQL, MAPE (mean absolute percentage error), and RMSE on the backtest. Priya compares to her naive seasonal-average baseline, if Canvas’s model doesn’t beat it by a material margin, the added complexity isn’t earning its keep.&lt;/li&gt;
  &lt;li&gt;Feature importance: a bar chart showing which columns drove predictions. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promo_flag&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;week_of_year&lt;/code&gt; dominate, the story is coherent; if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;category&lt;/code&gt; alone dominates, the model may be learning a category-level average and ignoring within-category variation.&lt;/li&gt;
  &lt;li&gt;Per-SKU plots: historical vs. forecast on held-out weeks. Priya clicks through a sample of 20 SKUs and eyeballs whether the forecasts look reasonable. This is the human judgement step that no backtest metric captures.&lt;/li&gt;
  &lt;li&gt;What-if: for a chosen future week, override &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promo_flag&lt;/code&gt; from 0 to 1 and see the forecast shift. This is the explainability story for finance: “if we don’t run the Q2 promo, the forecast drops 15%.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prediction. Canvas generates a forecast CSV: one row per SKU × future week × quantile, written back to S3. For a quarterly cadence, Priya registers the model to SageMaker Model Registry and an engineering partner wires up an EventBridge Scheduler rule that calls a Lambda that triggers a SageMaker batch-transform job on the registered model each quarter. Priya re-uses the same model for three quarters, retrains in Canvas when accuracy starts drifting or when new SKUs enter the catalogue.&lt;/p&gt;

&lt;h3 id=&quot;the-honest-limits&quot;&gt;The honest limits&lt;/h3&gt;

&lt;p&gt;Canvas isn’t magic. A few things to name:&lt;/p&gt;

&lt;p&gt;Small-history SKUs are still hard. A SKU with 12 weeks of data has no seasonal history; the model imputes from category peers, but confidence is low. Priya flagged these as fallback-to-human for the first forecast, which is the correct call. Trust the model where the data supports it.&lt;/p&gt;

&lt;p&gt;Exogenous-feature honesty. Classifying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;weather_index&lt;/code&gt; as known-in-advance would leak future information into training. Canvas would learn to “use next month’s weather” and produce a spuriously good backtest that fails in production. Classifying historical-only is the correct answer; accept that the model uses history of weather, not future, and that it might miss a forecast-able weather-driven shift.&lt;/p&gt;

&lt;p&gt;Stock-out handling is a modelling choice, not a tool choice. Canvas can’t know what a stock-out week’s “true” demand was. Priya chose to exclude them; someone else might scale up using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stock_out_days&lt;/code&gt; as a censoring indicator. Either is defensible; the choice should be documented so the next quarter’s forecast is consistent.&lt;/p&gt;

&lt;p&gt;The quantile spread is real information. A P10-to-P90 range that’s narrow says the model is confident; wide says the model doesn’t know. Finance should not be given only the P50, the range is part of the story. If the width is embarrassingly wide, the honest answer is “this forecast is a rough guide, not a commitment.”&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;SageMaker Canvas is AWS’s no-code ML interface. Tabular classification and regression via Autopilot; time-series forecasting as a first-class mode; image and text classification; GenAI-backed data exploration. Business analysts, product managers, and category managers are the audience.&lt;/li&gt;
  &lt;li&gt;Time-series forecasting is a distinct problem type. Target over time, ordered history, seasonality, exogenous features. Don’t solve it with cross-sectional regression; Canvas’s time-series mode is the correct tool.&lt;/li&gt;
  &lt;li&gt;The global-model approach pools across items. 400 SKUs × 78 weeks is better trained as one model over 400 series than as 400 separate models. Short-history items benefit from patterns learned on richer series.&lt;/li&gt;
  &lt;li&gt;Classify exogenous features correctly. Known-in-advance (promo calendar, calendar features) go into future predictions directly. Historical-only (weather, price) inform via lag correlations but aren’t known for future weeks. Misclassifying leaks future information and produces optimistic backtests.&lt;/li&gt;
  &lt;li&gt;Data preparation is where business judgement lives. Canvas doesn’t know what a stock-out week means. Filtering or adjusting target values is a modelling choice; document it.&lt;/li&gt;
  &lt;li&gt;Backtest metrics plus per-item plots plus feature importance is the review triangle. Don’t trust one metric alone; eyeball a sample of forecasts, confirm the drivers look sensible, and compare to a naive baseline before trusting the model.&lt;/li&gt;
  &lt;li&gt;Quantile forecasts give finance downside and upside. P10/P50/P90 is more honest than a single point estimate. Narrow quantile spread means confidence; wide means the model doesn’t know, and saying so is better than faking precision.&lt;/li&gt;
  &lt;li&gt;Model Registry + EventBridge + batch transform is the quarterly-cadence plumbing. Canvas builds the model; an engineering partner wires the schedule. One Canvas build can serve several quarters before retraining is warranted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A category manager with a spreadsheet-level skill set and two weeks can produce a defensible 13-week forecast for 400 SKUs, with quantile uncertainty and feature-attribution explanations, using Canvas. The ML team stays free for the harder problems. What Canvas gives up, the last few percentage points of accuracy a hand-tuned model might squeeze, is usually worth trading for the months of analyst time it returns.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Business Model Canvas: Does This Actually Work?</title>
    <link href="/writing/business-model-canvas-does-this-actually-work/"/>
    <updated>2026-05-12T06:00:00+08:00</updated>
    <id>/writing/business-model-canvas-does-this-actually-work/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/finding-the-fit/&quot;&gt;Finding the Fit&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Greenbox is a produce-box startup with 200 subscribers in Perth, racing to reach 1,000 within six months. They’ve discovered their customers care more about convenience than local sourcing, and now they need to figure out whether the business model actually works at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maya has a board meeting in three weeks. The agenda: present a credible path from 200 to 1,000 subscribers. If she can’t make the case, the money stops.&lt;/p&gt;

&lt;p&gt;She’s been working on the pitch deck in the evenings. Good slides. Compelling narrative. But on Wednesday morning, she stares at slide nine, the financial projections, and realises she’s been avoiding the hard question. Not “can we grow?” but “can we grow &lt;em&gt;profitably&lt;/em&gt;?”&lt;/p&gt;

&lt;h3 id=&quot;reaching-for-the-familiar&quot;&gt;Reaching for the familiar&lt;/h3&gt;

&lt;p&gt;Tom suggests Impact Mapping. The team spends thirty minutes on it. Useful, it shows the path to 1,000 involves both reducing churn and expanding acquisition. But Maya shakes her head.&lt;/p&gt;

&lt;p&gt;“This tells me &lt;em&gt;how&lt;/em&gt; to grow. It doesn’t tell me whether we can afford to.”&lt;/p&gt;

&lt;p&gt;Lee recognises the gap. “What you need is a picture of the whole machine, how money comes in, where it goes out, and whether the engine runs at the scale you’re targeting. The Business Model Canvas maps that out. We can do that this morning.”&lt;/p&gt;

&lt;p&gt;He pauses. The team is watching him. Lee has been their guide through the entire discovery journey. He’s the person who always has the next technique, the calm voice that says “let’s try this.”&lt;/p&gt;

&lt;p&gt;“What I &lt;em&gt;can’t&lt;/em&gt; do,” Lee says, “is read it for you once it’s mapped. CAC, lifetime value, what your moat looks like against a competitor with sixty times your funding. I’ve been around those questions, I’ve never run a subscription business through the wall they put up. If I try to interpret the canvas for you, I’ll be doing exactly what we tell teams not to do: guessing at the answers instead of finding someone who knows.”&lt;/p&gt;

&lt;p&gt;The room is quiet. It’s a harder thing to say than it sounds. Admitting a limit feels like stepping off a cliff. But he feels something he hasn’t felt in twenty years of consulting: relief.&lt;/p&gt;

&lt;p&gt;His phone buzzes in his pocket. He glances at it, a text from Yuki: &lt;em&gt;Dad, can you call me this weekend?&lt;/em&gt; He puts the phone away. Maya notices.&lt;/p&gt;

&lt;p&gt;“You can take that,” she says.&lt;/p&gt;

&lt;p&gt;“She’ll call back,” Lee says.&lt;/p&gt;

&lt;p&gt;Maya looks at him. “Will she?”&lt;/p&gt;

&lt;p&gt;Lee doesn’t answer. He turns back to the whiteboard.&lt;/p&gt;

&lt;p&gt;“We’ll map it this morning. Then I’m going to call someone. Charlotte Wong, she’s scaled two subscription businesses past Series A. Once we’ve got the picture, she can read it.”&lt;/p&gt;

&lt;h3 id=&quot;what-a-business-model-canvas-is&quot;&gt;What a Business Model Canvas is&lt;/h3&gt;

&lt;p&gt;The Business Model Canvas was created by Alexander Osterwalder. Nine building blocks on a single page describing how a business creates, delivers, and captures value.&lt;/p&gt;

&lt;style&gt;
  .bmc-canvas { border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0; }
  .bmc-canvas__top { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr; }
  .bmc-canvas__bottom { display: grid; grid-template-columns: 1fr 1fr; }
  .bmc-canvas__cell { padding: var(--space-sm); border-right: 1px solid var(--color-rule); border-bottom: 1px solid var(--color-rule); }
  .bmc-canvas__label { display: block; font-size: 0.85rem; color: var(--color-accent); }
  .bmc-canvas__cell--infra { background: rgba(65,105,225,0.08); }
  .bmc-canvas__cell--value { background: rgba(46,139,87,0.08); }
  .bmc-canvas__cell--customer { background: rgba(255,140,0,0.08); }
  .bmc-canvas__cell--money { background: rgba(184,134,11,0.08); }
  .bmc-canvas__cell--partners { grid-row: 1 / 3; }
  .bmc-canvas__cell--proposition { grid-row: 1 / 3; }
  .bmc-canvas__cell--segments { grid-row: 1 / 3; border-right: none; }
  .bmc-canvas__cell--cost { border-bottom: none; }
  .bmc-canvas__cell--revenue { border-right: none; border-bottom: none; }

  @media (max-width: 640px) {
    .bmc-canvas__top { grid-template-columns: 1fr 1fr 1fr; }
    .bmc-canvas__cell--partners { grid-row: 1; grid-column: 1; }
    .bmc-canvas__cell--activities { grid-row: 2; grid-column: 1; }
    .bmc-canvas__cell--resources { grid-row: 3; grid-column: 1; }
    .bmc-canvas__cell--proposition { grid-row: 1 / 4; grid-column: 2; }
    .bmc-canvas__cell--relationships { grid-row: 1; grid-column: 3; border-right: none; }
    .bmc-canvas__cell--channels { grid-row: 2; grid-column: 3; border-right: none; }
    .bmc-canvas__cell--segments { grid-row: 3; grid-column: 3; }
  }
&lt;/style&gt;

&lt;div class=&quot;bmc-canvas&quot;&gt;
  &lt;div class=&quot;bmc-canvas__top&quot;&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--infra bmc-canvas__cell--partners&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Key Partners&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--infra bmc-canvas__cell--activities&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Key Activities&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--value bmc-canvas__cell--proposition&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Value Propositions&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--customer bmc-canvas__cell--relationships&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Customer Relationships&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--customer bmc-canvas__cell--segments&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Customer Segments&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--infra bmc-canvas__cell--resources&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Key Resources&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--customer bmc-canvas__cell--channels&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Channels&lt;/strong&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;bmc-canvas__bottom&quot;&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--money bmc-canvas__cell--cost&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Cost Structure&lt;/strong&gt;
    &lt;/div&gt;
    &lt;div class=&quot;bmc-canvas__cell bmc-canvas__cell--money bmc-canvas__cell--revenue&quot;&gt;
      &lt;strong class=&quot;bmc-canvas__label&quot;&gt;Revenue Streams&lt;/strong&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The power is that it forces everything onto one page. The connections, and contradictions, become visible.&lt;/p&gt;

&lt;h3 id=&quot;filling-it-in&quot;&gt;Filling it in&lt;/h3&gt;

&lt;p&gt;Lee facilitates. The team takes a morning. Maya brings the business knowledge, Sam brings customer data, Tom and Priya bring operational reality, Jas brings the product perspective. Tom jokes about buying shares in 3M. Nobody laughs, which tells you something about the mood.&lt;/p&gt;

&lt;p&gt;Customer Segments: Two segments from the JTBD and assumption mapping: &lt;em&gt;Convenience seekers (60%)&lt;/em&gt; who hire Greenbox to eliminate dinner stress, and &lt;em&gt;Local food advocates (40%)&lt;/em&gt; who believe in supporting local farms and eating seasonal produce.&lt;/p&gt;

&lt;p&gt;Value Propositions: For convenience seekers: “Dinner decided.” For local advocates: “Know your farmer.” Maya writes both on the board and steps back. “We’ve been marketing one value proposition to two segments. That’s a problem.”&lt;/p&gt;

&lt;p&gt;Channels: Word-of-mouth (31%), Google search (28%), Instagram (19%), local press (14%). Delivery via local courier. Customer communication by email.&lt;/p&gt;

&lt;p&gt;Customer Relationships: First-box discount for acquisition. Recipe cards, pause/skip, box preview emails for retention. Referral programme for growth.&lt;/p&gt;

&lt;p&gt;Revenue Streams: $25/week subscription. Potentially $20/week for a mixed-sourcing box.&lt;/p&gt;

&lt;p&gt;At 200 subscribers, all on the $25 box: $5,000 per week. $260,000 per year. Sounds decent.&lt;/p&gt;

&lt;p&gt;But Maya hasn’t looked at the other side yet.&lt;/p&gt;

&lt;p&gt;Cost Structure:&lt;/p&gt;

&lt;p&gt;This is where the room goes quiet. Maya pulls up the numbers on the projector. She hasn’t shared them with the full team before.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Cost component&lt;/th&gt;
      &lt;th&gt;Per box&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Produce (farm gate price)&lt;/td&gt;
      &lt;td&gt;$14.00&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Packing (materials + labour)&lt;/td&gt;
      &lt;td&gt;$3.50&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Delivery (courier)&lt;/td&gt;
      &lt;td&gt;$4.50&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total variable cost&lt;/td&gt;
      &lt;td&gt;$22.00&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Revenue per box: $25.00. Margin per box: $3.00.&lt;/p&gt;

&lt;p&gt;Tom does the arithmetic. “Three dollars margin per box. Two hundred boxes a week. That’s $600 a week. $31,200 a year.”&lt;/p&gt;

&lt;p&gt;“And that’s just the box,” Priya says. “Revenue minus what it costs to put one together and get it to the door. The $3 hasn’t paid the warehouse, the software, anyone’s salary, or marketing yet. It hasn’t been taxed yet either. Everything else the business does has to come out of that $31,200.”&lt;/p&gt;

&lt;p&gt;The room is silent. The number doesn’t survive that subtraction.&lt;/p&gt;

&lt;p&gt;“What about at 1,000 subscribers?” Priya asks.&lt;/p&gt;

&lt;p&gt;Maya updates the spreadsheet. Some costs improve with volume. Produce costs are relatively fixed, farms don’t offer bulk discounts at this scale.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Cost component&lt;/th&gt;
      &lt;th&gt;Per box (at 1,000)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Produce (farm gate price)&lt;/td&gt;
      &lt;td&gt;$13.00&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Packing (materials + labour)&lt;/td&gt;
      &lt;td&gt;$2.50&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Delivery (courier, volume rate)&lt;/td&gt;
      &lt;td&gt;$3.50&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total variable cost&lt;/td&gt;
      &lt;td&gt;$19.00&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Margin per box: $6.00. $6,000 per week. $312,000 per year. Barely covers operations. No money for growth.&lt;/p&gt;

&lt;p&gt;Tom stares at the projector. “We’re building a charity.”&lt;/p&gt;

&lt;h3 id=&quot;the-two-tier-question&quot;&gt;The two-tier question&lt;/h3&gt;

&lt;p&gt;The canvas is showing contradictions. 60% of subscribers would accept mixed sourcing at $20. But the cost structure assumes 100% local at $25. Maya is paying the premium for local produce, but the majority of her customers wouldn’t notice if she didn’t.&lt;/p&gt;

&lt;p&gt;“What if we offered the mixed-sourcing box?” Jas asks.&lt;/p&gt;

&lt;p&gt;Maya runs the numbers. If produce cost drops to $8 per box with mixed sourcing:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Model&lt;/th&gt;
      &lt;th&gt;Revenue/box&lt;/th&gt;
      &lt;th&gt;Cost/box&lt;/th&gt;
      &lt;th&gt;Margin/box&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;100% local, $25&lt;/td&gt;
      &lt;td&gt;$25.00&lt;/td&gt;
      &lt;td&gt;$19.00&lt;/td&gt;
      &lt;td&gt;$6.00&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Mixed sourcing, $20&lt;/td&gt;
      &lt;td&gt;$20.00&lt;/td&gt;
      &lt;td&gt;$14.00&lt;/td&gt;
      &lt;td&gt;$6.00&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The margin per box is the same. But the subscriber ceiling changes. With a $25-only model, the addressable market is the 40% who value local sourcing enough to pay the premium. With two tiers, the team can serve both segments.&lt;/p&gt;

&lt;p&gt;Priya adds: “And mixed sourcing means we’re not dependent on local farms scaling up in six months. Dave told you he can’t increase supply until next growing season.”&lt;/p&gt;

&lt;h3 id=&quot;charlotte&quot;&gt;Charlotte&lt;/h3&gt;

&lt;p&gt;Lee sets up a video call for Friday. Charlotte Wong joins from her home office in Perth’s northern suburbs. She’s 41, short grey hair, a bookshelf behind her stuffed with business books and, inexplicably, a small collection of wooden ducks.&lt;/p&gt;

&lt;p&gt;Charlotte grew up in Penang, Malaysia. Moved to Australia at fifteen. Engineering degree from UNSW, then a career in the specific kind of companies that either scale or die: a meal kit company, a SaaS platform, a logistics startup. The SaaS platform was acquired. The logistics startup is still running. The meal kit company, the one she doesn’t talk about unless you ask directly, folded eighteen months after she joined. She’d done everything correctly, or thought she had. The unit economics were wrong from the start and nobody caught it until the cash ran out. She keeps a spreadsheet of every business she’s ever worked with. Row 47 is Greenbox. She added it yesterday, after Lee’s call.&lt;/p&gt;

&lt;p&gt;Lee gives Charlotte a ten-minute summary. He shares the canvas. Charlotte listens without interrupting. Her face is still, not hostile, diagnostic. She’s reading the canvas the way a mechanic reads an engine.&lt;/p&gt;

&lt;p&gt;Then she asks three questions.&lt;/p&gt;

&lt;p&gt;“What’s your customer acquisition cost?”&lt;/p&gt;

&lt;p&gt;Silence. Nobody knows.&lt;/p&gt;

&lt;p&gt;“You don’t know,” Charlotte says. “That’s the most important number in a subscription business. If you can’t tell the board what it costs to acquire a customer, you can’t tell them whether growth is profitable or just expensive.”&lt;/p&gt;

&lt;p&gt;“What’s your subscriber lifetime value?”&lt;/p&gt;

&lt;p&gt;Maya starts: “Well, the average subscriber stays for…” She trails off.&lt;/p&gt;

&lt;p&gt;“At 5% monthly churn, average lifetime is about twenty months,” Charlotte says. “At $25 a week, that’s roughly $2,000 lifetime revenue. Minus variable costs, about $480 lifetime margin at 1,000 subscribers. If your acquisition cost is more than $480, you lose money on every subscriber you add. Growth makes you poorer, not richer.”&lt;/p&gt;

&lt;p&gt;She says this without emotion, but behind the flat tone is the meal kit company. They’d grown to 4,000 subscribers before anyone realised the CAC was higher than the lifetime margin. She’s never fully stopped carrying that one.&lt;/p&gt;

&lt;p&gt;“One more thing. Freshly charges eighteen dollars a week. You charge twenty-five. They have sixty times your funding and a polished app. If your customers are convenience-driven, and your JTBD data says sixty percent are, and Freshly delivers convenience at a lower price with better technology, what’s your moat?”&lt;/p&gt;

&lt;p&gt;Nobody answers. Charlotte doesn’t wait for one.&lt;/p&gt;

&lt;p&gt;“Your canvas shows two segments. Have you modelled what happens to your farm relationships if you introduce mixed sourcing? If 60% of subscribers switch to the mixed box, your local farm orders drop by 60%. Dave and Rachel are suddenly selling you 40% of what they used to. Can their businesses survive that?”&lt;/p&gt;

&lt;p&gt;Nobody had considered this. Charlotte saw the dependency that the canvas made visible, changing the cost structure could destroy the partnerships.&lt;/p&gt;

&lt;p&gt;“I’m not saying the mixed box is wrong. I’m saying you need to model the second-order effects. You need to bring your farms along, or you’ll have a cheap box with no story and an expensive box with no supply.”&lt;/p&gt;

&lt;p&gt;Maya writes furiously. Charlotte winds up the call.&lt;/p&gt;

&lt;p&gt;“Lee told me about the discovery work. Event Storming, JTBD, assumption mapping. That’s genuinely impressive for a team this size. Most startups your stage are still arguing about what the product should be. You know your domain and your customers. That’s rare.” She pauses. “The next problem is different. You need to know whether the &lt;em&gt;business&lt;/em&gt; works, not just the &lt;em&gt;product&lt;/em&gt;. I can help with that.”&lt;/p&gt;

&lt;p&gt;After the call, Charlotte sits in her home office. She picks up her phone and calls James.&lt;/p&gt;

&lt;p&gt;“How was it?” he asks. She can hear the boys arguing in the background.&lt;/p&gt;

&lt;p&gt;“I just told a founder her business model doesn’t work. The look on her face.”&lt;/p&gt;

&lt;p&gt;“Is the business worth saving?”&lt;/p&gt;

&lt;p&gt;Charlotte thinks about Maya’s eyes when the $3 margin appeared on the projector. Not defeat, recognition.&lt;/p&gt;

&lt;p&gt;“I think so. But she has to decide that, not me.”&lt;/p&gt;

&lt;p&gt;She opens her spreadsheet. Row 47. In the “First Impression” column: &lt;em&gt;Strong discovery culture. Broken unit economics. Founder identity tied to local sourcing, biggest risk is emotional, not financial.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;mayas-draft&quot;&gt;Maya’s draft&lt;/h3&gt;

&lt;p&gt;That night, Maya sits at the kitchen table in Fremantle. Nadia is in the other room reading. The house is quiet. Maya opens her laptop and starts a new email.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Dear Greenbox subscribers,&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We’ve made the difficult decision to pause operations while we reassess our business model to ensure we can continue to deliver the quality you expect.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;She reads the three sentences back. They’re corporate and bloodless and they sound nothing like her. She imagines Mrs Patterson reading them. She imagines Patrick reading them. She imagines Dave reading them and thinking: &lt;em&gt;Another one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;She doesn’t delete the draft. She doesn’t send it. She closes the laptop.&lt;/p&gt;

&lt;p&gt;Nadia appears in the doorway. “Come to bed.”&lt;/p&gt;

&lt;p&gt;“Coming.”&lt;/p&gt;

&lt;p&gt;She doesn’t tell Nadia about the email. She doesn’t tell anyone. The draft sits in her email, unsent, for the next six months.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-a-business-model-canvas&quot;&gt;When to use a Business Model Canvas&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Preparing to pitch investors. The canvas forces you to think about the whole business, not just the product.&lt;/li&gt;
  &lt;li&gt;Considering a significant business model change. Launching a new tier, entering a new market, the canvas shows second-order effects.&lt;/li&gt;
  &lt;li&gt;Post-revenue, pre-profitability. When the product works and people pay, but the model might not sustain itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;when-not-to-use-it&quot;&gt;When not to use it&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;When the problem is execution, not strategy. If deliveries arrive late, fix logistics. The canvas is for strategic clarity.&lt;/li&gt;
  &lt;li&gt;When you need detailed financial modelling. The canvas shows &lt;em&gt;what&lt;/em&gt; the cost structure looks like. For exact numbers, you need a spreadsheet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-comes-next&quot;&gt;What comes next&lt;/h3&gt;

&lt;p&gt;Maya has three weeks to prepare her board pitch. She has JTBD data, validated and invalidated assumptions, the canvas, and Charlotte’s framework for calculating the numbers that matter.&lt;/p&gt;

&lt;p&gt;She’s also preparing to propose something that would have been unthinkable three months ago: a two-tier product that partially abandons the 100% local sourcing she built the company around. The data says it’s the correct move. Her gut says it’s a betrayal.&lt;/p&gt;

&lt;p&gt;Charlotte told her, on that first call: “The founders who scale are the ones who fall in love with the problem, not the solution. You fell in love with local sourcing. Your customers fell in love with not thinking about dinner. Those aren’t the same thing.”&lt;/p&gt;

&lt;p&gt;Maya is still thinking about that.&lt;/p&gt;

&lt;p&gt;But thinking isn’t a plan. The team has data, frameworks, and broken unit economics. They know what’s wrong. They can’t fix everything at once. The board meeting is in three weeks.&lt;/p&gt;

&lt;p&gt;The question isn’t what to change. It’s &lt;a href=&quot;/writing/prioritisation-what-changes-first/&quot;&gt;what changes first&lt;/a&gt;.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Grounding a Chatbot in Your Own PDFs</title>
    <link href="/writing/grounding-a-chatbot-in-your-own-pdfs/"/>
    <updated>2026-05-11T06:00:00+08:00</updated>
    <id>/writing/grounding-a-chatbot-in-your-own-pdfs/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;AI Practitioner&lt;/strong&gt; · AIF-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A facilities-engineering team at a manufacturing site maintains 600 PDFs covering roughly 200 pieces of equipment, 50 safety procedures, and 30 maintenance schedules. Documents range from 5 to 300 pages; the largest are OEM manuals with dense tables, wiring diagrams, and exploded parts views. A handful are scans of older paper manuals where the PDF is a picture of a page.&lt;/p&gt;

&lt;p&gt;The engineers, around 40 on rotating shifts, currently type keywords into SharePoint search, open the top three or four hits, and Ctrl-F through them. Time-to-answer for “what’s the torque spec on the chiller’s compressor mount?” averages 8-12 minutes. The team lead has asked whether “one of those AI things” could shorten that to under a minute, with a citation back to the exact manual and page.&lt;/p&gt;

&lt;p&gt;There is already an S3 bucket mirroring the SharePoint drive (nightly sync). The team has AWS access; they don’t have ML engineers. Managed RAG services have come up in conversation; the question is what configuration actually makes a RAG pipeline work &lt;em&gt;well&lt;/em&gt; for this corpus, versus what configuration just makes it work at all.&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;Before reaching for a managed service, name the levers that govern answer quality in a RAG pipeline, and which ones this corpus is going to push on hardest.&lt;/p&gt;

&lt;p&gt;The first is &lt;em&gt;the shape of the corpus&lt;/em&gt;. 600 PDFs averaging, say, 40 pages each is roughly 24,000 pages of text. Some have rich tables; some are OEM manuals with figure captions and callouts; some are scanned. A generic “chunk every 300 &lt;label for=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;tokens&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;” strategy will split a wiring-diagram table across two chunks, and the retrieved half won’t make sense on its own. Knowing where the corpus sits on the structured-to-unstructured axis drives the chunking choice; this corpus sits squarely in “structured-heavy.”&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;what questions the engineers actually ask&lt;/em&gt;. “How do I reset the chiller?” maps well to procedure sections, which have clear headings. “What’s the torque spec on the compressor mount?” maps to a table of values. “What PPE do I need for this maintenance?” maps to a safety section. If most questions land on structured regions (tables, bulleted procedures, numbered safety steps), the retrieval needs to handle structure; if most are paraphrased conceptual questions (“why does the chiller do X?”), pure &lt;label for=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt; similarity is fine.&lt;/p&gt;

&lt;p&gt;The third is &lt;em&gt;scale and cost&lt;/em&gt;. 24,000 pages at, say, 500 tokens per page is 12M tokens of corpus. &lt;label for=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Embedding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; the whole thing once is a one-time pennies-to-dollars cost at the cheap per-token tier; re-embedding on updates is fractions of that. A dedicated managed vector store starts at hundreds of dollars a month for its minimum capacity allocation; piggy-backing on an existing relational database costs the additional storage of a vector column. The cost floor is mostly vector-store running cost, not embedding or querying.&lt;/p&gt;

&lt;p&gt;The fourth is &lt;em&gt;scans&lt;/em&gt;. Some of the PDFs are image-only. Text doesn’t come out of them without OCR. Any pipeline that ingests this corpus needs a parsing path that calls out to OCR (and ideally layout-aware OCR for tables) instead of silently producing empty chunks. Without that, the scanned manuals are dark matter, they exist in the index but their chunks are near-empty.&lt;/p&gt;

&lt;p&gt;The fifth is &lt;em&gt;citation format&lt;/em&gt;. The engineers want “chiller manual page 42, section 3.2” as the citation, not a raw S3 URI. That means chunks need to carry location metadata (S3 URI, page number, and ideally section/heading context) all the way through to the response. If the chunks are parsed badly, the citations are rough.&lt;/p&gt;

&lt;p&gt;The sixth is &lt;em&gt;governance&lt;/em&gt;. Every retrieval call invokes a foundation &lt;label for=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt;; every call needs to land in CloudTrail and optionally invocation logs. The engineers aren’t making sensitive queries, but the corpus contains supplier confidential information (some OEM manuals are marked “not for distribution”). The chosen pipeline needs a place to redact model numbers or supplier names from output, and an IAM-scoped query API so the engineer tool’s role can’t reach beyond the corpus.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Six configuration decisions for a managed RAG pipeline, scored against this particular corpus.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Chunking strategy, arbitrary-token splits, or boundaries that respect the document’s own structure?&lt;/li&gt;
  &lt;li&gt;Embedding model. English-only or multilingual; what dimension and cost trade-off?&lt;/li&gt;
  &lt;li&gt;Vector store, a managed standalone, or piggy-backed on a database the team already runs?&lt;/li&gt;
  &lt;li&gt;Parsing, default text extraction, or layout-aware extraction that handles tables, multi-column, and scans?&lt;/li&gt;
  &lt;li&gt;Retrieval configuration, how many results, vector-only or hybrid with keyword, and what metadata filters?&lt;/li&gt;
  &lt;li&gt;Generation model and &lt;label for=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; template, what quality tier, with what instructions about &lt;label for=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-grounding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-grounding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;grounding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-grounding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-grounding-a-chatbot-in-your-own-pdfs-grounding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Grounding&lt;/span&gt;Constraining a model to answer from provided sources rather than from whatever it absorbed during training.
&lt;/span&gt;, citation, and refusal?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-configuration-landscape&quot;&gt;The configuration landscape&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Chunking strategy. Bedrock Knowledge Bases offers four options. Default chunks into roughly 300-token pieces with ~20% overlap, safe, generic, ignores structure. Fixed-size lets you set chunk size and overlap explicitly. Hierarchical creates a two-level index: larger “parent” chunks for context and smaller “child” chunks for retrieval; the child is matched but the parent is what gets sent to the generation model. Semantic chunks using a foundation model to identify natural boundaries, paragraphs, sections, topic shifts, instead of arbitrary token counts. For dense technical manuals with heading structure, semantic or hierarchical chunking retrieves more cleanly than fixed-size because chunk boundaries match the document’s own logic.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Embedding model. Titan Text Embeddings v2 produces 1024-dimensional vectors, costs $0.00002 per 1K tokens, and is multilingual (100+ languages). Cohere Embed English v3 (1024 dims) is English-only and often retrieves slightly better on English-heavy corpora in Cohere’s own benchmarks. Cohere Embed Multilingual v3 (1024 dims) handles non-English. For a mostly-English manuals corpus with occasional non-English OEM content (German machine-tool manuals, Japanese electronics datasheets), Titan v2 is the safe default; Cohere Multilingual if multilingual retrieval quality is proven to be better on a test set.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Vector store. OpenSearch Serverless is the zero-plumbing choice. Bedrock can create it for you. Minimum 2 OCUs at roughly $0.24/OCU/hour means a floor of roughly $350/month. Aurora PostgreSQL with pgvector piggy-backs on an existing Aurora cluster: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CREATE EXTENSION vector;&lt;/code&gt; and a vector column on a table. No additional running cost beyond what the cluster already burns, but you manage the schema, the ingestion hooks, and the index tuning. Pinecone and Redis Enterprise Cloud are third-party integrations; useful if the organisation already runs one of them but usually not the first choice for a new build.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Parsing. Default parsing extracts text from PDFs using AWS’s standard extractors, fast, cheap, loses layout. Scanned pages produce no text. Advanced parsing routes documents through a foundation model (Claude, Nova) that sees the page layout, tables, figures, columns, and emits structured text preserving that layout. Costs per-page extra (priced like a model invocation); for a 24,000-page corpus that’s a material ingestion bill. Pays back on corpora where layout matters (tables, multi-column, scanned). Defaults work on plain-text PDFs.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Retrieval configuration. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;numberOfResults&lt;/code&gt;, how many chunks to retrieve per query, defaults to 5. For a 600-PDF corpus where relevant content might be split across chunks, 6-10 is often better. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;overrideSearchType&lt;/code&gt; controls vector-only vs. hybrid (vector similarity plus keyword BM25). Hybrid matters when exact terms (part numbers, equipment tags) drive the query. Metadata filters let queries constrain retrieval by fields on the source document (“only safety manuals”, “only equipment in building B”), requires metadata to be attached at ingestion via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.metadata.json&lt;/code&gt; sidecars in S3.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Generation model and prompt template. The generation model is independently configurable: Claude Sonnet, Nova Pro, Llama, any Bedrock-hosted text model. The Knowledge Base has a default prompt template that injects retrieved chunks under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$search_results$&lt;/code&gt; and asks the model to answer from them; you can override it with a custom template that specifies citation format, refusal behaviour, and tone.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;p&gt;Matching each decision to the facilities-engineering corpus:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Decision&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Default&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;This corpus&lt;/th&gt;
      &lt;th&gt;Rationale&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Chunking&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Default (300 tokens)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Semantic&lt;/td&gt;
      &lt;td&gt;Manual sections have natural boundaries; avoid splitting tables&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Embedding model&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Titan v2&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Titan v2&lt;/td&gt;
      &lt;td&gt;English-heavy, occasional multilingual, multilingual model as default&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Vector store&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;OpenSearch Serverless&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;OpenSearch Serverless&lt;/td&gt;
      &lt;td&gt;Lowest friction; no existing Aurora to piggy-back on&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Parsing&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Default text extraction&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Advanced parsing&lt;/td&gt;
      &lt;td&gt;Scans + tables + figures require layout-aware extraction&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Retrieval&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;5 results, vector-only&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;8 results, hybrid&lt;/td&gt;
      &lt;td&gt;Part numbers and equipment tags need keyword precision&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Generation model&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Claude Sonnet&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Claude Sonnet&lt;/td&gt;
      &lt;td&gt;Quality on drafting technical procedures justifies the token cost&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The two decisions that matter most for this corpus are advanced parsing (scanned manuals are otherwise invisible) and hybrid retrieval (part numbers and equipment tags are exact-match hints that pure vector search can miss). The others are close to defaults.&lt;/p&gt;

&lt;h3 id=&quot;how-the-pieces-fit-together&quot;&gt;How the pieces fit together&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 600&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Bedrock Knowledge Bases architecture split into two phases. Top half labelled ingestion: S3 bucket of PDFs feeds into advanced parsing using a foundation model for layout extraction, then into semantic chunking, then into Titan embeddings v2, which writes 1024-dimensional vectors to OpenSearch Serverless. Bottom half labelled query: engineer asks a question, the question is embedded, OpenSearch Serverless returns top 8 chunks via hybrid search, chunks are injected into a prompt template, Claude Sonnet generates an answer with citations, citations point back to source PDFs in S3.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .gcp-bg-ingest   { fill: rgba(70, 140, 200, 0.06); stroke: rgba(70, 140, 200, 0.5); stroke-width: 1.5; stroke-dasharray: 6 4; }
      .gcp-bg-query    { fill: rgba(46, 138, 90, 0.06); stroke: rgba(46, 138, 90, 0.5); stroke-width: 1.5; stroke-dasharray: 6 4; }
      .gcp-box         { fill: #fff; stroke: #333; stroke-width: 1.5; }
      .gcp-box-aws     { fill: rgba(255, 153, 0, 0.10); stroke: #cc7a00; stroke-width: 1.5; }
      .gcp-box-store   { fill: rgba(100, 60, 180, 0.08); stroke: #5a3aa0; stroke-width: 1.5; }
      .gcp-phase-title { font-size: 16px; font-weight: 700; }
      .gcp-phase-i     { fill: rgb(50, 110, 170); }
      .gcp-phase-q     { fill: rgb(36, 108, 70); }
      .gcp-box-title   { font-size: 13px; font-weight: 700; fill: #222; }
      .gcp-box-sub     { font-size: 11px; fill: #444; }
      .gcp-arrow       { fill: none; stroke: #555; stroke-width: 1.8; }
      .gcp-flow-label  { font-size: 11px; fill: #444; font-style: italic; }
    &lt;/style&gt;
    &lt;marker id=&quot;gcp-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;6&quot; markerHeight=&quot;6&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;!-- Ingestion phase --&gt;
  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;1060&quot; height=&quot;240&quot; rx=&quot;10&quot; class=&quot;gcp-bg-ingest&quot; /&gt;
  &lt;text x=&quot;40&quot; y=&quot;50&quot; class=&quot;gcp-phase-title gcp-phase-i&quot;&gt;Ingestion (one-off + on update)&lt;/text&gt;

  &lt;rect x=&quot;40&quot; y=&quot;80&quot; width=&quot;140&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box&quot; /&gt;
  &lt;text x=&quot;110&quot; y=&quot;110&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;S3 bucket&lt;/text&gt;
  &lt;text x=&quot;110&quot; y=&quot;128&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;600 PDFs&lt;/text&gt;
  &lt;text x=&quot;110&quot; y=&quot;144&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;SharePoint mirror&lt;/text&gt;

  &lt;path d=&quot;M180,120 L215,120&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;

  &lt;rect x=&quot;215&quot; y=&quot;80&quot; width=&quot;160&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box-aws&quot; /&gt;
  &lt;text x=&quot;295&quot; y=&quot;108&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;Advanced parsing&lt;/text&gt;
  &lt;text x=&quot;295&quot; y=&quot;126&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;Claude / Nova&lt;/text&gt;
  &lt;text x=&quot;295&quot; y=&quot;142&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;preserves tables &amp;amp; layout&lt;/text&gt;
  &lt;text x=&quot;295&quot; y=&quot;157&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;handles scans via Textract&lt;/text&gt;

  &lt;path d=&quot;M375,120 L410,120&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;

  &lt;rect x=&quot;410&quot; y=&quot;80&quot; width=&quot;160&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box-aws&quot; /&gt;
  &lt;text x=&quot;490&quot; y=&quot;108&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;Semantic chunking&lt;/text&gt;
  &lt;text x=&quot;490&quot; y=&quot;126&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;boundaries respect&lt;/text&gt;
  &lt;text x=&quot;490&quot; y=&quot;142&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;sections and headings&lt;/text&gt;

  &lt;path d=&quot;M570,120 L605,120&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;

  &lt;rect x=&quot;605&quot; y=&quot;80&quot; width=&quot;160&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box-aws&quot; /&gt;
  &lt;text x=&quot;685&quot; y=&quot;108&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;Titan v2 embeddings&lt;/text&gt;
  &lt;text x=&quot;685&quot; y=&quot;126&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;1024-dim vectors&lt;/text&gt;
  &lt;text x=&quot;685&quot; y=&quot;142&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;$0.00002 / 1K tokens&lt;/text&gt;

  &lt;path d=&quot;M765,120 L800,120&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;

  &lt;rect x=&quot;800&quot; y=&quot;80&quot; width=&quot;200&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box-store&quot; /&gt;
  &lt;text x=&quot;900&quot; y=&quot;108&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;OpenSearch Serverless&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;126&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;vector index + keyword index&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;142&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;supports hybrid search&lt;/text&gt;

  &lt;text x=&quot;300&quot; y=&quot;210&quot; class=&quot;gcp-flow-label&quot;&gt;Ingestion runs once for the initial corpus, then StartIngestionJob re-runs&lt;/text&gt;
  &lt;text x=&quot;300&quot; y=&quot;226&quot; class=&quot;gcp-flow-label&quot;&gt;incrementally on changed or added documents (EventBridge schedule, weekly).&lt;/text&gt;

  &lt;!-- Query phase --&gt;
  &lt;rect x=&quot;20&quot; y=&quot;290&quot; width=&quot;1060&quot; height=&quot;290&quot; rx=&quot;10&quot; class=&quot;gcp-bg-query&quot; /&gt;
  &lt;text x=&quot;40&quot; y=&quot;320&quot; class=&quot;gcp-phase-title gcp-phase-q&quot;&gt;Query (per-question, seconds)&lt;/text&gt;

  &lt;rect x=&quot;40&quot; y=&quot;350&quot; width=&quot;140&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box&quot; /&gt;
  &lt;text x=&quot;110&quot; y=&quot;380&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;Engineer tool&lt;/text&gt;
  &lt;text x=&quot;110&quot; y=&quot;398&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;RetrieveAndGenerate&lt;/text&gt;
  &lt;text x=&quot;110&quot; y=&quot;414&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;API call&lt;/text&gt;

  &lt;path d=&quot;M180,390 L215,390&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;

  &lt;rect x=&quot;215&quot; y=&quot;350&quot; width=&quot;160&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box-aws&quot; /&gt;
  &lt;text x=&quot;295&quot; y=&quot;378&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;Embed question&lt;/text&gt;
  &lt;text x=&quot;295&quot; y=&quot;396&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;same Titan v2 model&lt;/text&gt;
  &lt;text x=&quot;295&quot; y=&quot;412&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;(must match index)&lt;/text&gt;

  &lt;path d=&quot;M375,390 L410,390&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;

  &lt;rect x=&quot;410&quot; y=&quot;350&quot; width=&quot;160&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box-store&quot; /&gt;
  &lt;text x=&quot;490&quot; y=&quot;378&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;Hybrid search&lt;/text&gt;
  &lt;text x=&quot;490&quot; y=&quot;396&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;vector + keyword&lt;/text&gt;
  &lt;text x=&quot;490&quot; y=&quot;412&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;top 8 chunks&lt;/text&gt;

  &lt;path d=&quot;M570,390 L605,390&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;

  &lt;rect x=&quot;605&quot; y=&quot;350&quot; width=&quot;160&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box-aws&quot; /&gt;
  &lt;text x=&quot;685&quot; y=&quot;378&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;Prompt assembly&lt;/text&gt;
  &lt;text x=&quot;685&quot; y=&quot;396&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;question + chunks&lt;/text&gt;
  &lt;text x=&quot;685&quot; y=&quot;412&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;custom template&lt;/text&gt;

  &lt;path d=&quot;M765,390 L800,390&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;

  &lt;rect x=&quot;800&quot; y=&quot;350&quot; width=&quot;200&quot; height=&quot;80&quot; rx=&quot;4&quot; class=&quot;gcp-box-aws&quot; /&gt;
  &lt;text x=&quot;900&quot; y=&quot;378&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-title&quot;&gt;Claude Sonnet&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;396&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;grounded answer&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;412&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-box-sub&quot;&gt;+ citations&lt;/text&gt;

  &lt;!-- Return path --&gt;
  &lt;path d=&quot;M900,430 L900,475 L110,475 L110,430&quot; class=&quot;gcp-arrow&quot; marker-end=&quot;url(#gcp-arrow)&quot; /&gt;
  &lt;text x=&quot;500&quot; y=&quot;495&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-flow-label&quot;&gt;Answer text + retrieved-references list (S3 URI, page number) flows back to the engineer.&lt;/text&gt;

  &lt;text x=&quot;500&quot; y=&quot;540&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-flow-label&quot;&gt;A typical round trip is 1-3 seconds. CloudTrail records the call; invocation logging captures the full&lt;/text&gt;
  &lt;text x=&quot;500&quot; y=&quot;556&quot; text-anchor=&quot;middle&quot; class=&quot;gcp-flow-label&quot;&gt;prompt and output to S3 for audit, encrypted under a customer-managed KMS key.&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Two phases, seven moving parts. Ingestion is rare and expensive per-page; query is frequent and cheap per-call.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-configuration-in-depth&quot;&gt;The configuration in depth&lt;/h3&gt;

&lt;p&gt;Creating the Knowledge Base. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreateKnowledgeBase&lt;/code&gt; takes a data source configuration (S3 bucket ARN, optional inclusion/exclusion filters), an embedding model ARN (Titan v2 for this build), a vector store configuration (OpenSearch Serverless collection ARN and field mappings), and an IAM service role Bedrock will assume to read S3 and write to OpenSearch. The field mappings are worth getting right: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vectorField&lt;/code&gt; names the column holding the 1024-dim vector, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;textField&lt;/code&gt; holds the chunk text, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metadataField&lt;/code&gt; holds anything else (source URI, page number, section heading).&lt;/p&gt;

&lt;p&gt;Advanced parsing configuration. In the data source config, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parsingConfiguration&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parsingStrategy: BEDROCK_FOUNDATION_MODEL&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrockFoundationModelConfiguration&lt;/code&gt; pointing at Claude 3 Haiku (cheapest capable option) or Claude Sonnet (more accurate on complex layouts). The parser sees each PDF page as an image and emits layout-aware text: tables as tables, figures with captions, multi-column text reassembled in reading order. Costs scale per page; budget for a one-off few-hundred-dollar ingestion bill on the initial 24,000 pages.&lt;/p&gt;

&lt;p&gt;Chunking configuration. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chunkingConfiguration&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chunkingStrategy: SEMANTIC&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;semanticChunkingConfiguration&lt;/code&gt; specifying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxTokens&lt;/code&gt; (e.g. 600), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bufferSize&lt;/code&gt; (e.g. 1), and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;breakpointPercentileThreshold&lt;/code&gt; (e.g. 95). The threshold controls how aggressively the chunker splits: higher values mean fewer, larger chunks; lower means more, smaller. 95 is a reasonable starting point for procedure-style documents; tune by running a test set of queries and looking at whether retrieved chunks contain the whole answer or half of it.&lt;/p&gt;

&lt;p&gt;Data source sync. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StartIngestionJob&lt;/code&gt; kicks off ingestion. For the initial run, this parses, chunks, embeds, and indexes the full corpus (24,000 pages taking typically a few hours end-to-end, mostly in advanced parsing). For subsequent runs, Bedrock diffs against the last manifest and only re-processes changed files. An EventBridge Scheduler rule running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StartIngestionJob&lt;/code&gt; nightly (or hourly if updates are frequent) keeps the index current.&lt;/p&gt;

&lt;p&gt;The retrieval call. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; takes the question text and a configuration:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;input&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;How do I reset the chiller on floor 4?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;retrieveAndGenerateConfiguration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;KNOWLEDGE_BASE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;knowledgeBaseConfiguration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;knowledgeBaseId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;KB-FACILITIES&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;modelArn&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;arn:aws:bedrock:eu-west-1::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;retrievalConfiguration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;vectorSearchConfiguration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;numberOfResults&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;overrideSearchType&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;HYBRID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;equals&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;building&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;B&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;generationConfiguration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;promptTemplate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;textPromptTemplate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;You are a facilities-engineering assistant. Answer the engineer&apos;s question using only the provided manual excerpts. If the excerpts don&apos;t cover it, say &apos;I don&apos;t have that procedure in the indexed manuals&apos; rather than guessing. Always cite the source as [manual name, page N, section if given]. Be concise; engineers are often on the floor. Question: $query$&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Manual excerpts:&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;$search_results$&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;inferenceConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;textInferenceConfig&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;maxTokens&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;800&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;temperature&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Three points on that call. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; constrains retrieval to metadata matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;building=B&lt;/code&gt;, which works because the ingestion step attached building metadata via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.metadata.json&lt;/code&gt; sidecars in S3 (each PDF has a companion JSON listing building, equipment type, manufacturer). The custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;promptTemplate&lt;/code&gt; is the refusal-and-citation instruction; without it the default template does a passable job but not a great one. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature: 0.1&lt;/code&gt; keeps the model close to the retrieved text, higher values invite embellishment, which for a safety-adjacent domain is exactly what isn’t wanted.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-question&quot;&gt;A worked question&lt;/h3&gt;

&lt;p&gt;Ravi is on shift and the chiller alarm is blinking. He opens the engineer tool on his tablet.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Ravi: &quot;Chiller floor 4 fault code E-207, what&apos;s the reset procedure?&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The tool calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt;. A second later:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Answer:

Fault code E-207 on the floor-4 chiller (Trane CGAM 050, manual v3.2)
indicates a high-pressure cutout on the refrigerant circuit. Reset procedure:

1. Verify the condenser fans are running and unobstructed.
2. Wait 5 minutes for head pressure to normalise.
3. At the controller panel, press MENU -&amp;gt; DIAGNOSTICS -&amp;gt; CLEAR FAULT.
4. If E-207 returns within 10 minutes, escalate to maintenance
   (do not attempt a second reset).

PPE required: safety glasses only for the reset; refrigerant PPE only
if a circuit must be opened.

Sources:
  [Trane CGAM 050 Manual v3.2, page 142, §7.4]
  [Chiller Fault Code Quick Reference, page 3]
  [Site PPE Matrix - Building B, page 12]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What happened under the hood:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Ravi’s question was embedded with Titan v2.&lt;/li&gt;
  &lt;li&gt;OpenSearch Serverless ran a hybrid query, the vector matched conceptually to “chiller reset”, the keyword half matched “E-207” exactly, returning 8 chunks.&lt;/li&gt;
  &lt;li&gt;The top chunks were §7.4 of the Trane manual, the entry for E-207 in the quick-reference, and the PPE matrix section for Building B.&lt;/li&gt;
  &lt;li&gt;Claude Sonnet saw the chunks, the custom prompt template, and produced a grounded answer with the three citations. Every cited fact came from the retrieved text.&lt;/li&gt;
  &lt;li&gt;The tool rendered clickable citations that link back to the S3 URI and page number of each source document. Tapping “[Trane CGAM 050 Manual v3.2, page 142, §7.4]” opens the PDF at that page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Round trip: ~2 seconds. The model didn’t invent a fault code (it exists), didn’t invent a page number (it matches the source), and didn’t skip the PPE step (retrieval surfaced the site matrix). Time-to-answer went from 10 minutes to 5 seconds.&lt;/p&gt;

&lt;h3 id=&quot;edge-cases-the-configuration-handles&quot;&gt;Edge cases the configuration handles&lt;/h3&gt;

&lt;p&gt;The scanned manual. An older Siemens drive manual is a scan, not a text PDF. Without advanced parsing, its chunks would be near-empty and it would be invisible to retrieval. With advanced parsing, Claude extracts the text from each page image; the chunks carry real content. The OCR quality is imperfect on handwritten annotations, but the typewritten body text extracts cleanly enough for questions about it to land on its pages.&lt;/p&gt;

&lt;p&gt;The multi-building filter. Some procedures differ between Building A and Building B (different equipment models, different PPE requirements). Each PDF has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.metadata.json&lt;/code&gt; sidecar specifying which building it applies to. The retrieval call’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; constrains to the engineer’s current building, so “what’s the PPE for confined-space entry?” returns the Building B matrix, not Building A’s.&lt;/p&gt;

&lt;p&gt;The torque-spec table. A question like “what’s the torque spec on the compressor mount?” hits a table in the manual. Default parsing would have flattened the table row-by-row and split it across chunks; advanced parsing preserves it. Semantic chunking keeps the table intact within one chunk. The retrieved chunk contains the full table; the model extracts the right row based on the question’s equipment reference.&lt;/p&gt;

&lt;p&gt;The no-answer case. An engineer asks “what’s the torque spec on the new HVAC from SupplierCo?”, but the SupplierCo HVAC was installed last week and its manual hasn’t been added yet. Hybrid retrieval returns low-relevance chunks. The custom prompt template’s instruction, “If the excerpts don’t cover it, say ‘I don’t have that procedure in the indexed manuals’ rather than guessing”, kicks in, and the model refuses gracefully, prompting the engineer to add the manual or call the supplier.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Bedrock Knowledge Bases is end-to-end managed RAG. Point it at S3, configure it, call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt;. Ingestion, embedding, storage, retrieval, and generation plumbed for you.&lt;/li&gt;
  &lt;li&gt;Advanced parsing is the right default for document-heavy corpora. It costs real money at ingestion but turns scans into text and preserves tables and layout. Defaults lose all of that.&lt;/li&gt;
  &lt;li&gt;Semantic chunking respects document structure. Fixed-size chunking splits tables and procedure lists at arbitrary points. Semantic chunking aligns boundaries with the document’s own sections and paragraphs.&lt;/li&gt;
  &lt;li&gt;Hybrid search beats vector-only when exact terms matter. Part numbers, equipment tags, fault codes, keyword BM25 gets these right; pure vector search can miss them when surface forms don’t match.&lt;/li&gt;
  &lt;li&gt;Metadata filters are the scoping lever. Sidecar &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.metadata.json&lt;/code&gt; files attach structured attributes to each document; retrieval calls can filter by any of them. This is how you get per-building, per-equipment, per-role retrieval from one index.&lt;/li&gt;
  &lt;li&gt;Custom prompt templates are where refusal and citation behaviour lives. The default is adequate; a custom template is where you instruct the model to say “I don’t know” instead of inventing, and to format citations the way your UI expects.&lt;/li&gt;
  &lt;li&gt;Ingestion cost is one-off plus incremental; query cost is per-call. The big bill is the initial advanced-parsing pass over the whole corpus. Subsequent updates only re-parse changed documents; queries are standard Bedrock on-demand.&lt;/li&gt;
  &lt;li&gt;Invocation logging + CloudTrail + KMS keep the governance story complete. Every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; call emits CloudTrail; invocation logs capture full prompt and response to S3 under a customer-managed key; Knowledge Base IAM is a separate policy from the underlying model policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A working facilities chatbot isn’t a single configuration choice, it’s six of them, each justified by the shape of the corpus. Advanced parsing and hybrid retrieval are the two that shift this build from “it mostly works” to “engineers trust it on the floor.” The others are close to defaults, and that’s fine: the defaults exist because they’re sensible starting points. The craft is knowing which defaults to change.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>After the Transformer</title>
    <link href="/writing/after-the-transformer/"/>
    <updated>2026-05-09T06:00:00+08:00</updated>
    <id>/writing/after-the-transformer/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Your &lt;label for=&quot;sn-writing-after-the-transformer-context-window&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-after-the-transformer-context-window-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;context window&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-after-the-transformer-context-window&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-after-the-transformer-context-window-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Context window&lt;/span&gt;The maximum number of tokens an LLM can attend to in a single call – prompt plus output combined.
&lt;/span&gt; is one million &lt;label for=&quot;sn-writing-after-the-transformer-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-after-the-transformer-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;tokens&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-after-the-transformer-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-after-the-transformer-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;. The &lt;label for=&quot;sn-writing-after-the-transformer-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-after-the-transformer-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-after-the-transformer-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-after-the-transformer-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; bills you per token in and per token out, and the in-token bill grows linearly with the prompt, but the underlying compute grows quadratically. At a million tokens, the &lt;label for=&quot;sn-writing-after-the-transformer-attention&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-after-the-transformer-attention-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;attention&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-after-the-transformer-attention&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-after-the-transformer-attention-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Attention&lt;/span&gt;The mechanism inside a transformer that lets each token weigh how much every other token in the context matters to it.
&lt;/span&gt; step is doing roughly a trillion pairwise calculations. Someone is paying for that. It’s you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A handful of new architectures claim they can do the same job at linear cost. Some of them can. Some of them can’t. None of them have replaced transformers yet, but at least one of them is going to.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;/writing/to-llms-and-beyond/&quot;&gt;To LLMs… and Beyond!&lt;/a&gt; we mentioned state-space models, specifically Mamba, as the leading post-transformer candidate. That’s accurate but underspecified. There’s a whole research front trying to do better than the &lt;label for=&quot;sn-writing-after-the-transformer-transformer&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-after-the-transformer-transformer-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;transformer&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-after-the-transformer-transformer&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-after-the-transformer-transformer-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Transformer&lt;/span&gt;The neural network architecture that underpins modern LLMs – stacks of self-attention layers that let every token look at every other token in the context.
&lt;/span&gt; at sequence modelling, and the candidates differ in what they’re trying to fix. This post walks the field.&lt;/p&gt;

&lt;p&gt;The point isn’t that transformers are about to be replaced. They aren’t. The point is that the assumption “transformer = the only way” is already broken, and the alternatives are interesting enough to know about before they show up in production.&lt;/p&gt;

&lt;h3 id=&quot;whats-wrong-with-transformers&quot;&gt;What’s wrong with transformers&lt;/h3&gt;

&lt;p&gt;The transformer’s superpower is its attention mechanism: every token can attend to every other token. That’s how it captures long-range dependencies, and it’s why it dominates language modelling.&lt;/p&gt;

&lt;p&gt;The cost is also right there in the design. If your sequence has &lt;em&gt;n&lt;/em&gt; tokens, the attention step does roughly &lt;em&gt;n²&lt;/em&gt; pairwise comparisons. Double the input, quadruple the compute and the memory.&lt;/p&gt;

&lt;p&gt;For short sequences this doesn’t matter. For long ones it dominates. A 2,000-token prompt is fine. A 200,000-token prompt is expensive. A 2,000,000-token prompt is, on a vanilla transformer, infeasible.&lt;/p&gt;

&lt;p&gt;The industry has worked around this with engineering. FlashAttention, sliding-window attention, ring attention, KV-cache compression, and the workable context window has stretched from 2k tokens (GPT-3) to 1M+ tokens (Claude, Gemini) over a few years. But the underlying complexity is still quadratic. The workarounds are clever, not free.&lt;/p&gt;

&lt;p&gt;The post-transformer architectures all share one design goal: sub-quadratic scaling in sequence length. Beyond that they diverge sharply.&lt;/p&gt;

&lt;h3 id=&quot;state-space-models-mamba&quot;&gt;State-space models: Mamba&lt;/h3&gt;

&lt;p&gt;The most-discussed post-transformer architecture is the state-space model (SSM), and the leading example is Mamba (Gu and Dao, 2023).&lt;/p&gt;

&lt;p&gt;The intuition is the one we used in the &lt;a href=&quot;/writing/to-llms-and-beyond/&quot;&gt;entry post&lt;/a&gt;: instead of every token attending to every other token (the “re-read the book each time” approach), the model maintains a compressed hidden state that gets updated as each token comes in (the “running notes” approach). The cost of updating is constant per token, so the total cost is linear in sequence length, not quadratic.&lt;/p&gt;

&lt;p&gt;The catch is that the hidden state is lossy. It’s a fixed-size summary of everything that came before. If a transformer wants to recall the seventh sentence of a hundred-page document, it has the full attention budget to do so. If Mamba wants to recall it, it has to have written something useful about it into the hidden state at the time, and the hidden state has finite capacity.&lt;/p&gt;

&lt;p&gt;The Mamba innovation that mattered was making the state-update mechanism selective, the model learns which tokens to actually attend to and which to skim past, rather than treating every token equally. This narrowed the gap with transformers significantly, particularly on language modelling benchmarks.&lt;/p&gt;

&lt;p&gt;As of 2026, Mamba and Mamba-2 are competitive with transformers of similar size on many language tasks, sometimes superior on tasks involving very long sequences (DNA, audio, ultra-long documents), and sometimes weaker on tasks requiring precise long-range recall (associative memory). The honest summary: Mamba is real, it works, and it hasn’t beaten transformers across the board.&lt;/p&gt;

&lt;h3 id=&quot;the-hybrid-approach-striped-hyena-jamba&quot;&gt;The hybrid approach: Striped Hyena, Jamba&lt;/h3&gt;

&lt;p&gt;Most serious research on post-transformer architectures has converged on a pragmatic answer: don’t pick one, mix them.&lt;/p&gt;

&lt;p&gt;Hyena (Stanford, 2023) and its successor Striped Hyena are sub-quadratic architectures that interleave Hyena blocks with attention blocks, letting the cheap Hyena blocks do most of the work and the expensive attention blocks handle the parts that genuinely need cross-token comparison.&lt;/p&gt;

&lt;p&gt;Jamba (AI21 Labs, 2024) does the same thing but with Mamba blocks: a transformer-Mamba hybrid that uses Mamba layers for efficiency and transformer layers for the kinds of pattern matching transformers are still better at.&lt;/p&gt;

&lt;p&gt;The hybrid pattern is now the default assumption for “what comes after the pure transformer.” It’s not “Mamba replaces attention,” it’s “Mamba is a cheap layer that lets you spend your attention budget more carefully.”&lt;/p&gt;

&lt;h3 id=&quot;rwkv-and-retnet-the-rnn-comeback&quot;&gt;RWKV and RetNet: the RNN comeback&lt;/h3&gt;

&lt;p&gt;Two other notable lines try to revive the recurrent neural network, the architecture transformers replaced, with modern training tricks.&lt;/p&gt;

&lt;p&gt;RWKV (Receptance Weighted Key Value, BlinkDL, 2023+) is an RNN that can be trained like a transformer. Standard RNNs are notoriously slow to train because they’re inherently sequential, token &lt;em&gt;t+1&lt;/em&gt; depends on token &lt;em&gt;t&lt;/em&gt;. RWKV reformulates the recurrence in a way that allows parallel training (like a transformer) but sequential &lt;label for=&quot;sn-writing-after-the-transformer-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-after-the-transformer-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-after-the-transformer-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-after-the-transformer-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt; at constant cost per token (like an RNN). At inference time, an RWKV model uses constant memory regardless of sequence length, the dream that transformers can’t achieve.&lt;/p&gt;

&lt;p&gt;RetNet (Retentive Network, Microsoft, 2023) takes a similar approach with a different mechanism. It claims the “impossible triangle”: parallel training, recurrent inference, and strong performance.&lt;/p&gt;

&lt;p&gt;Neither has displaced transformers. Both are competitive in their weight classes and both are interesting if you care about deployment cost more than peak quality, a constant-memory inference path is genuinely useful when you’re running models on phones or in tight latency budgets.&lt;/p&gt;

&lt;h3 id=&quot;liquid-neural-networks&quot;&gt;Liquid neural networks&lt;/h3&gt;

&lt;p&gt;Liquid AI (an MIT spin-out) builds on a different research lineage: continuous-time neural networks where the hidden state evolves according to differential equations rather than discrete update steps. The promise is dramatically smaller models (often orders of magnitude smaller) that match the performance of much larger transformers on specific tasks.&lt;/p&gt;

&lt;p&gt;It’s early. Their language models are interesting and small (Liquid’s LFM-3B punches above its weight), but the wider research community hasn’t replicated the results across the spectrum of language tasks. Worth knowing exists. Probably not worth deploying yet unless you have a specific reason.&lt;/p&gt;

&lt;h3 id=&quot;diffusion-for-text&quot;&gt;Diffusion for text&lt;/h3&gt;

&lt;p&gt;Image generation switched from autoregressive to diffusion years ago (DALL-E 1 was autoregressive; DALL-E 2 onwards is diffusion). The natural question: why not the same for text?&lt;/p&gt;

&lt;p&gt;The answer for a long time was “because text is discrete and diffusion is continuous.” Recent work has found ways around this: discrete diffusion (operating directly on token distributions rather than continuous latents), masked diffusion (a generalisation of BERT’s masking objective), and absorbing-state diffusion (gradually replacing tokens with a special mask token, then learning to reverse the masking).&lt;/p&gt;

&lt;p&gt;Models in this space include SEDD (Score Entropy Discrete Diffusion), Plaid, and LLaDA (Large Language Diffusion Model, 2024-2025). The pitch is interesting: instead of generating left-to-right one token at a time, the model generates the whole output simultaneously and refines it over multiple denoising steps. This gives you parallel generation (faster wall-clock for long outputs) and the ability to edit or fill in any part of the output (not just append to the end).&lt;/p&gt;

&lt;p&gt;As of 2026, diffusion language models are competitive with similarly-sized autoregressive transformers on some benchmarks but lag on others. They’re a genuine alternative paradigm, not just a tweak. Whether they end up dominant or niche is one of the more open questions in the field.&lt;/p&gt;

&lt;h3 id=&quot;a-comparison&quot;&gt;A comparison&lt;/h3&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Architecture&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Sequence cost&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Inference memory&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Strengths&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Weaknesses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Transformer&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;O(n²)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Grows with context&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;General performance, ecosystem maturity&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Cost at long context&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Mamba (SSM)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;O(n)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Constant per token&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Long-sequence efficiency&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Lossy hidden state, weaker associative recall&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Striped Hyena / Jamba (hybrid)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Sub-quadratic&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Mostly constant + some attention KV&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Pragmatic mix, often best of both&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;More complex to train&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;RWKV / RetNet (RNN-like)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;O(n) train, constant inference&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Constant&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Cheapest inference, edge-friendly&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Smaller ecosystem, training quirks&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Liquid (continuous-time)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;O(n) typical&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Constant or near-constant&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Very small models punching up&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Early, narrower benchmark coverage&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Diffusion (discrete)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;O(n) per step × steps&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Holds full sequence&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Parallel generation, in-place editing&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Fixed step count, less mature for text&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h3 id=&quot;whats-actually-in-production&quot;&gt;What’s actually in production&lt;/h3&gt;

&lt;p&gt;In 2026, transformers still dominate every major API and almost every open-weight release. The frontier models. Claude, GPT, Gemini, are transformers. The leading open-weight models. Llama, Mistral, Qwen, are transformers.&lt;/p&gt;

&lt;p&gt;The cracks where alternatives have started shipping:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Long-sequence applications (DNA, audio, ultra-long-document analysis) increasingly use Mamba or hybrid architectures because the quadratic cost is the binding constraint.&lt;/li&gt;
  &lt;li&gt;Edge deployment (phones, embedded devices) is where RWKV and RetNet have the most traction, constant-memory inference matters more than peak benchmark scores when you have 4GB of RAM.&lt;/li&gt;
  &lt;li&gt;Hybrid models like Jamba are starting to appear in commercial offerings, mostly behind the scenes.&lt;/li&gt;
  &lt;li&gt;Diffusion language models are research today, productisation tomorrow, the parallel generation property is too useful to ignore long-term.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-this-means-for-you&quot;&gt;What this means for you&lt;/h3&gt;

&lt;p&gt;Probably nothing immediate. If you’re building on Claude or GPT or a Llama derivative, you’re using a transformer, and you’ll keep using a transformer for the foreseeable future. The point of knowing the alternatives isn’t to switch away from transformers tomorrow.&lt;/p&gt;

&lt;p&gt;The point is to recognise the shape of the next disruption when it lands. The story of “X dominated Y until something better came along” is the story of every architecture in the history of machine learning. Convolutional networks dominated vision for a decade until Vision Transformers came for them. RNNs dominated sequence modelling until transformers came for them. Transformers will eventually be replaced by something, and the candidates above are the live ones in 2026.&lt;/p&gt;

&lt;p&gt;If you maintain AI infrastructure, the bet that pays off is keeping the &lt;em&gt;interfaces&lt;/em&gt; clean, treating “the &lt;label for=&quot;sn-writing-after-the-transformer-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-after-the-transformer-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;language model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-after-the-transformer-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-after-the-transformer-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt;” as a swappable component rather than baking transformer-specific assumptions into your stack. The day a hybrid architecture starts winning at half the cost, you want to be able to swap.&lt;/p&gt;

&lt;p&gt;The pure transformer is showing its age in one specific way: the quadratic cost of attending every token to every other token, which the workarounds soften but don’t remove. The candidates all try to escape that ceiling by some flavour of compressed running state. Mamba writes notes as it goes and pays the price in lossy recall. RWKV and RetNet pull the recurrent network out of retirement with new training tricks and get constant-memory inference in return. Liquid networks let the hidden state evolve continuously and squeeze surprising performance out of very small models. Diffusion abandons the left-to-right loop entirely and refines a whole output across multiple passes. None of these has unseated the transformer, and the hybrids. Striped Hyena, Jamba, are an admission that the most useful answer in the medium term is a mix.&lt;/p&gt;

&lt;p&gt;If you’re building on Claude or GPT today, the practical takeaway is to keep the interface to “the language model” honest and swappable. The history of machine learning is a sequence of architectures dominating until something better arrived. Transformers will get their turn. The architecture that eventually replaces them is probably already in a paper somewhere on arXiv.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: Jobs to be Done</title>
    <link href="/writing/the-workshop-jobs-to-be-done/"/>
    <updated>2026-05-08T06:00:00+08:00</updated>
    <id>/writing/the-workshop-jobs-to-be-done/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Customers don’t buy products; they hire them to do a job. JTBD is the interview technique that surfaces the actual job and the alternatives they’d defect to. Switch interviews (structured interviews with people who recently switched products or services, asking what triggered the move) are the core mechanic. &lt;a href=&quot;/writing/jobs-to-be-done-why-subscribers-actually-stay/&quot;&gt;Why Subscribers Actually Stay&lt;/a&gt; is the worked example; this post is the playbook.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;jobs-to-be-done&quot;&gt;Jobs to be Done&lt;/h3&gt;

&lt;p&gt;Jobs to be Done runs switch interviews with recent customers and reads them through the four forces (push from the old situation, pull of the new option, anxiety about switching, habit holding the customer in place), so the team ends up with candidate job statements grounded in what customers actually said rather than what the room already believed. Sometimes called JTBD, job mapping, or outcome-driven innovation, though outcome-driven innovation is a distinct quantitative framework (Ulwick) that layers over the qualitative interviewing. The switch-interview technique comes from Bob Moesta and the Re-Wired Group; the four-forces framing from Moesta and Chris Spiek; the broader theory from Clayton Christensen. Frequently confused with user personas: personas describe &lt;em&gt;who&lt;/em&gt; a user is; jobs describe &lt;em&gt;what they’re trying to get done&lt;/em&gt;, a different axis and a more useful one for product decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; a facilitator, two or three rotating interviewers, a note-taker, the product lead, and ideally a CS or ops observer. Four to six team members, around 3h 45min with two interviews or 4h 30min with three.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; 3–5 candidate job statements in the form &lt;em&gt;“When [situation], I want to [motivation], so I can [outcome]”&lt;/em&gt;, a clustered wall of verbatim quotes tagged against the four forces, and the interview transcripts filed for future reading.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; churn that won’t move, a new feature area where the team can’t agree on the problem it solves, or several teams prioritising against different implicit jobs. Not for tactical backlog refinement, not when there are no recent switchers to talk to, and not when the team has decided the answer and only wants validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whats-it-for&quot;&gt;What’s It For&lt;/h3&gt;

&lt;p&gt;A team builds a feature someone asked for. The feature lands, the telemetry looks fine for a week, and then the customer who asked for it cancels. Nobody connects the cancellation to the feature (it was a quarter ago, a different person, a different conversation) but the pattern repeats quietly over the year. The backlog fills with requests. The product changes shape. Churn doesn’t move.&lt;/p&gt;

&lt;p&gt;The problem is that asking a customer what they want produces a list of features. The list is honest and useless. Customers describe solutions they can imagine because describing causes they’re half-aware of is hard. The Jobs to be Done school of thinking (Moesta, Christensen, Ulwick) reframes the interview: don’t ask what they want. Ask what happened the day they switched. What prompted it. What they were trying to get done. What they’d been doing before. What would have made them stay with the old thing.&lt;/p&gt;

&lt;p&gt;Switch interviews replace the feature wishlist with a story about a decision. The story contains the job. The job is usually not the one the team expected.&lt;/p&gt;

&lt;p&gt;This workshop exists to collapse that reframing into a single session: three or four interviews, silent discovery, a clustering round, and a set of candidate job statements the whole team watched emerge. The statements are the artefact. The shared view is the point.&lt;/p&gt;

&lt;p&gt;Reach for it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Churn is stable but not improving and you’ve exhausted surface-level theories&lt;/li&gt;
  &lt;li&gt;A new feature area is being considered and the team can’t agree on the problem it solves&lt;/li&gt;
  &lt;li&gt;You have access to recent switchers: people who started or stopped using the product in the last ninety days&lt;/li&gt;
  &lt;li&gt;Personas are in use and clearly not driving decisions&lt;/li&gt;
  &lt;li&gt;Several teams are prioritising against different implicit jobs and colliding&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-its-not-for&quot;&gt;What It’s Not For&lt;/h3&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You don’t have switchers to talk to. The technique &lt;em&gt;is&lt;/em&gt; switch interviewing; discovery without interviews is just speculation in a conference room.&lt;/li&gt;
  &lt;li&gt;The decision you’re trying to make is tactical. JTBD is a framing exercise, not a backlog refinement tool.&lt;/li&gt;
  &lt;li&gt;The team believes they already know the job and you’re being asked to validate it. Confirmation-seeking kills the interviews.&lt;/li&gt;
  &lt;li&gt;You can’t get 2–3 hours of focus out of the product lead. The discovery cannot be delegated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop a session that’s already started if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The interviewees can’t remember why they switched; they’re not recent enough&lt;/li&gt;
  &lt;li&gt;The sticky-note wall is mostly empty after thirty minutes; the interviews didn’t land&lt;/li&gt;
  &lt;li&gt;The room is arguing about whether switch interviews are valid; you have a trust problem, not a method problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stopping after the first interview to regroup on technique is not failure. Running three mediocre interviews and producing confident statements from them is.&lt;/p&gt;

&lt;h3 id=&quot;definitions--background&quot;&gt;Definitions &amp;amp; Background&lt;/h3&gt;

&lt;p&gt;Three dimensions of a job. Every job has three layers, and the richest material lives in the second and third:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Functional: the practical thing being done. &lt;em&gt;“Plan the week’s meals.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Social: how the customer wants to be seen while doing it. &lt;em&gt;“Be the parent who feeds the family well.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Emotional: how they want to feel, or stop feeling. &lt;em&gt;“Stop having to think about dinner on Sunday.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams capture only the functional layer and miss why the job actually matters. Push every candidate job statement to expose all three.&lt;/p&gt;

&lt;p&gt;The switch timeline. Moesta’s interview structure has five anchor moments along the customer’s path to switching. Knowing the names lets the interviewer ask for each one explicitly:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;First Thought: the moment the customer first considered making any change. Usually weeks or months before the switch. &lt;em&gt;Push&lt;/em&gt; lives here.&lt;/li&gt;
  &lt;li&gt;Passive Looking: low-effort browsing, not actively shopping yet.&lt;/li&gt;
  &lt;li&gt;Active Looking: shortlisting, comparing, asking around.&lt;/li&gt;
  &lt;li&gt;Deciding: choosing between candidates. &lt;em&gt;Anxiety&lt;/em&gt; spikes here.&lt;/li&gt;
  &lt;li&gt;Consuming: using the new thing for the first time. &lt;em&gt;Habit&lt;/em&gt; sets in or it doesn’t.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The four forces (push from the old situation, pull of the new, anxiety about switching, habit holding the customer in place) map onto these moments rather than being asked about abstractly.&lt;/p&gt;

&lt;h3 id=&quot;inputs&quot;&gt;Inputs&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Three to six recent switchers scheduled for 45-minute calls (ideally two recent customers who started, one recent canceller, or the equivalent for your switch).&lt;/li&gt;
  &lt;li&gt;A rough interview guide (we’ll give one below) but not a script.&lt;/li&gt;
  &lt;li&gt;The team to have &lt;em&gt;read&lt;/em&gt; one or two switch interview transcripts before the session so they recognise the shape.&lt;/li&gt;
  &lt;li&gt;Recording setup (with permission) and a shared document for verbatim notes.&lt;/li&gt;
  &lt;li&gt;Sticky notes and a wall for the discovery and clustering phases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t yet know which customers to talk to or what switch you’re trying to understand, run &lt;a href=&quot;/writing/the-workshop-event-storming-a-domain/&quot;&gt;Event Storming a Domain&lt;/a&gt; first to map the customer landscape, or pull a churn list from your CS team to seed the recruit.&lt;/p&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;What lands at the end:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;3–5 candidate job statements in the form &lt;em&gt;“When [situation], I want to [motivation], so I can [outcome].”&lt;/em&gt; These are the headline artefact.&lt;/li&gt;
  &lt;li&gt;A wall of verbatim quotes, clustered, with each cluster named.&lt;/li&gt;
  &lt;li&gt;Interview transcripts filed somewhere the team can read them for months. Redact names and any personal detail not relevant to the job.&lt;/li&gt;
  &lt;li&gt;Tags against the four forces: which quotes show push, which show pull, which show anxiety, which show habit. The tags are the evidence behind each job statement.&lt;/li&gt;
  &lt;li&gt;A “not our job” list, sometimes: the requests you can now deliberately decline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These outputs feed straight into:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt;: Impact Mapping tells you what behaviour to change; JTBD tells you what job the customer is hiring you for. Run JTBD first when you don’t know the job yet; run Impact Mapping first when the job is clear but the behaviour change isn’t.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt;: once the jobs are named, User Story Mapping lays out the journey through them and slices the backlog against each job.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt;: Example Mapping turns a story into concrete rules; JTBD turns a customer conversation into a story worth writing. They compose at opposite ends of the refinement pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whos-needed&quot;&gt;Who’s Needed&lt;/h3&gt;

&lt;p&gt;Four to six team members for roughly 3h 45min (with two interviews) to 4h 30min (with three):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Facilitator. Runs the session, conducts or co-conducts the interviews, keeps the discovery honest. Someone who has done switch interviews before is a large advantage. If nobody in the room has, schedule a practice interview with a friendly customer the week before.&lt;/li&gt;
  &lt;li&gt;Interviewers. Two or three, rotating. One person asks, another listens and notes, they swap between interviews. The rotation matters; it stops any single interviewer’s theory hardening into the session’s finding.&lt;/li&gt;
  &lt;li&gt;Note-taker. Often the facilitator doubles here, but if the interviews are back-to-back, split the role. The note-taker captures verbatim quotes, not paraphrases. Paraphrase is where the team’s existing theory sneaks in.&lt;/li&gt;
  &lt;li&gt;Product lead. Mandatory. The job statements will reshape the roadmap, and the product lead needs to have been in the room when they came out. If they arrive only for the readout, the statements will land as someone else’s conclusions, and they will be argued rather than used.&lt;/li&gt;
  &lt;li&gt;Optional ops / CS observer. Someone who talks to customers every day. Their job is to contradict the neat story that emerges from three interviews with the people who picked up the phone. They know the customers who didn’t, and that context keeps the discovery honest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Group size is 4–6 team members (interviewees are not counted). Below four and the clustering lacks the friction it needs; above six and the silent discovery phase becomes committee writing.&lt;/p&gt;

&lt;p&gt;Who to leave out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Large groups of stakeholders. This is not a readout session. Discovery with more than six voices collapses into consensus-seeking.&lt;/li&gt;
  &lt;li&gt;People who can’t let go of existing features. If someone is going to defend the current roadmap sentence-by-sentence during clustering, they will prevent the session from doing its job. Invite them to the readout afterwards.&lt;/li&gt;
  &lt;li&gt;Anyone who won’t suspend their theory for three hours. JTBD interviews are deliberately theory-free. Bring a theory into the listening and you’ll hear confirmation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;how-to-run-it&quot;&gt;How To Run It&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Duration&lt;/th&gt;
      &lt;th&gt;Materials&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Brief and prep&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;Interview guide, recording setup&lt;/td&gt;
      &lt;td&gt;“What are we listening for?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Switch interview 1&lt;/td&gt;
      &lt;td&gt;45 min&lt;/td&gt;
      &lt;td&gt;Phone / call, notes&lt;/td&gt;
      &lt;td&gt;“Tell me about the day you switched.”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Switch interview 2&lt;/td&gt;
      &lt;td&gt;45 min&lt;/td&gt;
      &lt;td&gt;Phone / call, notes&lt;/td&gt;
      &lt;td&gt;“Tell me about the day you switched.”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Switch interview 3 &lt;em&gt;(optional)&lt;/em&gt;&lt;/td&gt;
      &lt;td&gt;45 min&lt;/td&gt;
      &lt;td&gt;Phone / call, notes&lt;/td&gt;
      &lt;td&gt;“Tell me about the day you switched.”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Silent discovery&lt;/td&gt;
      &lt;td&gt;30 min&lt;/td&gt;
      &lt;td&gt;Sticky notes, quotes&lt;/td&gt;
      &lt;td&gt;“What did we actually hear?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cluster into candidate jobs&lt;/td&gt;
      &lt;td&gt;30 min&lt;/td&gt;
      &lt;td&gt;Clustered quotes&lt;/td&gt;
      &lt;td&gt;“What story do these clusters tell?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Name 3–5 candidate jobs&lt;/td&gt;
      &lt;td&gt;20 min&lt;/td&gt;
      &lt;td&gt;Job statement cards&lt;/td&gt;
      &lt;td&gt;“When… I want to… so I can…”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Wrap-up&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;“Who owns what next?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;~3h 45min with two interviews, ~4h 30min with three&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The interviews drive the day. Everything else is in service of extracting the job from what the switchers said. If the interviews don’t happen (schedule slips, no-shows, technical failures) postpone the discovery. Don’t fake it with remembered quotes.&lt;/p&gt;

&lt;h4 id=&quot;listening-and-discovery&quot;&gt;Listening and discovery&lt;/h4&gt;

&lt;p&gt;Two distinct modes, and the discipline of keeping them separate is most of the technique:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Listening mode. During the interviews. Open questions, long silences, “and then what happened?” nudges. No theorising, no reframing the question, no rescuing the interviewee when they stall. The pauses are where the good material comes out.&lt;/li&gt;
  &lt;li&gt;Discovery mode. After the interviews. Quotes on sticky notes, clustered by pattern, named at the end. Silent individual work first; discussion only after the clusters are visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The four forces come out in the second mode, not the first. Don’t ask interviewees about push, pull, anxiety, and habit; listen for them.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Push of the current situation. What was annoying, broken, or insufficient about what they were doing before.&lt;/li&gt;
  &lt;li&gt;Pull of the new solution. What drew them toward the new thing. What it promised.&lt;/li&gt;
  &lt;li&gt;Anxiety about the change. What made them hesitate. What they were afraid would go wrong.&lt;/li&gt;
  &lt;li&gt;Habit of the present. What made it easier to keep doing what they were already doing, even when it wasn’t working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A real switch story contains all four. The four forces are a lens for reading the transcript, not a question to ask.&lt;/p&gt;

&lt;p&gt;“Pulling them out” is the mechanic of that lens. After the interview, go back through the transcript and tag the lines that show each force. To make this concrete, here’s how it might land in a switch interview about a meal-box subscription: &lt;em&gt;“The supermarket veg kept going off before we’d eaten it”&lt;/em&gt; is push, &lt;em&gt;“My neighbour’s box looked amazing on Instagram”&lt;/em&gt; is pull, &lt;em&gt;“What if we get things we don’t know how to cook?”&lt;/em&gt; is anxiety, and &lt;em&gt;“We’d done the same Saturday shop for years”&lt;/em&gt; is habit. The interviewee never labelled any of them; they told a story, and the team tagged it afterwards. Those tags are the evidence behind the job statements you write later: when the situation clause reads &lt;em&gt;“When the weekly shop has stopped working…”&lt;/em&gt; you can point at the push quote it came from.&lt;/p&gt;

&lt;p&gt;Forces you can’t tag matter too. Strong push and weak anxiety is a switcher who was already on the way out. Strong habit and weak pull is a switcher who needs a bigger nudge than the product is currently offering. The tagging is what turns three interview stories into a map of the decision shape, not just three transcripts in a folder.&lt;/p&gt;

&lt;h4 id=&quot;phase-1-brief-and-prep-15-min&quot;&gt;Phase 1: Brief and prep (15 min)&lt;/h4&gt;

&lt;p&gt;Gather the team. Walk through three things, briefly:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“We’re going to run switch interviews. The shape is: tell me about the day you switched, walk me backwards to when you first started thinking about it, and tell me what else you considered. We’re listening for what was going on in their life when they made the change, not for feature feedback. We’re not going to ask them what they want. We’re going to ask them what happened.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Second: the four forces, one line each. Tell the team to keep the four forces in the back of their heads, not the front. The interview is not a forces-extraction machine.&lt;/p&gt;

&lt;p&gt;Third: the roles for the first interview. Who asks, who takes notes, who observes in silence. Set the expectation that roles rotate.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Pre-loading theories. &lt;em&gt;“I bet they’re going to say it’s about convenience.”&lt;/em&gt; Name it and park it: &lt;em&gt;“Let’s see what they say.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Prepared questions. Interviewers who’ve written a list of fifteen things they want to ask will interrupt the story. The guide below is three prompts, not fifteen.&lt;/li&gt;
  &lt;li&gt;Recording permission missed. If you’re recording, confirm permission explicitly at the top of the call. If you can’t record, double the note-taking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interview guide:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;“Take me back to the day you decided to switch. What was happening that day?”&lt;/li&gt;
  &lt;li&gt;“When did you first start thinking about it? What else were you considering?”&lt;/li&gt;
  &lt;li&gt;“Was there anything that almost stopped you? What made you go ahead anyway?”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else is follow-up prompts: &lt;em&gt;“tell me more about that,”&lt;/em&gt; &lt;em&gt;“what happened next,”&lt;/em&gt; &lt;em&gt;“who else was involved,”&lt;/em&gt; &lt;em&gt;“how did that feel.”&lt;/em&gt;&lt;/p&gt;

&lt;h4 id=&quot;phase-2-switch-interviews-45-min-each&quot;&gt;Phase 2: Switch interviews (45 min each)&lt;/h4&gt;

&lt;p&gt;Run the interview by phone or video. Camera on if the interviewee’s comfortable, off if not. The note-taker captures verbatim quotes in a shared document, with timestamps if the call is recorded.&lt;/p&gt;

&lt;p&gt;Ask the first question and then &lt;em&gt;wait&lt;/em&gt;. The interviewee will start. Don’t fill silences. If they stop after thirty seconds, prompt with &lt;em&gt;“and then what?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Push for the concrete scene. &lt;em&gt;“What day of the week was it? Where were you when you first thought about it? Who did you talk to?”&lt;/em&gt; Abstractions hide jobs; specifics reveal them.&lt;/p&gt;

&lt;p&gt;Walk them backwards along the timeline. When they’ve finished the story of the day itself, walk back: when they first thought about it, what they were doing before, what triggered the first thought. The “first thought” is often weeks or months before the switch, and that’s where the push usually lives.&lt;/p&gt;

&lt;p&gt;When they’re done with the timeline, ask about alternatives. &lt;em&gt;“What else did you consider? Why did you pick this one? What would have made you stay with what you had before?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Generic answers. &lt;em&gt;“It was just more convenient.”&lt;/em&gt; Push: &lt;em&gt;“Convenient how? Give me the specific thing that annoyed you last time you did it the old way.”&lt;/em&gt; A generic answer is an unearned abstraction; the story is always underneath.&lt;/li&gt;
  &lt;li&gt;Rationalised stories. The interviewee has told themselves a tidy narrative about why they switched. You’ll hear marketing language in their mouth. Rewind: &lt;em&gt;“Before you decided that, what were you actually doing?”&lt;/em&gt; Walk to the concrete scene.&lt;/li&gt;
  &lt;li&gt;Interviewers filling silences. Note-takers should kick the interviewer under the table. Thirty seconds of silence almost always produces the best quote of the interview.&lt;/li&gt;
  &lt;li&gt;The team diagnosing during the call. &lt;em&gt;“Oh: they want a pause feature.”&lt;/em&gt; No. Listening only. Diagnosis is the next phase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;End the interview cleanly. Thank them. Don’t summarise back to them; summaries bias the memory of what they said.&lt;/p&gt;

&lt;h4 id=&quot;phase-3-silent-discovery-30-min&quot;&gt;Phase 3: Silent discovery (30 min)&lt;/h4&gt;

&lt;p&gt;Print or project the transcripts. Each team member works alone. The instruction is one sentence:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Write every verbatim quote that feels telling onto a sticky note. One quote per note. No interpretation.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Telling means: reveals a push, a pull, an anxiety, a habit, a moment of decision, a named alternative, an outcome they were trying to achieve. If the quote is about a feature they wanted, it’s probably not telling; that’s a solution, not a job.&lt;/p&gt;

&lt;p&gt;Silent, individual, no discussion. Set a timer. When the timer ends, everyone posts their notes on the wall without comment.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Paraphrase creep. Someone writes &lt;em&gt;“customer wants convenience”&lt;/em&gt; on a note. That’s paraphrase. Push back: &lt;em&gt;“What did they actually say? Use their words.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Feature requests mistaken for jobs. &lt;em&gt;“They said they want a weekly summary.”&lt;/em&gt; That’s a solution. The question underneath is what the weekly summary is being hired to do.&lt;/li&gt;
  &lt;li&gt;One team member producing twice as many notes as anyone else. Good. Don’t suppress it. The clustering will balance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-4-cluster-into-candidate-jobs-30-min&quot;&gt;Phase 4: Cluster into candidate jobs (30 min)&lt;/h4&gt;

&lt;p&gt;Look at the wall. Ask the room to cluster notes that belong together. No talking for the first five minutes (the affinity-map convention: silently group sticky notes by similarity, then name the clusters). People move notes silently, and if two people keep moving the same note back and forth, it’s flagged for discussion.&lt;/p&gt;

&lt;p&gt;After five silent minutes, open the conversation. For each cluster, ask two questions:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What’s the pattern here? What are these quotes all saying?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Is this a situation, a motivation, or an outcome?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those three words (situation, motivation, outcome) are the JTBD shape. A cluster might be &lt;em&gt;all situations&lt;/em&gt; (things that were going on in customers’ lives), &lt;em&gt;all motivations&lt;/em&gt; (what they were trying to get done), or &lt;em&gt;all outcomes&lt;/em&gt; (what they wanted to be true afterwards). Often a single cluster contains one of each and is the seed of a job statement.&lt;/p&gt;

&lt;p&gt;Expect 4–7 clusters from three interviews. Fewer than four and you’ve over-abstracted; more than seven and you haven’t clustered enough.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Everything-is-one-cluster. The room collapses the wall into two huge piles. Push for distinctions: &lt;em&gt;“What’s different about these two quotes?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;The wishlist cluster. A cluster forms around feature requests. Re-frame: &lt;em&gt;“If we built all of these, what would customers be able to do that they can’t do now?”&lt;/em&gt; The answer is usually the job.&lt;/li&gt;
  &lt;li&gt;Forgotten negative space. Quotes about anxiety and habit rarely cluster on their own unless you prompt for them. &lt;em&gt;“Which of these clusters contains ‘what almost stopped them’? Is that cluster complete?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-5-name-35-candidate-jobs-20-min&quot;&gt;Phase 5: Name 3–5 candidate jobs (20 min)&lt;/h4&gt;

&lt;p&gt;Pick the 3–5 strongest clusters and turn each into a job statement. The form is strict:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“When [situation], I want to [motivation], so I can [outcome].”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is Klement’s &lt;em&gt;job story&lt;/em&gt; form (Alan Klement, who refined the JTBD interview practice into the modern job-story shape), which bakes the situation in. Christensen’s classical &lt;em&gt;job statement&lt;/em&gt; form is shorter, &lt;em&gt;“Help me [verb] [object] [modifier]”&lt;/em&gt;, and useful for headline framing. We use Klement’s form here because the situation is what the four-forces evidence directly supports.&lt;/p&gt;

&lt;p&gt;Each slot is a concrete phrase, not a category.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Situation:&lt;/em&gt; the context the customer is in when the job arises. Time, place, people, constraints.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Motivation:&lt;/em&gt; the action they want to take. A verb and an object.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Outcome:&lt;/em&gt; the state of the world they want to be true as a result. What they get to do next, how they want to feel, what they no longer have to worry about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write each statement on a card the whole room can see. Read it aloud. Challenge it against the quotes: does any sentence in the transcripts contradict the statement? If yes, adjust.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Abstract outcomes. &lt;em&gt;“So I can be happy.”&lt;/em&gt; Push for what happy looks like. &lt;em&gt;“So I can stop having to think about dinner on Sunday.”&lt;/em&gt; That’s a specific outcome.&lt;/li&gt;
  &lt;li&gt;Product names in the statement. &lt;em&gt;“So I can use our app.”&lt;/em&gt; No, that’s a solution. &lt;em&gt;“So I can plan the week without a grocery trip.”&lt;/em&gt; That’s the job.&lt;/li&gt;
  &lt;li&gt;Two jobs in one statement. &lt;em&gt;“When it’s busy, I want to plan the week and also try new recipes, so I can feed the family without stress.”&lt;/em&gt; Split into two statements. One job per card.&lt;/li&gt;
  &lt;li&gt;Committee wording. The room rewrites the same statement four times. Park it. Accept the rough version and move on; polish later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-6-wrap-up-10-min&quot;&gt;Phase 6: Wrap-up (10 min)&lt;/h4&gt;

&lt;p&gt;Pin the 3–5 job statement cards on the wall. Photograph them. Read each aloud one more time with the team. Then name the owners:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Product lead: these are yours from here. Ops observer: you’re running the sanity check against what you hear on calls next week. Engineering lead: I’ll walk these past you Monday.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;End on commitments, not summaries.&lt;/p&gt;

&lt;h4 id=&quot;worked-example&quot;&gt;Worked example&lt;/h4&gt;

&lt;p&gt;See &lt;a href=&quot;/writing/jobs-to-be-done-why-subscribers-actually-stay/&quot;&gt;Jobs to be Done: Why Subscribers Actually Stay&lt;/a&gt; for a fictional team’s first switch-interview session, including the moment three interviewees independently describe the same Sunday-night job the team had never heard named. The product in that story is a meal-box subscription, but the shape of the session is the same in any domain.&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What Can Go Wrong&lt;/h3&gt;

&lt;p&gt;The feature wishlist. The interview turns into a list of features the customer wants.
  &lt;em&gt;Recovery:&lt;/em&gt; Interrupt gently: &lt;em&gt;“Let me take a step back: what were you doing before you switched? Walk me through that week.”&lt;/em&gt; Pull them back to the timeline.
  &lt;em&gt;Stop if:&lt;/em&gt; The same interviewee keeps returning to features despite three rewinds. Thank them, end the call, and try a different interviewee.&lt;/p&gt;

&lt;p&gt;The generic answer. Everything is “convenience” or “quality” or “the vibe.”
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“When you say convenience, what did the morning of your Monday look like before, versus after?”&lt;/em&gt; Specifics always.
  &lt;em&gt;Stop if:&lt;/em&gt; They genuinely can’t recall. They’re probably not a recent switcher: check when they actually switched.&lt;/p&gt;

&lt;p&gt;The rationalised story. The interviewee has a clean narrative that sounds like your own marketing.
  &lt;em&gt;Recovery:&lt;/em&gt; Walk to the concrete scene. &lt;em&gt;“Before you decided that, what were you actually doing on a Tuesday night?”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; They resist the concrete scene. Rationalisation is often protective; don’t force it.&lt;/p&gt;

&lt;p&gt;Jobs conflated with solutions. During discovery, someone keeps writing job statements that include the product.
  &lt;em&gt;Recovery:&lt;/em&gt; Delete the product name and see if the statement still holds. If it doesn’t, it’s not a job; it’s a feature brief.
  &lt;em&gt;Stop if:&lt;/em&gt; The whole wall is solution-shaped. The interviews didn’t produce enough material; schedule more.&lt;/p&gt;

&lt;p&gt;The wording committee. Four people argue about the wording of a single statement for twenty minutes.
  &lt;em&gt;Recovery:&lt;/em&gt; Force a rough version. &lt;em&gt;“Worst acceptable version. We’ll polish next week.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The argument is actually about whether the job is real. Go back to the quotes and check.&lt;/p&gt;

&lt;p&gt;Confirmation bias. The room is finding what it already believed.
  &lt;em&gt;Recovery:&lt;/em&gt; Ask the ops / CS observer to challenge every statement against the customers they talk to. &lt;em&gt;“Would anyone you speak to on the phone recognise themselves in this?”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; Two observers independently say the statements don’t match what they hear. The interviews may be unrepresentative; schedule different interviewees.&lt;/p&gt;

&lt;p&gt;Other failure modes worth naming so you can spot them early:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The team treats three interviews as definitive and skips follow-up&lt;/li&gt;
  &lt;li&gt;The statements get written and shelved; the roadmap continues as before&lt;/li&gt;
  &lt;li&gt;Interviewers slide into persuasion mode and start explaining the product to the interviewee&lt;/li&gt;
  &lt;li&gt;Discovery collapses into consensus around the theory the product lead walked in with&lt;/li&gt;
  &lt;li&gt;Quotes get paraphrased into notes and the verbatim material is lost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The session has costs as well as benefits, and naming them helps the team commit honestly: 4–5 hours of session time plus 3–4 hours of interview scheduling and coordination, the emotional cost of hearing customers describe problems you haven’t solved, and the fact that the candidate job statements are &lt;em&gt;candidates&lt;/em&gt;; they want validation with more interviews before they drive anything irreversible. Interviewer skill compounds; early sessions produce rougher material.&lt;/p&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;

&lt;p&gt;The session ends; the work begins.&lt;/p&gt;

&lt;p&gt;Same day, the facilitator:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Photographs of the wall: the full cluster layout, each candidate job card in close-up, the verbatim quote notes against their clusters.&lt;/li&gt;
  &lt;li&gt;Files the interview transcripts somewhere the team can read them for months. Redact names and any personal detail not relevant to the job.&lt;/li&gt;
  &lt;li&gt;Writes a short summary: the 3–5 candidate jobs, one sentence each about the strongest quote behind each, and the four forces where they showed up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This week, the product lead:&lt;/p&gt;

&lt;p&gt;This is where the pattern earns its cost, and the work is mostly the product lead’s.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Walk the candidate jobs past the ops / CS team. People who talk to customers every day will either nod or wince. Both reactions are useful. The wince is more useful.&lt;/li&gt;
  &lt;li&gt;Schedule three more interviews to validate the strongest candidate. Three interviews is not enough to commit; three more either strengthen the statement or reveal the hole. Treat the first session’s output as a hypothesis.&lt;/li&gt;
  &lt;li&gt;Map the current roadmap against the jobs. Which features serve a named job? Which don’t? A feature that doesn’t serve any job is either a job you haven’t articulated yet or work that shouldn’t be in the quarter.&lt;/li&gt;
  &lt;li&gt;Refuse the next feature request that doesn’t match a job. Politely. With a reason. This is the hardest week-after task and the one that makes JTBD earn its keep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ongoing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Re-run JTBD when the product shape changes materially: a new segment, a new pricing tier, a new acquisition channel. The jobs change when the customers change.&lt;/li&gt;
  &lt;li&gt;Keep the job statements visible. Pin them in the team’s main room. When someone proposes a feature, they should be able to point at the job it serves.&lt;/li&gt;
  &lt;li&gt;Track the feature requests that don’t match any job. If the list grows, you’re either missing a job or missing the discipline to say no.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The benefits compound: job statements concrete enough to make product decisions (features that serve a named job get built; features that don’t get parked), a shared framing across product, engineering, and operations that reduces backlog churn, interview transcripts that earn their keep for months as future hires read them and onboard faster, a “not our job” list that is just as valuable as the jobs themselves, and the push / pull / anxiety / habit lens available for every future product conversation.&lt;/p&gt;

&lt;h3 id=&quot;variants&quot;&gt;Variants&lt;/h3&gt;

&lt;p&gt;The default switch-interview shape captures one direction: people who chose the product. Two adjacent shapes capture the jobs you’re missing: customers who left, and prospects who never started.&lt;/p&gt;

&lt;p&gt;Switch-out interviews (churn). Run the same playbook with people who cancelled in the last ninety days. The prompts adapt:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;em&gt;“Take me back to the day you decided to cancel. What was happening that week?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“When did you first start thinking about it? What pushed you over the edge?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“What are you doing now instead? Did you switch to something else, or go back to what you had before?”&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The four forces re-orient. Push is what &lt;em&gt;your product&lt;/em&gt; was doing wrong: the feature that broke, the support reply that landed badly, the price increase that finally tipped them over. Pull is the destination, which is often &lt;em&gt;nothing&lt;/em&gt;: the cancelled customer went back to the way they did it before, not to a competitor. That’s a stronger signal than competitive churn; it means the job you thought you were doing wasn’t being done well enough to displace the old way at all. Anxiety is what made cancelling hard: the workflow they’d built around the product, the data or history they’d lose, the loyalty discount they’d give up. Habit is the inertia that kept them paying past the point of value: how many months did the bill go out after they’d stopped really using it?&lt;/p&gt;

&lt;p&gt;A churn interview where the canceller went back to the way they did it before is the most useful kind you can run. It tells you the job you wrote down isn’t real, or isn’t being delivered. The team won’t want to hear it; the temptation will be to dismiss the canceller as not the target. Resist.&lt;/p&gt;

&lt;p&gt;Non-adoption interviews. People who looked at the product and didn’t sign up, or fit the audience and never engaged. Harder to recruit (you don’t have their email) but the most valuable shape when growth has stalled and churn doesn’t explain the shortfall.&lt;/p&gt;

&lt;p&gt;The prompts shift, because there’s no “day they switched”:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;em&gt;“Tell me about the last time you thought about a product like ours. What was happening?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“What did you end up doing instead?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“What stopped you from trying it?”&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The forces re-orient again, and the &lt;em&gt;missing&lt;/em&gt; forces are the finding. Push is what’s not working about whatever they’re using today; usually it’s weak, because the existing alternative is an adequate solution for adequate people. Pull is what your product promised them; usually it’s weak too, because if pull had been strong they’d have signed up. Anxiety is what stopped them: what if it doesn’t fit how they actually work, what if they can’t get value out of it, what if it locks them in. Habit is the strongest force in this set: most non-adopters are well served by what they’ve used for years, and the real question is whether anything could ever move them.&lt;/p&gt;

&lt;p&gt;Recruit non-adopters by:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Asking churned customers to introduce contacts who &lt;em&gt;also&lt;/em&gt; considered the product but didn’t sign up&lt;/li&gt;
  &lt;li&gt;Running a short paid screener through a research panel&lt;/li&gt;
  &lt;li&gt;Offering a small incentive through channels where the audience you serve already gathers&lt;/li&gt;
  &lt;li&gt;Using mutual connections, carefully, and never as a sales channel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three non-adopter interviews are harder to schedule than ten switch interviews, but the missing jobs they reveal are the ones the rest of the playbook can’t see.&lt;/p&gt;

&lt;p&gt;When to run which:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Switch-in only. A new team learning the technique, or a product where adoption and retention are both healthy.&lt;/li&gt;
  &lt;li&gt;Switch-in plus switch-out. The default for a team that wants the full picture of who they keep and who they lose. Run a session of each in the same fortnight; make sense of each separately, then compare the job statements.&lt;/li&gt;
  &lt;li&gt;All three. When growth has plateaued and churn data alone doesn’t explain it. The non-adopter shape is the one that finds the job you haven’t named yet.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Does Time Even Exist?</title>
    <link href="/writing/does-time-even-exist/"/>
    <updated>2026-05-07T06:00:00+08:00</updated>
    <id>/writing/does-time-even-exist/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;&lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;Time Is Weirder Than You Think&lt;/a&gt; showed time bending near mass, dilating with motion, rippling when black holes collide, always as a thing that exists. This post asks whether it does. The arrow that distinguishes past from future isn’t in the equations. “Now” isn’t a location in spacetime. The equations of quantum gravity may contain no time variable at all. Some physicists think time is a shadow of something simpler. A few think it has more dimensions than we can see. A handful think it doesn’t fundamentally exist.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-arrow-of-time&quot;&gt;The arrow of time&lt;/h3&gt;

&lt;p&gt;At the quantum level, the equations of physics are mostly time-symmetric: they work just as well running backwards. Maxwell’s equations, the Schrodinger equation, even the equations of general relativity: none of them distinguish past from future. Run the film backwards and the physics still works. Yet we experience time as having a clear direction. Eggs break but don’t unbreak. You remember yesterday but not tomorrow. What gives time its arrow?&lt;/p&gt;

&lt;p&gt;The standard answer involves entropy: roughly, the disorder of a system. There are astronomically more ways for an egg to be broken than for it to be perfectly intact. A broken egg isn’t going to spontaneously reassemble, not because the laws of physics forbid it, but because the odds against it are absurdly, comically enormous. This is the second law of thermodynamics: things tend to move from ordered states to disordered ones, because disordered states are overwhelmingly more probable.&lt;/p&gt;

&lt;p&gt;But this just pushes the question back a step: &lt;em&gt;why&lt;/em&gt; does entropy increase? The second law is statistical, not fundamental; it says that higher-entropy states are more probable, so systems tend to evolve toward them. But that only works if the universe &lt;em&gt;started&lt;/em&gt; in a low-entropy state, a highly ordered initial condition. Why did it? This is one of the deepest unsolved problems in physics, and it sits at the intersection of cosmology, thermodynamics, and the foundations of quantum mechanics. Roger Penrose has devoted much of his career to it; he estimates the probability of the universe’s initial low-entropy state arising by chance at roughly 1 in 10^(10^123), a number so absurdly large that writing it out would require more paper than exists in the observable universe.&lt;/p&gt;

&lt;p&gt;The arrow of time, on this view, isn’t a property of the equations; it’s a property of the initial condition. The universe was handed an astronomically improbable starting state, and everything since has been the slow unwinding of that order into disorder. Take away that initial condition and the arrow vanishes.&lt;/p&gt;

&lt;h3 id=&quot;the-block-universe&quot;&gt;The block universe&lt;/h3&gt;

&lt;p&gt;At the cosmic level, time is inseparable from space. General relativity describes them as a single four-dimensional fabric, spacetime, that can be curved, stretched, and warped by mass and energy. The notion of “now” is surprisingly hard to define across large distances. In special relativity, simultaneity is relative. Two lightning bolts strike opposite ends of a train simultaneously, from the platform’s point of view. A passenger on the train, moving toward one bolt and away from the other, sees them hit at different times, and according to relativity, both observers are equally right. There is no universal “now”. There is only &lt;em&gt;your&lt;/em&gt; now, defined by your position and velocity, and it disagrees with everyone else’s.&lt;/p&gt;

&lt;p&gt;This leads some physicists to the block universe interpretation: the idea that past, present, and future all exist equally and simultaneously. The four-dimensional spacetime block simply &lt;em&gt;is&lt;/em&gt;, complete and unchanging. What we experience as the flow of time is an artefact of our consciousness moving through this block. In this view, the future is as real as the past; we just haven’t encountered it yet.&lt;/p&gt;

&lt;p&gt;It’s a view that Einstein himself appears to have held. After the death of his lifelong friend Michele Besso in 1955, Einstein wrote to Besso’s family: “For those of us who believe in physics, the distinction between past, present, and future is only a stubbornly persistent illusion.”&lt;/p&gt;

&lt;p&gt;If the block universe is right, there’s no such thing as the flow of time. There’s only a static four-dimensional structure, and the appearance of passage is something our brains impose on it. Which is uncomfortable, because the passage of time feels like the most obvious thing in the world.&lt;/p&gt;

&lt;h3 id=&quot;the-beginning-of-time&quot;&gt;The beginning of time&lt;/h3&gt;

&lt;p&gt;If time bends near mass and stops at an event horizon, what happened at the Big Bang, the most extreme gravitational event of all? In 1983, Stephen Hawking and James Hartle proposed that the question is malformed. In their no-boundary proposal, as you trace time back toward the Big Bang, the distinction between time and space dissolves. Time doesn’t hit a wall or a starting gun. It smoothly becomes something more like a spatial dimension: rounded off, with no edge and no “before.”&lt;/p&gt;

&lt;p&gt;Hawking’s analogy: asking what happened before the Big Bang is like asking what’s south of the South Pole. You can walk south from anywhere on Earth, and at every step there’s more south ahead of you, until you reach the pole, where “south” doesn’t end in a wall. The concept simply stops applying. There’s no sign saying “end of south.” There’s just a smooth surface that curves in a way that makes the question dissolve. Time at the Big Bang, in the Hartle-Hawking model, does the same thing. The universe didn’t begin at a first moment. The geometry of spacetime curves in a way that removes the need for a first moment.&lt;/p&gt;

&lt;h3 id=&quot;every-possible-history-all-at-once&quot;&gt;Every possible history, all at once&lt;/h3&gt;

&lt;p&gt;The no-boundary proposal isn’t just a clever picture. It’s calculated using a technique from quantum mechanics called the path integral, an idea Feynman developed in the 1940s. Here’s the intuition.&lt;/p&gt;

&lt;p&gt;Normally, if you want to know how a ball gets from point A to point B, you calculate the one path it takes: the arc through the air that Newton’s laws dictate. Feynman showed that in quantum mechanics, this is wrong. The ball takes &lt;em&gt;every possible path simultaneously&lt;/em&gt;: straight lines, spirals, loops, detours through the next room and back. Every path contributes to the outcome. Most of them cancel each other out, and what survives is something that looks very much like Newton’s single arc. But the cancellation is the reason, not the single path.&lt;/p&gt;

&lt;p&gt;Now apply this to the universe. In quantum cosmology, the universe didn’t take one history from the Big Bang to now. It took every possible history: every possible geometry of spacetime, every possible arrangement of matter and energy, all at once. Some of those histories have time that looks like ours. Some have radically different causal structures. Some might have multiple time dimensions, or looping time, or no time at all. What we observe is the interference pattern of all of them.&lt;/p&gt;

&lt;p&gt;It’s like a choir. A hundred singers each sing a different note. Most of the notes clash and cancel. What the audience hears isn’t silence; it’s a chord. The chord is our universe. The individual notes are the histories that were summed over to produce it. Our experience of time, flowing forward, one second after another, is the chord that survived the cancellation. It’s not the only note that was sung.&lt;/p&gt;

&lt;h3 id=&quot;hawkings-last-act&quot;&gt;Hawking’s last act&lt;/h3&gt;

&lt;p&gt;Hawking spent his final years refining this picture with Thomas Hertog. Their 2018 paper, submitted just weeks before Hawking’s death, used the holographic principle (more on that in a moment) to argue that the multiverse, if it exists, is far more constrained than the “anything goes” version popular in science fiction. Different regions of the universe might settle into different vacuum states (different stable configurations of the fundamental fields) and each vacuum state could have different effective physics. Different particle masses. Different force strengths. Possibly different properties of time itself.&lt;/p&gt;

&lt;p&gt;This isn’t parallel universes in the Star Trek sense. It’s more like ice forming on a pond. Water can crystallise in different orientations, and different patches of ice have their crystals aligned differently. Same water, same physics, different local structure. Hawking and Hertog proposed that the universe is the same way: one underlying theory, but different regions that “froze” into different configurations. Time in one region might tick with subtly different properties than time in another, not because the laws are different, but because the local vacuum is.&lt;/p&gt;

&lt;h3 id=&quot;the-holographic-principle&quot;&gt;The holographic principle&lt;/h3&gt;

&lt;p&gt;In 1993, Gerard ‘t Hooft proposed, and Leonard Susskind later developed, an idea that sounds absurd: all the information in a three-dimensional region of space can be encoded on its two-dimensional boundary. Like a hologram on a credit card that looks three-dimensional but is physically flat.&lt;/p&gt;

&lt;p&gt;This wasn’t metaphor. It grew out of Hawking’s own work on black holes. Hawking showed in 1974 that black holes radiate, slowly evaporating over astronomical timescales. Jacob Bekenstein had shown that a black hole’s entropy (roughly, the amount of information it contains) is proportional to the &lt;em&gt;area&lt;/em&gt; of its event horizon, not its volume. That’s deeply strange. The information content of a room is proportional to its volume: more room, more stuff, more information. But for a black hole, it’s the surface that matters. The interior is, informationally speaking, redundant.&lt;/p&gt;

&lt;p&gt;If this holds generally (and there’s strong theoretical evidence that it does) then our entire three-dimensional experience, time included, might be a projection from a lower-dimensional boundary. Consider a shadow puppet show. Puppets move in 3D behind the screen. The audience sees 2D shadows on the wall. The holographic principle says something far stranger: the 2D shadow might be the fundamental reality, and the 3D puppet is the projection. We’re the audience &lt;em&gt;and&lt;/em&gt; the shadow, convinced we live in 3D because the projection is so convincing.&lt;/p&gt;

&lt;p&gt;What does this mean for time? On the boundary, time might work differently, or might not exist in the form we recognise. The “bulk” (our 3+1 dimensional experience) and the boundary encode the same information, but the encoding is radically different. The best-studied example is the AdS/CFT correspondence, discovered by Juan Maldacena in 1997, which shows an exact mathematical equivalence between a gravitational theory in a curved spacetime and a quantum field theory on its boundary: a theory that has no gravity at all. Same physics. Completely different description. In one description, time curves and dilates near massive objects. In the other, there’s no gravity to curve anything. Both are equally correct. They’re not two approximations of the same thing; they’re two exact descriptions of &lt;em&gt;the same thing&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;two-times&quot;&gt;Two times&lt;/h3&gt;

&lt;p&gt;If time can be a projection of something simpler, can it also be a shadow of something &lt;em&gt;richer&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;Itzhak Bars at the University of Southern California has been developing a framework called two-time physics since the late 1990s. The idea: our universe has not four dimensions (three space, one time) but six: four of space and two of time. We can’t perceive the extra dimensions directly, any more than a shadow on a wall can perceive the lamp behind it. Our 3+1 dimensional experience is a particular &lt;em&gt;projection&lt;/em&gt; of the 4+2 dimensional reality.&lt;/p&gt;

&lt;p&gt;Here’s what makes it interesting. A 3D object casts different 2D shadows depending on the angle of the light. A cube’s shadow can look like a square, a hexagon, or a diamond. Same object, different projections, each one a valid 2D description. Bars showed that the same 4+2 dimensional physics, projected differently, gives different 3+1 dimensional theories: theories that look completely unrelated but are secretly the same underlying reality seen from different angles. Some of those projections have a time dimension that behaves like ours. Others have time that works differently. All are equally valid shadows of the same six-dimensional object.&lt;/p&gt;

&lt;p&gt;This is speculative. There’s no experimental evidence for two time dimensions, and the framework is constructed to be mathematically consistent rather than empirically motivated. But it’s a legitimate research programme, published in peer-reviewed journals, and it demonstrates something important: our assumption that there’s exactly one time dimension is a choice, not a logical necessity. The mathematics works perfectly well with more.&lt;/p&gt;

&lt;h3 id=&quot;time-loops&quot;&gt;Time loops&lt;/h3&gt;

&lt;p&gt;General relativity doesn’t just allow time to slow down or speed up. Under certain conditions, it permits time to form closed loops: paths through spacetime that return to their own starting point. Gödel found the first one in 1949. Spinning black holes have them. Wormholes might too. Hawking took the idea seriously enough to propose a law of physics to prevent it. It gets &lt;a href=&quot;/writing/can-you-turn-back-time/&quot;&gt;much stranger from there&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;time-crystals&quot;&gt;Time crystals&lt;/h3&gt;

&lt;p&gt;Start with the warm-up. Salt is a crystal because its atoms sit in a repeating pattern: atom, gap, atom, gap, atom, gap. Nothing in the laws of physics insists they line up that way; they just do, because the arrangement is stable. The pattern is in space.&lt;/p&gt;

&lt;p&gt;In 2012, Frank Wilczek (a Nobel laureate) asked the obvious next question: could a pattern repeat in &lt;em&gt;time&lt;/em&gt; instead? Could a system tick, tick, tick forever on its own preferred schedule, in its lowest energy state, with no energy input?&lt;/p&gt;

&lt;p&gt;This was controversial. A system oscillating in its ground state would seem to violate the expectation that ground states are static: nothing happening, no change, as boring as physics gets. But in 2017, two teams independently built time crystals in the lab. One at Harvard using a chain of ytterbium ions, another at the University of Maryland using a different approach. The trick was a clever sleight of hand. You can’t just shake something and call it a time crystal, because then it’s only dancing to your beat. The Harvard and Maryland teams drove their systems at one speed and watched them respond at a &lt;em&gt;different&lt;/em&gt;, slower speed: tap once a second, tick once every two seconds. That mismatch is the giveaway. The rhythm comes from inside the system, not from the experimenter. Time-translation symmetry (the assumption that the laws of physics are the same from one moment to the next) was broken.&lt;/p&gt;

&lt;p&gt;Ordinary crystals break spatial symmetry: space looks the same in every direction, but inside a crystal, some directions are special. Time crystals do the same thing to time: time flows the same way from moment to moment, but inside the crystal, some moments are special. The crystal has a rhythm the underlying laws don’t require. It’s a genuinely new kind of stable arrangement of matter, a new “phase” alongside solid, liquid, gas, and magnet. We didn’t know matter could organise itself in time the way it organises itself in space. Now we know it can.&lt;/p&gt;

&lt;p&gt;It’s tempting to read this as evidence that time itself is chunky, that the universe has a preferred beat hidden in it somewhere. It isn’t. The discreteness lives in the &lt;em&gt;system’s state&lt;/em&gt;, not in time. Same as salt: atoms sit at specific spots, but the space between them is still a smooth continuum. The pattern is in the matter, not in the stage the matter sits on.&lt;/p&gt;

&lt;p&gt;Whether the stage itself has a smallest possible tick (whether time is smooth all the way down, or whether the universe has a frame rate) is a different question entirely.&lt;/p&gt;

&lt;h3 id=&quot;the-smallest-tick&quot;&gt;The smallest tick&lt;/h3&gt;

&lt;p&gt;Is there a shortest possible moment? A tick so small that “before” and “after” stop meaning anything?&lt;/p&gt;

&lt;p&gt;Maybe. It’s called the Planck time, and it’s about 5.4 × 10⁻⁴⁴ seconds. To get a feel for how small that is: the ratio between one Planck time and one second is roughly the same as the ratio between one second and a hundred trillion trillion times the current age of the universe. It’s not a duration anyone has measured or ever will measure. It’s more like a speed limit sign at the edge of the map: our best theories of physics say “beyond here, we don’t know what happens.”&lt;/p&gt;

&lt;p&gt;The number comes from combining three fundamental constants, the speed of light, the gravitational constant, and Planck’s constant, in the only way that gives you a unit of time. It’s the scale where quantum mechanics and gravity would both matter simultaneously, and right now we don’t have a theory that handles both at once. Our two best frameworks, quantum mechanics (which explains the very small) and general relativity (which explains the very massive), give contradictory answers at this scale.&lt;/p&gt;

&lt;p&gt;Some physicists think the Planck time is a real boundary: that time is genuinely granular at this level, like pixels on a screen. Below one Planck time, there’s no “shorter.” Others think time is smooth all the way down and the Planck time is just where our equations stop working, not where time itself stops. We don’t know. We’re nowhere near being able to test it. But it’s a striking thought: the universe might have a frame rate.&lt;/p&gt;

&lt;h3 id=&quot;does-time-exist-at-all&quot;&gt;Does time exist at all?&lt;/h3&gt;

&lt;p&gt;Some physicists have gone further. Julian Barbour, in &lt;em&gt;The End of Time&lt;/em&gt;, argued that time doesn’t fundamentally exist. What we call time is just the way we experience the relationships between configurations of matter. The universe doesn’t evolve &lt;em&gt;through&lt;/em&gt; time; it simply &lt;em&gt;is&lt;/em&gt; a collection of states, and our brains string them into a narrative.&lt;/p&gt;

&lt;p&gt;Carlo Rovelli, in &lt;em&gt;The Order of Time&lt;/em&gt;, takes a related but more nuanced position: time as we experience it (flowing, universal, directed) is an emergent property that arises from our limited perspective as macroscopic beings who interact with the world thermodynamically. At the most fundamental level of quantum gravity, the equations may contain no time variable at all.&lt;/p&gt;

&lt;p&gt;When physicists try to write down an equation that combines quantum mechanics and gravity, the so-called Wheeler-DeWitt equation, they get something startling: the equation has no time variable at all. It describes a universe where nothing changes. How you get from a timeless equation to our everyday experience of things happening one after another is, to put it mildly, an open question.&lt;/p&gt;

&lt;p&gt;This is philosophy as much as physics, and it’s nowhere near settled experimentally. But it illustrates how deep the rabbit hole goes. We started with a simple question, “what time is it?”, and ended up with equations in which time has no place.&lt;/p&gt;

&lt;h3 id=&quot;where-this-leaves-us&quot;&gt;Where this leaves us&lt;/h3&gt;

&lt;p&gt;None of the foundations in this post are settled. The block universe is an interpretation, not a measurement. The no-boundary proposal is a model, not a verdict. The holographic principle has strong theoretical support but no direct experimental test. Two-time physics is consistent mathematics without empirical backing. Time crystals exist, but they’re a curiosity rather than a revolution. The Planck time is a scale we can’t probe. The Wheeler-DeWitt equation has no time variable, and nobody knows what to do about that.&lt;/p&gt;

&lt;p&gt;What all of them share is the unsettling implication that the time we experience (flowing, directed, universal, one thing after another) might be a surface feature of something deeper. The equations don’t need the arrow. “Now” isn’t in the maths. The fundamental theories we have either don’t mention time or treat it as a dimension no more special than space.&lt;/p&gt;

&lt;p&gt;And yet we live in time. Things happen. The egg breaks and doesn’t unbreak. You remember yesterday and not tomorrow. Whatever time is fundamentally, emergently, or not-at-all, our experience of it is real enough to live by.&lt;/p&gt;

&lt;p&gt;There’s one more direction the equations let us push, and it’s the direction most people would actually want to use a time machine for. Not forward (forward is easy and we’ve &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;covered it&lt;/a&gt;). Backward.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/writing/can-you-turn-back-time/&quot;&gt;Can You Turn Back Time?&lt;/a&gt; is next, and the equations are more permissive than you’d expect.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Choosing Between Prompting, RAG, and Fine-Tuning</title>
    <link href="/writing/choosing-between-prompting-rag-and-fine-tuning/"/>
    <updated>2026-05-06T06:00:00+08:00</updated>
    <id>/writing/choosing-between-prompting-rag-and-fine-tuning/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;AI Practitioner&lt;/strong&gt; · AIF-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;The in-house legal team maintains 4,000 contract templates across 12 jurisdictions and 30 contract types (employment, NDA, master services, licensing, etc.). Each template is between 5 and 80 pages. They live in SharePoint today; an S3 bucket is being stood up to mirror them. Templates change: roughly 50 are updated every month when a jurisdiction’s law changes or a clause is renegotiated at the enterprise level.&lt;/p&gt;

&lt;p&gt;Paralegals currently find the correct clause by searching SharePoint for keywords and skimming results. The turnaround for “what’s the standard force majeure language for a French SaaS contract?” is 10-15 minutes of human grepping. Legal-ops wants this under 30 seconds with a citation back to the exact template and clause.&lt;/p&gt;

&lt;p&gt;A first prototype called Claude with the question in the prompt. It hallucinated, clauses that sounded right but used clause numbers the templates don’t use, jurisdictions the clause doesn’t cover, and in one case a citation to a template that doesn’t exist. Three techniques are on the table to fix it: prompt engineering (rewrite the prompt better), RAG (retrieve relevant template excerpts and include them in the prompt), and fine-tuning (train the &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; on the template corpus until it knows the language natively). The team want a decision.&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;The three techniques are not interchangeable. They solve different problems, and the first mistake is treating them as points on a single “quality” axis.&lt;/p&gt;

&lt;p&gt;Prompt engineering is changing the text you send to the model. Better instructions, worked examples in the prompt (few-shot), explicit format requirements, a &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-system-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-system-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;system prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-system-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-system-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;System prompt&lt;/span&gt;The instruction block that frames the model’s behaviour for a session, separate from the user’s messages.
&lt;/span&gt; that sets the model’s persona. It costs nothing in infrastructure, no &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-training&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-training-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;training&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-training&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-training-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Training&lt;/span&gt;The process of fitting a model’s weights to data by minimising a loss function.
&lt;/span&gt; run, no new data pipeline, though iterating on the wording can take hours to days of engineering time. It is also the only technique that works if what the model is producing is &lt;em&gt;the wrong shape&lt;/em&gt;, too long, too short, wrong tone, missing a required field. A hallucinating model doesn’t need a better prompt alone; it needs &lt;em&gt;information it doesn’t have&lt;/em&gt;. Prompt engineering is necessary, always, but rarely sufficient on its own for a knowledge-grounding problem.&lt;/p&gt;

&lt;p&gt;Retrieval-augmented generation is pulling relevant documents into the prompt at query time. The model doesn’t need to &lt;em&gt;know&lt;/em&gt; the 4,000 templates; it needs to be &lt;em&gt;handed&lt;/em&gt; the correct three when the paralegal asks a question. The architecture is: pre-compute &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt; &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embeddings&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; of each template chunk (a chunk being a paragraph, a section, or a page), store them in a vector database, and at query time embed the user’s question, retrieve the top-k most similar chunks, and include them in the model’s prompt along with the question. The model answers from the retrieved text. If the retrieval is good, &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-hallucination&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-hallucination-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;hallucination&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-hallucination&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-hallucination-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Hallucination&lt;/span&gt;An LLM stating something false with the same confidence it states something true.
&lt;/span&gt; drops to near-zero on questions the corpus can answer.&lt;/p&gt;

&lt;p&gt;Fine-tuning adjusts the weights of the model on a training dataset. For generative models, this usually means providing input-output pairs (“when you see X, produce Y”) and running a training job that nudges the model’s behaviour toward those examples. Fine-tuning teaches &lt;em&gt;style, format, or specialised vocabulary&lt;/em&gt;, not facts. A fine-tuned model trained on the templates would learn to sound like a legal template, would learn the vocabulary and cadence of the corpus, but would still not reliably cite a specific clause number. Facts go stale the moment the corpus changes; weights don’t update when a template does.&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;update frequency&lt;/em&gt;. The templates change 50 times a month. Fine-tuning takes hours to days to run and costs money per run; running it weekly to stay current would be expensive and would leave a gap between “clause updated” and “model knows.” RAG updates by re-embedding a changed document, minutes, maybe seconds, and the next query sees the new version immediately. Freshness is a first-class requirement for this domain, and fine-tuning does badly on freshness.&lt;/p&gt;

&lt;p&gt;The third is &lt;em&gt;data volume and quality for fine-tuning&lt;/em&gt;. Fine-tuning typically wants hundreds to thousands of high-quality labelled examples for the behaviour you’re trying to teach. For the legal-ops team, that would mean writing hundreds of (question, ideal-answer) pairs by hand, the kind of project that takes months and is done infrequently. By contrast, RAG needs the &lt;em&gt;documents&lt;/em&gt; (already have them) and an embedding model (off the shelf). The bar to entry is lower by an order of magnitude.&lt;/p&gt;

&lt;p&gt;The fourth is &lt;em&gt;cost shape at &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt;&lt;/em&gt;. Prompt engineering and RAG both run on the standard on-demand per-token bill. A custom-trained model typically requires reserved capacity instead, a multi-month commitment of model units. That is a very different cost profile: RAG at 500 queries a day costs cents; reserved capacity for a custom model is thousands of dollars a month whether anyone uses it or not. Fine-tuning is appropriate when volume is high and predictable enough to saturate a reserved endpoint. Not a legal-ops team of 20 paralegals.&lt;/p&gt;

&lt;p&gt;The fifth is &lt;em&gt;explainability&lt;/em&gt;. When the model cites a specific clause, the paralegal wants to click through to the source template. RAG gives that for free, the retrieved chunks are the citation. Fine-tuning erases the provenance: the model emits text that came from somewhere in training, but “somewhere” isn’t a clickable link.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Six filters, applied to each of the three techniques.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Corrects hallucination on proprietary data, does the technique give the model access to the actual templates?&lt;/li&gt;
  &lt;li&gt;Adapts format and tone, does the technique change how the model writes (length, structure, register)?&lt;/li&gt;
  &lt;li&gt;Handles data freshness, when a template updates, how fast does the system reflect it?&lt;/li&gt;
  &lt;li&gt;Setup cost, time and data-labelling effort to get to first working version?&lt;/li&gt;
  &lt;li&gt;Inference cost shape, on-demand per &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;token&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;, provisioned, or something else?&lt;/li&gt;
  &lt;li&gt;Provides citations, can the paralegal trace the answer to a specific source?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-three-technique-landscape&quot;&gt;The three-technique landscape&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Prompt engineering. Refining the text sent to the model. For a question-answering task: system prompt that sets role and constraints (“You are a legal research assistant. Only answer based on provided source text. If the source doesn’t contain the answer, say so.”); few-shot examples showing the desired question-answer shape; instructions on format (“cite the template name and section number”). No new AWS services; the work is in the application code. Inference cost is whatever Bedrock on-demand charges. Setup: hours to days of iteration. Citation: only if the source text is already in the prompt (i.e. paired with retrieval).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Retrieval-augmented generation (RAG). Pre-embed the corpus, store embeddings, retrieve-and-inject at query time. AWS building blocks: Bedrock Knowledge Bases (managed: point at S3, configure chunking and embedding model, query via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock-agent-runtime:RetrieveAndGenerate&lt;/code&gt;), or DIY with a Bedrock embedding model (Titan Text Embeddings v2, Cohere Embed) plus a vector store (OpenSearch Serverless, Aurora PostgreSQL with pgvector, Pinecone, etc.). Inference cost: on-demand Bedrock per token + vector-store running cost. Setup: hours to days, depending on chunking strategy. Citation: native, the retrieved chunks carry their source metadata.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Fine-tuning. Adjusting model weights on task-specific training data. Bedrock supports fine-tuning selected models (Nova, Titan, Llama) via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock:CreateModelCustomizationJob&lt;/code&gt;: upload JSONL training data to S3, configure hyperparameters, start the job. Output is a custom model that requires provisioned throughput to serve. Setup: days to weeks to prepare a training set of hundreds of high-quality examples, plus the training job itself (hours), plus evaluation. Inference cost: provisioned throughput starting at a few dollars per hour per model unit, 1- or 6-month commitments. Citation: none by default; the model produces text without provenance.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Continued pre-training. Bedrock’s other customisation path: feed a large corpus of unlabelled domain text (a few gigabytes to tens) and further train the base model on it. Useful for teaching the model a specialised vocabulary or domain (medical, legal, financial) when fine-tuning on labelled pairs isn’t enough. Same cost shape as fine-tuning: provisioned throughput to serve, days of setup. Mentioned for completeness; rarely the correct first answer for a question-answering problem.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Technique&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Corrects hallucination&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Adapts format/tone&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Handles freshness&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Setup cost&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Inference cost&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Citations&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Prompt engineering&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;N/A&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Low&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;On-demand&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RAG&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (seconds)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Medium&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;On-demand + vector store&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Fine-tuning&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial (style only)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗ (re-train)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;High&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Provisioned throughput&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Continued pre-training&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Partial (vocabulary)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗ (re-train)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Very high&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Provisioned throughput&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Reading the table against the legal-ops team’s actual problem: the templates change 50 times a month (fine-tuning fails freshness), the paralegals want citations (fine-tuning doesn’t provide them), and the hallucination is about facts, not style (fine-tuning doesn’t fix facts). RAG is the technique for this problem. Prompt engineering will still be necessary on top of RAG, the retrieved chunks need the correct framing, but it’s not sufficient alone because the model needs the information injected, not just instructed.&lt;/p&gt;

&lt;h3 id=&quot;when-each-technique-earns-its-keep&quot;&gt;When each technique earns its keep&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 560&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Decision tree for choosing between prompt engineering, RAG, and fine-tuning. Starts with the question: does the problem require knowledge the model does not have? If no, go to format-and-tone questions which lead to prompt engineering or fine-tuning. If yes, ask whether the knowledge changes frequently. If yes, RAG. If no, ask whether training-data pairs exist. Different combinations lead to different techniques.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .prft-q          { fill: #fff; stroke: #5a7a9a; stroke-width: 1.8; }
      .prft-leaf-p     { fill: rgba(70, 140, 200, 0.12); stroke: rgb(50, 110, 170); stroke-width: 2; }
      .prft-leaf-r     { fill: rgba(46, 138, 90, 0.12); stroke: rgb(36, 108, 70); stroke-width: 2; }
      .prft-leaf-f     { fill: rgba(180, 60, 150, 0.10); stroke: rgb(140, 40, 115); stroke-width: 2; }
      .prft-leaf-fp    { fill: rgba(180, 120, 60, 0.10); stroke: rgb(150, 90, 30); stroke-width: 2; }
      .prft-q-text     { font-size: 13px; fill: #222; }
      .prft-leaf-title { font-size: 15px; font-weight: 700; fill: #222; }
      .prft-leaf-sub   { font-size: 11px; fill: #444; }
      .prft-arrow      { fill: none; stroke: #666; stroke-width: 1.5; }
      .prft-label-y    { font-size: 11px; font-weight: 700; fill: rgb(36, 108, 70); }
      .prft-label-n    { font-size: 11px; font-weight: 700; fill: rgb(170, 60, 60); }
      .prft-header     { font-size: 16px; font-weight: 700; fill: #222; }
    &lt;/style&gt;
    &lt;marker id=&quot;prft-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;6&quot; markerHeight=&quot;6&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#666&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;40&quot; y=&quot;30&quot; class=&quot;prft-header&quot;&gt;Picking the technique&lt;/text&gt;

  &lt;!-- Root question --&gt;
  &lt;rect x=&quot;400&quot; y=&quot;60&quot; width=&quot;300&quot; height=&quot;60&quot; rx=&quot;8&quot; class=&quot;prft-q&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;85&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot;&gt;Does the model need knowledge&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;103&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot;&gt;it does not already have?&lt;/text&gt;

  &lt;!-- Branch No --&gt;
  &lt;path d=&quot;M475,120 L225,175&quot; class=&quot;prft-arrow&quot; marker-end=&quot;url(#prft-arrow)&quot; /&gt;
  &lt;text x=&quot;310&quot; y=&quot;150&quot; class=&quot;prft-label-n&quot;&gt;NO — pure style/format problem&lt;/text&gt;

  &lt;!-- Branch Yes --&gt;
  &lt;path d=&quot;M625,120 L825,175&quot; class=&quot;prft-arrow&quot; marker-end=&quot;url(#prft-arrow)&quot; /&gt;
  &lt;text x=&quot;770&quot; y=&quot;150&quot; class=&quot;prft-label-y&quot;&gt;YES — knowledge-grounding problem&lt;/text&gt;

  &lt;!-- Left subtree: format/tone --&gt;
  &lt;rect x=&quot;80&quot; y=&quot;180&quot; width=&quot;300&quot; height=&quot;60&quot; rx=&quot;8&quot; class=&quot;prft-q&quot; /&gt;
  &lt;text x=&quot;230&quot; y=&quot;205&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot;&gt;Hundreds of labelled examples&lt;/text&gt;
  &lt;text x=&quot;230&quot; y=&quot;223&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot;&gt;of the desired behaviour?&lt;/text&gt;

  &lt;!-- Left: No -&gt; prompt --&gt;
  &lt;path d=&quot;M155,240 L140,310&quot; class=&quot;prft-arrow&quot; marker-end=&quot;url(#prft-arrow)&quot; /&gt;
  &lt;text x=&quot;125&quot; y=&quot;280&quot; class=&quot;prft-label-n&quot;&gt;NO&lt;/text&gt;

  &lt;rect x=&quot;40&quot; y=&quot;310&quot; width=&quot;240&quot; height=&quot;100&quot; rx=&quot;8&quot; class=&quot;prft-leaf-p&quot; /&gt;
  &lt;text x=&quot;160&quot; y=&quot;338&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-title&quot;&gt;Prompt engineering&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;360&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;system prompt + few-shot&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;376&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;on-demand Bedrock&lt;/text&gt;
  &lt;text x=&quot;160&quot; y=&quot;392&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;iterate on wording&lt;/text&gt;

  &lt;!-- Left: Yes -&gt; fine-tune for format --&gt;
  &lt;path d=&quot;M305,240 L335,310&quot; class=&quot;prft-arrow&quot; marker-end=&quot;url(#prft-arrow)&quot; /&gt;
  &lt;text x=&quot;340&quot; y=&quot;280&quot; class=&quot;prft-label-y&quot;&gt;YES&lt;/text&gt;

  &lt;rect x=&quot;300&quot; y=&quot;310&quot; width=&quot;240&quot; height=&quot;100&quot; rx=&quot;8&quot; class=&quot;prft-leaf-f&quot; /&gt;
  &lt;text x=&quot;420&quot; y=&quot;338&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-title&quot;&gt;Fine-tuning&lt;/text&gt;
  &lt;text x=&quot;420&quot; y=&quot;360&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;JSONL training pairs&lt;/text&gt;
  &lt;text x=&quot;420&quot; y=&quot;376&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;provisioned throughput&lt;/text&gt;
  &lt;text x=&quot;420&quot; y=&quot;392&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;for style, not facts&lt;/text&gt;

  &lt;!-- Right subtree: knowledge grounding --&gt;
  &lt;rect x=&quot;720&quot; y=&quot;180&quot; width=&quot;300&quot; height=&quot;60&quot; rx=&quot;8&quot; class=&quot;prft-q&quot; /&gt;
  &lt;text x=&quot;870&quot; y=&quot;205&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot;&gt;Does that knowledge change&lt;/text&gt;
  &lt;text x=&quot;870&quot; y=&quot;223&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot;&gt;faster than quarterly?&lt;/text&gt;

  &lt;!-- Right: Yes -&gt; RAG --&gt;
  &lt;path d=&quot;M795,240 L780,310&quot; class=&quot;prft-arrow&quot; marker-end=&quot;url(#prft-arrow)&quot; /&gt;
  &lt;text x=&quot;765&quot; y=&quot;280&quot; class=&quot;prft-label-y&quot;&gt;YES&lt;/text&gt;

  &lt;rect x=&quot;680&quot; y=&quot;310&quot; width=&quot;240&quot; height=&quot;100&quot; rx=&quot;8&quot; class=&quot;prft-leaf-r&quot; /&gt;
  &lt;text x=&quot;800&quot; y=&quot;338&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-title&quot;&gt;RAG&lt;/text&gt;
  &lt;text x=&quot;800&quot; y=&quot;360&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;Bedrock Knowledge Bases&lt;/text&gt;
  &lt;text x=&quot;800&quot; y=&quot;376&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;or DIY vector store&lt;/text&gt;
  &lt;text x=&quot;800&quot; y=&quot;392&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;citations built in&lt;/text&gt;

  &lt;!-- Right: No -&gt; continued pre-training / fine-tune on domain --&gt;
  &lt;path d=&quot;M945,240 L975,310&quot; class=&quot;prft-arrow&quot; marker-end=&quot;url(#prft-arrow)&quot; /&gt;
  &lt;text x=&quot;980&quot; y=&quot;280&quot; class=&quot;prft-label-n&quot;&gt;NO&lt;/text&gt;

  &lt;rect x=&quot;940&quot; y=&quot;310&quot; width=&quot;240&quot; height=&quot;100&quot; rx=&quot;8&quot; class=&quot;prft-leaf-fp&quot; /&gt;
  &lt;text x=&quot;1060&quot; y=&quot;338&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-title&quot;&gt;Continued pre-training&lt;/text&gt;
  &lt;text x=&quot;1060&quot; y=&quot;360&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;large unlabelled corpus&lt;/text&gt;
  &lt;text x=&quot;1060&quot; y=&quot;376&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;provisioned throughput&lt;/text&gt;
  &lt;text x=&quot;1060&quot; y=&quot;392&quot; text-anchor=&quot;middle&quot; class=&quot;prft-leaf-sub&quot;&gt;stable-vocabulary domains&lt;/text&gt;

  &lt;!-- Footer note --&gt;
  &lt;rect x=&quot;80&quot; y=&quot;450&quot; width=&quot;1000&quot; height=&quot;80&quot; rx=&quot;8&quot; class=&quot;prft-q&quot; /&gt;
  &lt;text x=&quot;580&quot; y=&quot;477&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot; style=&quot;font-weight: 700;&quot;&gt;All four can combine.&lt;/text&gt;
  &lt;text x=&quot;580&quot; y=&quot;497&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot;&gt;RAG is rarely deployed without prompt engineering on top of it. Fine-tuning plus RAG is how some domain-specific&lt;/text&gt;
  &lt;text x=&quot;580&quot; y=&quot;513&quot; text-anchor=&quot;middle&quot; class=&quot;prft-q-text&quot;&gt;chatbots are built: the fine-tune teaches voice and format, retrieval supplies the facts.&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Two questions, does the model need new knowledge, and does that knowledge change, partition the techniques. The legal-ops scenario lands on RAG.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-pick-in-depth&quot;&gt;The pick in depth&lt;/h3&gt;

&lt;p&gt;RAG via Bedrock Knowledge Bases, with prompt engineering on top. Bedrock Knowledge Bases is the managed RAG path: point it at an S3 bucket of source documents, configure an embedding model and a vector store, and Bedrock handles chunking, embedding, indexing, and retrieval. At query time, one API call (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock-agent-runtime:RetrieveAndGenerate&lt;/code&gt;) takes the user’s question, retrieves the most relevant chunks, constructs the prompt, calls the generation model, and returns the answer with source citations.&lt;/p&gt;

&lt;p&gt;The configuration surface that matters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Embedding model. The embedding model turns text into a vector (a list of numbers like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[0.12, -0.45, ..., 0.08]&lt;/code&gt;, typically 1024 or 1536 dimensions). Similar meanings produce similar vectors. Bedrock Knowledge Bases supports Titan Text Embeddings v2 (Amazon), Cohere Embed English v3, and Cohere Embed Multilingual v3. For a multi-jurisdiction legal corpus including non-English templates, Cohere Multilingual is the correct default.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Chunking strategy. A 50-page template isn’t embedded as one vector; it’s split into chunks, usually a few hundred tokens each, and each chunk gets its own vector. Default chunk size is 300 tokens with 20% overlap. For legal templates where clauses have meaningful boundaries, a semantic chunking strategy (chunks respect paragraph or heading boundaries) often retrieves more cleanly than fixed-size chunks. Bedrock Knowledge Bases supports default, fixed-size, hierarchical, and semantic chunking.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Vector store. Where the embeddings live. OpenSearch Serverless is the default (Bedrock can create it for you). Aurora PostgreSQL with pgvector is the alternative if you already run Aurora; Pinecone and Redis Enterprise Cloud are supported third-party options. For 4,000 templates of varying size, OpenSearch Serverless is the lowest-friction choice; Aurora pgvector matters if the legal team already runs metadata in Postgres and wants SQL joins across vector and structured data.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Retrieval configuration. How many chunks to retrieve per query (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;numberOfResults&lt;/code&gt;, default 5; legal corpora with many adjacent clauses often benefit from 6-8 to capture related sections), and whether to use hybrid search (vector similarity plus keyword matching) versus pure vector. For legal templates where exact clause names matter, hybrid search often retrieves more reliably than pure-vector.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Generation model. Separately configurable: Claude Sonnet, Nova Pro, Llama, whichever. The generation model sees the retrieved chunks plus the question and produces the answer. Bedrock’s default prompt template includes the chunks under a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$search_results$&lt;/code&gt; placeholder and instructs the model to answer based on them.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt-engineering layer still matters. Knowledge Bases lets you override the default prompt template; a custom template for this use case might add instructions like “If the source templates don’t contain the answer, say ‘I don’t have a template matching those criteria’ rather than guessing. Always cite the template name and clause number in the format [Template Name, §Clause Number].” These instructions are why the technique works end-to-end: retrieval feeds the model the correct chunks; the prompt tells the model how to handle missing information without inventing it.&lt;/p&gt;

&lt;p&gt;Freshness is handled by Bedrock’s ingestion pipeline. A changed template in S3 triggers a re-sync, either on demand via the console or API, or scheduled, that re-embeds only the changed documents and updates the vector store. From template-change to model-knows is minutes, not a training run.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-query&quot;&gt;A worked query&lt;/h3&gt;

&lt;p&gt;A paralegal has a question. They type it into the team’s internal tool, which calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ aws bedrock-agent-runtime retrieve-and-generate \
    --input &apos;{&quot;text&quot;: &quot;What is our standard limitation of liability clause for French SaaS agreements, capped at 12 months of fees?&quot;}&apos; \
    --retrieve-and-generate-configuration &apos;{
      &quot;type&quot;: &quot;KNOWLEDGE_BASE&quot;,
      &quot;knowledgeBaseConfiguration&quot;: {
        &quot;knowledgeBaseId&quot;: &quot;KB-LEGAL-TEMPLATES&quot;,
        &quot;modelArn&quot;: &quot;arn:aws:bedrock:eu-west-1::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0&quot;,
        &quot;retrievalConfiguration&quot;: {
          &quot;vectorSearchConfiguration&quot;: {
            &quot;numberOfResults&quot;: 6,
            &quot;overrideSearchType&quot;: &quot;HYBRID&quot;
          }
        }
      }
    }&apos;

{
  &quot;output&quot;: {
    &quot;text&quot;: &quot;Our standard limitation of liability clause for French SaaS agreements, capped at 12 months of fees, is in SaaS-FR-v4.2 at §14.3. The clause reads: &apos;Except for breaches of confidentiality or indemnification obligations, each party&apos;s aggregate liability under this Agreement shall not exceed the fees paid or payable by Customer to Provider in the twelve (12) months immediately preceding the event giving rise to the claim.&apos; Related carve-outs for gross negligence are in §14.4.&quot;
  },
  &quot;citations&quot;: [
    {
      &quot;generatedResponsePart&quot;: { ... },
      &quot;retrievedReferences&quot;: [
        {
          &quot;content&quot;: { &quot;text&quot;: &quot;...&quot; },
          &quot;location&quot;: {
            &quot;type&quot;: &quot;S3&quot;,
            &quot;s3Location&quot;: { &quot;uri&quot;: &quot;s3://legal-templates/saas/FR/SaaS-FR-v4.2.docx&quot; }
          }
        }
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What happened:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;retrieve-and-generate&lt;/code&gt; call embedded the question with Cohere Multilingual v3.&lt;/li&gt;
  &lt;li&gt;Bedrock queried the OpenSearch Serverless index with the resulting vector, using hybrid search (vector + keyword for “French SaaS”, “limitation of liability”, “12 months”).&lt;/li&gt;
  &lt;li&gt;It retrieved the top 6 chunks by relevance. The top hit was §14.3 of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SaaS-FR-v4.2.docx&lt;/code&gt;; adjacent chunks included §14.4 and the definitions section.&lt;/li&gt;
  &lt;li&gt;The retrieved chunks were injected into the generation prompt under the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$search_results$&lt;/code&gt; placeholder; Claude Sonnet produced the answer, sticking to the retrieved text because the custom prompt template instructs it to.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;citations&lt;/code&gt; array on the response links each generated span to the source chunk. The UI renders a clickable citation: “[SaaS-FR-v4.2, §14.3]” jumps straight to the document in SharePoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The round trip is 1-3 seconds. The answer is grounded in actual text. If a paralegal asks about a jurisdiction the corpus doesn’t cover, the model says so rather than inventing.&lt;/p&gt;

&lt;h3 id=&quot;when-fine-tuning-would-be-the-right-choice&quot;&gt;When fine-tuning would be the right choice&lt;/h3&gt;

&lt;p&gt;Fine-tuning is the correct tool when the problem is &lt;em&gt;how&lt;/em&gt; the model writes, matching a company’s tone, producing a specific output format reliably, handling specialised vocabulary the base model gets wrong. Legal templates have some of that, but the &lt;em&gt;primary&lt;/em&gt; problem here is &lt;label for=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-grounding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-grounding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;grounding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-grounding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-choosing-between-prompting-rag-and-fine-tuning-grounding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Grounding&lt;/span&gt;Constraining a model to answer from provided sources rather than from whatever it absorbed during training.
&lt;/span&gt;, not voice. Solve the grounding problem with RAG first; fine-tune later if there’s still a style gap.&lt;/p&gt;

&lt;p&gt;For reference, fine-tuning a Bedrock model would have meant writing 500-2000 question-answer pairs by hand (a month of paralegal time), running a training job, then paying provisioned throughput at $2-$20 per hour to keep the custom model serving, charged whether anyone queries it or not. RAG plus prompt engineering ships in days at cents per query.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;The three techniques solve three different problems. Prompt engineering changes &lt;em&gt;instructions&lt;/em&gt;. RAG adds &lt;em&gt;facts&lt;/em&gt;. Fine-tuning changes &lt;em&gt;style and format&lt;/em&gt;. Reaching for the wrong one solves nothing.&lt;/li&gt;
  &lt;li&gt;Hallucination on proprietary data is almost always a retrieval problem, not a training problem. The base model doesn’t have your documents. Give it them at query time; don’t try to bake them into the weights.&lt;/li&gt;
  &lt;li&gt;Freshness kills fine-tuning for dynamic domains. If the knowledge changes faster than the training cadence, fine-tuned models are stale the moment they land. RAG’s freshness is minutes; fine-tuning’s is the next training run.&lt;/li&gt;
  &lt;li&gt;Bedrock Knowledge Bases is the managed RAG path. S3 in, vector store out, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetrieveAndGenerate&lt;/code&gt; as the single query API. Chunking strategy, embedding model, and retrieval config are the levers worth tuning.&lt;/li&gt;
  &lt;li&gt;Citations come from retrieval, not generation. RAG’s output carries &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;retrievedReferences&lt;/code&gt; pointing to source documents. Fine-tuning produces text without provenance; if citations matter, fine-tuning alone won’t suffice.&lt;/li&gt;
  &lt;li&gt;Provisioned throughput is the cost shape for fine-tuned models on Bedrock. That’s a commitment of model units for 1 or 6 months, in the thousands of dollars per month. On-demand per-token pricing doesn’t apply to custom models.&lt;/li&gt;
  &lt;li&gt;Prompt engineering is always part of the answer. Even with perfect retrieval, the model needs instructions: format the citation this way, refuse to answer if the source doesn’t cover it, adopt this tone. Prompt work sits on top of every technique.&lt;/li&gt;
  &lt;li&gt;Combine where it makes sense. RAG + prompt engineering is the common pair. Fine-tuning + RAG is the domain-chatbot pattern: the fine-tune teaches voice, retrieval supplies facts. Rarely is fine-tuning alone the correct choice for a knowledge-heavy task.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The legal-ops team’s first prototype hallucinated because the model didn’t have the templates. The fix isn’t a smarter prompt or a longer training run, it’s plumbing the templates into the model’s input at query time. RAG, with prompt engineering shaping the output and Bedrock Knowledge Bases doing the retrieval plumbing, is the technique that matches the problem.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Assumption Mapping: Testing What You Believe</title>
    <link href="/writing/assumption-mapping-testing-what-you-believe/"/>
    <updated>2026-05-05T06:00:00+08:00</updated>
    <id>/writing/assumption-mapping-testing-what-you-believe/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/finding-the-fit/&quot;&gt;Finding the Fit&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Greenbox delivers weekly produce boxes from local farms to 200 subscribers in Perth. Recent customer interviews revealed that people stay for the convenience, not the local sourcing the team assumed. Now the team needs to find out what else they believe that they haven’t actually tested.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;/writing/jobs-to-be-done-why-subscribers-actually-stay/&quot;&gt;JTBD interviews&lt;/a&gt; gave the Greenbox team a breakthrough. Subscribers don’t stay for fresh local vegetables. They stay because Greenbox eliminates weeknight dinner stress. The box arrives, dinner is decided, one less thing to worry about.&lt;/p&gt;

&lt;p&gt;That insight reshaped the product roadmap. Recipe cards went into every box. Churn dropped from 8% to 5% in the first month.&lt;/p&gt;

&lt;p&gt;But the interviews also revealed something less comfortable: a lot of what the team believes about the business is assumption, not fact.&lt;/p&gt;

&lt;p&gt;Maya believes subscribers value local sourcing. She built the entire brand around it. Tom believes the substitution algorithm is good enough. Sam believes word-of-mouth is the main acquisition channel. Priya believes the weekly delivery cadence is right.&lt;/p&gt;

&lt;p&gt;These aren’t minor details. They’re foundational assumptions. If any of them are wrong, the team could be optimising the wrong things for the next six months.&lt;/p&gt;

&lt;h3 id=&quot;how-assumptions-hide&quot;&gt;How assumptions hide&lt;/h3&gt;

&lt;p&gt;The tricky thing about assumptions is that the team doesn’t experience them as assumptions. They experience them as facts. “Subscribers value local sourcing” doesn’t feel like a guess, it feels like the foundation of the business. Maya would have said, with total confidence, that local sourcing is why people subscribe. Until the JTBD interviews showed otherwise.&lt;/p&gt;

&lt;p&gt;The ones you’re most confident about are often the ones you’ve tested least. Nobody tests what they consider obvious.&lt;/p&gt;

&lt;h3 id=&quot;introducing-assumption-mapping&quot;&gt;Introducing Assumption Mapping&lt;/h3&gt;

&lt;p&gt;Assumption Mapping is a structured way to surface, categorise, and prioritise what you believe but haven’t validated.&lt;/p&gt;

&lt;p&gt;Step 1: List your assumptions. Everything the team believes about the business. No judgement.&lt;/p&gt;

&lt;p&gt;Step 2: Rate each on two axes. How critical is it? (If wrong, how badly does it hurt?) How much evidence? (Tested, or just a feeling?)&lt;/p&gt;

&lt;p&gt;Step 3: Plot on a 2x2 grid.&lt;/p&gt;

&lt;div style=&quot;display: grid; grid-template-columns: 1fr 1fr; border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(220,50,50,0.08); border-right: 1px solid var(--color-rule); border-bottom: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.25em; color: var(--color-accent);&quot;&gt;Test Immediately&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;High risk, low evidence&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(65,105,225,0.08); border-bottom: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.25em; color: var(--color-accent);&quot;&gt;Monitor&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;High risk, high evidence&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(184,134,11,0.06); border-right: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.25em; color: var(--color-ink-tertiary);&quot;&gt;Park&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;Low risk, low evidence&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(46,139,87,0.08);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.25em; color: var(--color-ink-tertiary);&quot;&gt;Fine&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;Low risk, high evidence&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center; font-size: 0.8rem; color: var(--color-ink-tertiary); margin-top: var(--space-xs);&quot;&gt;Horizontal axis: Low Evidence &amp;rarr; High Evidence. Vertical axis: Low Risk &amp;rarr; High Risk.&lt;/p&gt;

&lt;p&gt;The top-left quadrant, high risk, low evidence, is where the landmines live.&lt;/p&gt;

&lt;h3 id=&quot;running-the-session&quot;&gt;Running the session&lt;/h3&gt;

&lt;p&gt;Lee facilitates. Dave is here. Maya invited him after the JTBD interviews, partly because the assumptions about farms need a farmer’s perspective, and partly because Dave has a way of saying things that cut through the noise. He drove in from Margaret River this morning, two hours in his ute with ABC Country playing the whole way. He sits at the end of the table in his work shirt, arms folded, watching the team arrange their sticky notes. He hasn’t been in this office since the Event Storm months ago. The walls are different now, covered in printouts and JTBD transcripts. Patrick’s quote is pinned above the whiteboard: &lt;em&gt;“I was paying twenty-five dollars a week to feel bad about myself.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dave reads it. He doesn’t say anything.&lt;/p&gt;

&lt;p&gt;“Write down everything you believe about Greenbox that you haven’t actually tested,” Lee says. “Not features. Beliefs. Things you’d bet the business on.”&lt;/p&gt;

&lt;p&gt;The team writes for ten minutes. Twenty-four assumptions pile up:&lt;/p&gt;

&lt;p&gt;Maya: &lt;em&gt;Subscribers value local sourcing. Farms will scale with us. Our price ($25/box) is competitive. Subscribers prefer curated over choosing. The brand matters more than the price.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tom: &lt;em&gt;The substitution algorithm produces acceptable results. The platform can handle 1,000 subscribers. Farms will use the portal. Weekly delivery is the right cadence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Priya: &lt;em&gt;Subscribers want more variety. Mobile is primary for account management. The signup conversion rate is acceptable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sam: &lt;em&gt;Word-of-mouth is our primary channel. Subscribers would recommend us. Instagram drives sign-ups. Churn is value-driven, not logistics-driven.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dave writes slowly. Three notes in large, deliberate handwriting: &lt;em&gt;Farms will scale with us. We’re the only option. Farmers will keep supplying if Greenbox has a bad quarter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sam sees Dave’s second note and writes his own version: “We don’t have a serious competitor in Perth.”&lt;/p&gt;

&lt;h3 id=&quot;plotting-the-map&quot;&gt;Plotting the map&lt;/h3&gt;

&lt;p&gt;The team takes each assumption and debates where it belongs. This is where the interesting conversations happen.&lt;/p&gt;

&lt;p&gt;“Subscribers value local sourcing.”&lt;/p&gt;

&lt;p&gt;Maya instinctively puts it top-right: high risk, high evidence. “It’s our brand identity.”&lt;/p&gt;

&lt;p&gt;Lee pushes back. “How many JTBD interviewees mentioned it as the &lt;em&gt;primary&lt;/em&gt; reason they subscribe?”&lt;/p&gt;

&lt;p&gt;Sam checks the LLM’s analysis. Local sourcing appeared in nine of fifteen interviews, but as the primary motivator in only three.&lt;/p&gt;

&lt;p&gt;The assumption moves to the top-left. High risk, low evidence.&lt;/p&gt;

&lt;p&gt;That move is uncomfortable. Maya built Greenbox around local sourcing. It’s not just a feature, it’s personal. Discovering that subscribers might not share that belief feels like a challenge to her identity, not just her business strategy.&lt;/p&gt;

&lt;p&gt;“The substitution algorithm produces acceptable results.”&lt;/p&gt;

&lt;p&gt;Tom puts it top-right. “Nobody complains.”&lt;/p&gt;

&lt;p&gt;Priya raises her hand. “Nobody complains to us. But three churned subscribers in the JTBD interviews mentioned getting items they didn’t want. One said ‘I got turnips three weeks in a row.’”&lt;/p&gt;

&lt;p&gt;Tom opens his laptop. Thirty seconds: “Turnips were available in bulk from Dave’s farm for three weeks. The algorithm scored them as the best substitution because they were cheap and plentiful. Root-for-root swaps. Technically correct. Terrible customer experience.”&lt;/p&gt;

&lt;p&gt;The assumption moves left. Working correctly and working well are different things.&lt;/p&gt;

&lt;p&gt;“Farms will scale with us as we grow.”&lt;/p&gt;

&lt;p&gt;Maya puts it top-right. “I talk to Dave and Rachel every week. They’re committed.”&lt;/p&gt;

&lt;p&gt;Dave clears his throat. The room turns.&lt;/p&gt;

&lt;p&gt;“Last bloke who asked me to scale went bust and owed me eight thousand dollars.”&lt;/p&gt;

&lt;p&gt;The room goes still.&lt;/p&gt;

&lt;p&gt;“Farm-to-table scheme out of Busselton. Three years ago. Promised guaranteed orders. I expanded my planting for them. Hired a casual for harvest. They folded in August and I was out the produce, the labour costs, and the eight grand they owed me. Never saw a cent.”&lt;/p&gt;

&lt;p&gt;He looks at Maya. Not with hostility, with the kind of frank assessment you give a stock fence before leaning on it.&lt;/p&gt;

&lt;p&gt;“I’m here because I trust you, Maya. I trust that you grew up on a farm and you know what it costs when things go wrong. But trust doesn’t plant seeds. Contracts plant seeds. And right now, you and I have a handshake.”&lt;/p&gt;

&lt;p&gt;Lee lets the silence sit. Then: “The assumption isn’t ‘Dave trusts us.’ It’s ‘farms will scale with us.’ And the evidence for that is a handshake and a history of being burned.”&lt;/p&gt;

&lt;p&gt;Top-left. Firmly.&lt;/p&gt;

&lt;p&gt;Maya writes “formalise farm contracts” on a fresh sticky note and puts it in her pocket. Dave’s words, &lt;em&gt;last bloke who asked me to scale went bust&lt;/em&gt;, sit in the room like weather.&lt;/p&gt;

&lt;p&gt;“We don’t have a serious competitor in Perth.”&lt;/p&gt;

&lt;p&gt;Sam opens his laptop. “Two churned subscribers mentioned a company called Freshly. I’ve been researching.”&lt;/p&gt;

&lt;p&gt;He walks the team through it: launched in Sydney four months ago, twelve million in Series A, ex-McKinsey founders, recruiting delivery drivers in Perth right now.&lt;/p&gt;

&lt;p&gt;“What do they charge?” Tom asks.&lt;/p&gt;

&lt;p&gt;“Eighteen dollars a week.”&lt;/p&gt;

&lt;p&gt;The room does the arithmetic. Greenbox charges twenty-five.&lt;/p&gt;

&lt;p&gt;Top-left quadrant. The team had been operating as if they were the only game in town.&lt;/p&gt;

&lt;p&gt;Dave, from the end of the table: “Freshly rang me last week. Asking about supply. I told them I was committed elsewhere. But they’ll ring Rachel next, if they haven’t already.”&lt;/p&gt;

&lt;p&gt;The session continues for another twenty minutes. When it’s done:&lt;/p&gt;

&lt;div style=&quot;display: grid; grid-template-columns: 1fr 1fr; border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(220,50,50,0.08); border-right: 1px solid var(--color-rule); border-bottom: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-accent);&quot;&gt;Test Immediately&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;High risk, low evidence&lt;/span&gt;
    &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
      &lt;li&gt;Subscribers value local sourcing&lt;/li&gt;
      &lt;li&gt;Our price point ($25/box) is competitive&lt;/li&gt;
      &lt;li&gt;Farms will scale with us as we grow&lt;/li&gt;
      &lt;li&gt;We don&apos;t have a serious competitor&lt;/li&gt;
      &lt;li&gt;Word-of-mouth is our primary acquisition channel&lt;/li&gt;
      &lt;li&gt;Weekly delivery is the right cadence&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(65,105,225,0.08); border-bottom: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-accent);&quot;&gt;Monitor&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;High risk, high evidence&lt;/span&gt;
    &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
      &lt;li&gt;Subscribers prefer a curated box&lt;/li&gt;
      &lt;li&gt;Recipe cards reduce churn&lt;/li&gt;
      &lt;li&gt;The brand matters more than the price&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(184,134,11,0.06); border-right: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-ink-tertiary);&quot;&gt;Park&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;Low risk, low evidence&lt;/span&gt;
    &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
      &lt;li&gt;Mobile is the primary account management channel&lt;/li&gt;
      &lt;li&gt;Instagram drives sign-ups&lt;/li&gt;
      &lt;li&gt;The unboxing experience matters for retention&lt;/li&gt;
      &lt;li&gt;People who cancel would come back with a discount&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(46,139,87,0.08);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-ink-tertiary);&quot;&gt;Fine&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;Low risk, high evidence&lt;/span&gt;
    &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
      &lt;li&gt;Subscribers would recommend Greenbox to friends&lt;/li&gt;
      &lt;li&gt;Signup flow conversion rate is acceptable&lt;/li&gt;
      &lt;li&gt;The platform can handle 1,000 subscribers&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Six assumptions in the “Test Immediately” quadrant. Six things the business depends on that nobody has validated.&lt;/p&gt;

&lt;h3 id=&quot;designing-cheap-experiments&quot;&gt;Designing cheap experiments&lt;/h3&gt;

&lt;p&gt;The team can’t do six research projects, they need to ship product and grow simultaneously. The experiments need to cost hours, not weeks.&lt;/p&gt;

&lt;p&gt;“For each assumption,” Lee says, “find the smallest, cheapest experiment that would change your mind.”&lt;/p&gt;

&lt;p&gt;Local sourcing: A survey to all active subscribers. Rank five factors. Would you consider a $20 mixed-sourcing box? The LLM helps phrase the questions to minimise leading bias. Twenty minutes to build.&lt;/p&gt;

&lt;p&gt;Price point: Two landing page variants, current pricing alone versus current pricing with a $20 “Mixed Box” option. Track click intent for a week.&lt;/p&gt;

&lt;p&gt;Farm scaling: Maya calls three farm partners and asks: “If we needed to double our order in three months, could you do it?”&lt;/p&gt;

&lt;p&gt;Acquisition channel: Tom adds a mandatory “How did you hear about us?” dropdown to the sign-up flow. One hour.&lt;/p&gt;

&lt;p&gt;Delivery cadence: Sam adds a question to the post-delivery email: weekly, fortnightly, or flexible?&lt;/p&gt;

&lt;p&gt;Five experiments. Total cost: about eight hours. Results in one to two weeks.&lt;/p&gt;

&lt;h3 id=&quot;the-results&quot;&gt;The results&lt;/h3&gt;

&lt;p&gt;Local sourcing: 168 responses. Only 12% ranked local sourcing as the most important factor. Convenience dominated (38%), followed by produce quality (26%) and recipe cards (18%). 60% said they’d likely switch to a $20 mixed-sourcing box.&lt;/p&gt;

&lt;p&gt;Maya sits with this. Sixty percent of her subscribers would accept non-local produce for a five-dollar saving. “I feel like I’ve been punched in the stomach,” she says.&lt;/p&gt;

&lt;p&gt;Lee lets the silence sit. “It doesn’t mean local sourcing is worthless. Twelve percent rank it first, that’s twenty-four people who might leave if you drop it. But 100% local at $25 might not be the only viable model.”&lt;/p&gt;

&lt;p&gt;Price point: 2.3x more clicks on “Subscribe” when the $20 mixed option appeared alongside the $25 local option. Having a choice made people more likely to subscribe at all.&lt;/p&gt;

&lt;p&gt;Farm scaling: Two of three farms could increase supply by 50%. Dave, the biggest supplier, would cap out at current levels. He’d need a full growing season to expand.&lt;/p&gt;

&lt;p&gt;Acquisition channel: Word-of-mouth: 31%. Google search: 28%. Instagram: 19%. Local press: 14%. Sam was partially right, word-of-mouth is biggest, but not dominant. Search and social together account for nearly half.&lt;/p&gt;

&lt;p&gt;Delivery cadence: 41% wanted weekly. 35% wanted fortnightly. 24% wanted flexible. More than half wanted &lt;em&gt;less&lt;/em&gt; frequent delivery. This explains churn the team hadn’t understood, subscribers accumulating unwanted produce and cancelling out of guilt.&lt;/p&gt;

&lt;h3 id=&quot;the-hard-conversation&quot;&gt;The hard conversation&lt;/h3&gt;

&lt;p&gt;Maya is quiet for a long time. “I built this business around an assumption I never tested. I assumed people cared about local sourcing as much as I do. They don’t.”&lt;/p&gt;

&lt;p&gt;“It means you have options you didn’t know you had,” Lee says. “A $20 mixed box could open up a much larger market. A fortnightly option reduces churn. Neither kills the local brand, you can still offer a premium local box for the people who value it most. But the path to 1,000 subscribers probably isn’t ‘1,000 people who care deeply about local produce.’ It’s ‘1,000 people who want dinner stress eliminated, some of whom also care about local.’”&lt;/p&gt;

&lt;p&gt;“That’s a different business than the one I set out to build,” Maya says. She looks at Dave.&lt;/p&gt;

&lt;p&gt;“Maybe,” Lee says. “Or maybe it’s the same business, with a broader front door.”&lt;/p&gt;

&lt;p&gt;Dave stands up. He needs to get back before dark. He shakes Maya’s hand at the door.&lt;/p&gt;

&lt;p&gt;“You’ll work it out,” he says. It’s not a compliment, it’s a bet. The same bet he made when he agreed to supply Greenbox on a handshake. He’s still holding.&lt;/p&gt;

&lt;p&gt;Maya watches his ute pull out of the car park.&lt;/p&gt;

&lt;p&gt;She thinks about his eight thousand dollars. Not a loan. A debt, the one the last bloke who asked Dave to scale left him holding when the business went under. Dave said it twice today. Once in the meeting and once in the way he shook her hand at the door. &lt;em&gt;You know what it costs when things go wrong.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;She does. That’s the problem.&lt;/p&gt;

&lt;p&gt;The numbers say the premise was wrong. Not wrong wrong, 12% is real, those are real people, but it was the &lt;em&gt;premise&lt;/em&gt;. Local at scale was the thing she was proving. If the market doesn’t want what she was proving, then she didn’t build a produce-box business. She built a vehicle for proving something nobody asked her to prove. And she got two farmers and two hundred subscribers and a handshake with Dave to sign on while she did it.&lt;/p&gt;

&lt;p&gt;Lee is right that she has options. A mixed box, a fortnightly cadence, a broader front door, on a whiteboard, it’s obvious. She could draw the pivot in fifteen minutes. But the pivot isn’t a whiteboard. The pivot is driving back to Margaret River and telling Dave the model is changing, the exact sentence, more or less, that the last operator said before he went bust owing Dave eight thousand dollars. It’s asking two farmers who signed up for “local produce to Perth” to bet on a new story. It’s telling two hundred subscribers that what they bought is not what they’re getting. Some will stay. Some will leave. She does not know which, and she does not know how many nights between now and knowing.&lt;/p&gt;

&lt;p&gt;And under all of that, older than all of that: her father. Who didn’t pivot either. Who held on until there was nothing to hold on to. In her family, the story of losing the farm is a story about a man who loved something too much to see it clearly. Maya has been building Greenbox partly to not be him. And today she is sitting in a car park being told that what she loves is not what the business needs her to love, and the only move that feels like the opposite of her father, the only move that isn’t &lt;em&gt;holding on anyway&lt;/em&gt;, is to stop. Her father held on and lost the farm. The last bloke scaled and lost Dave’s eight thousand dollars. Stopping is the one thing neither of them did.&lt;/p&gt;

&lt;p&gt;She knows, in the part of her brain that can still do arithmetic, that stopping and pivoting are not the same shape. That Dave’s eight thousand dollars gets paid back by a working business, not by an honourable wind-down. That pausing operations is still a kind of losing, just a tidier kind. But that part of her brain is tired and it is late and the drive home is long.&lt;/p&gt;

&lt;p&gt;She thinks about the “pausing operations” email she hasn’t written yet but can feel forming at the edges of her mind, like weather moving in from the coast. Three sentences. Honest. A clean door closed. She isn’t going to write it tonight. She might not write it at all. But she can feel the shape of it now, and that frightens her more than the survey did.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-assumption-mapping&quot;&gt;When to use Assumption Mapping&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Before major investment decisions. If the team is about to spend significant time or money, map the assumptions first. The cost is trivial compared to building on a wrong assumption.&lt;/li&gt;
  &lt;li&gt;After discovery reveals surprises. If one major assumption was wrong, others might be too.&lt;/li&gt;
  &lt;li&gt;When the team disagrees about direction. Disagreements often hide different assumptions. Mapping makes the disagreement concrete rather than political.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;when-not-to-use-it&quot;&gt;When not to use it&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;When the team isn’t safe enough to admit uncertainty. If admitting “I don’t have evidence” feels dangerous, the exercise produces a sanitised list. Fix the safety problem first.&lt;/li&gt;
  &lt;li&gt;As a substitute for talking to customers. The map tells you what to test. It doesn’t do the testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-comes-next&quot;&gt;What comes next&lt;/h3&gt;

&lt;p&gt;Maya has a board meeting in three weeks. She needs a credible path to 1,000 subscribers. The insights are powerful, mixed sourcing, fortnightly options, SEO investment. But do the numbers add up? Can Greenbox reach 1,000 with a model that works financially, especially with a competitor about to enter at $18 per week?&lt;/p&gt;

&lt;p&gt;That’s a question about the business model itself. And it’s where Lee starts to hit the limits of what he can help with.&lt;/p&gt;

&lt;p&gt;For that, Lee reaches for the &lt;a href=&quot;/writing/business-model-canvas-does-this-actually-work/&quot;&gt;Business Model Canvas&lt;/a&gt;, and Charlotte brings someone who can help with the numbers.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>How to Take a Foundation Model from Pick to Production Endpoint</title>
    <link href="/writing/how-to-take-a-foundation-model-from-pick-to-production-endpoint/"/>
    <updated>2026-05-04T06:00:00+08:00</updated>
    <id>/writing/how-to-take-a-foundation-model-from-pick-to-production-endpoint/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;AI Practitioner&lt;/strong&gt; · AIF-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A support organisation handles around 8,000 tickets a week across five product lines. Each ticket is a thread of customer messages and agent replies, averaging roughly 1,500 words. Managers want a one-paragraph summary at the top of each ticket, written in the same tone the company uses in its knowledge base, that a reviewer can read in ten seconds.&lt;/p&gt;

&lt;p&gt;The team is three backend engineers and a product manager. None of them has trained a model. The company already has an AWS account with a modest budget, the tickets live in an RDS Postgres database, and the security team has said anything sent to a third-party API needs a written exception. AWS-native is the path of least resistance.&lt;/p&gt;

&lt;p&gt;“Foundation model” has been floated as a solution but nobody in the room can define it, let alone explain the path from “a foundation model exists somewhere” to “a reviewer sees a summary in the ticket UI tomorrow morning.” The lifecycle is the thing to walk.&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;A foundation model, in the sense the industry now uses the term, is a large neural network trained on a broad corpus, text, code, sometimes images, that can be adapted to many downstream tasks without being retrained from scratch. “Foundation” is the metaphor: the model is the ground floor, and the application sits on top.&lt;/p&gt;

&lt;p&gt;The first thing worth thinking about is that there is no single “use a foundation model” step. There is a sequence: choose the model, get access to it, design the way you’ll &lt;label for=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; it, optionally teach it something about your data, deploy it behind an endpoint, plumb that endpoint into your application, and then watch what it does in production. Each of those is a distinct decision, and AWS sells a distinct service (or at least a distinct API surface) for each one.&lt;/p&gt;

&lt;p&gt;The second is that most of those stages are optional. A team that needs a summariser doesn’t have to train, doesn’t have to &lt;label for=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-fine-tuning&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-fine-tuning-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;fine-tune&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-fine-tuning&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-fine-tuning-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Fine-tuning&lt;/span&gt;Continuing to train an already-trained model on a smaller dataset to adapt its behaviour.
&lt;/span&gt;, and in many cases doesn’t even need retrieval, the model has read enough English by now that summarising a ticket is within its baseline capability. Recognising which stages a given problem needs is most of the work; adding stages that aren’t pulling weight is how projects end up with a &lt;label for=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-training&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-training-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;training&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-training&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-training-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Training&lt;/span&gt;The process of fitting a model’s weights to data by minimising a loss function.
&lt;/span&gt; pipeline they never use.&lt;/p&gt;

&lt;p&gt;The third is the managed-versus-self-managed axis. A managed API gives you a foundation model behind an SDK call with no infrastructure, you don’t see the GPUs, you pay per &lt;label for=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;token&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;. A self-hosted endpoint lets you take a model, put it on infrastructure you pick, and pay for the instance whether calls come or not. The first is the path for most text-summarisation-shaped problems; the second is the path when data can’t leave your VPC, when the model you want isn’t in the managed catalogue, or when latency demands a provisioned endpoint rather than an on-demand one.&lt;/p&gt;

&lt;p&gt;The fourth is the cost shape. On-demand managed-API pricing is per input and output token, with different rates for different models. For an 8,000-ticket-per-week workload that’s predictable enough to price up front, but the pricing model matters: a longer summary is more output tokens, and input tokens scale with the length of the ticket, so summarisation costs scale roughly linearly with workload.&lt;/p&gt;

&lt;p&gt;The fifth is governance. Once a model is behind an endpoint, every team in the company will want to call it. Who can, for which use cases, logged how, evaluated against what? “We stood up a model” is easy; “we stood up a model and a governance story around it” is the one that survives an audit.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Every foundation-model project passes through some subset of seven stages. Scoring each stage against the team’s situation is the filter that decides what gets built.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Model choice, which foundation model fits the task’s quality, language, context-window, and cost profile?&lt;/li&gt;
  &lt;li&gt;Access, managed API or self-hosted endpoint?&lt;/li&gt;
  &lt;li&gt;Adaptation, prompt engineering alone, &lt;label for=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-rag&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-rag-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;retrieval-augmented generation&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-rag&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-rag-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RAG&lt;/span&gt;A pattern where you retrieve relevant documents at query time and stuff them into the prompt so the model can ground its answer on them.
&lt;/span&gt;, fine-tuning, or continued pre-training?&lt;/li&gt;
  &lt;li&gt;Deployment surface, on-demand per-token, provisioned throughput, or a provisioned real-time endpoint?&lt;/li&gt;
  &lt;li&gt;Integration, how does the application call the endpoint and handle responses, errors, and rate limits?&lt;/li&gt;
  &lt;li&gt;Evaluation, how do we know the model is getting it correct, and how do we track that over time?&lt;/li&gt;
  &lt;li&gt;Governance, logging, &lt;label for=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-guardrail&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-guardrail-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;guardrails&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-guardrail&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-guardrail-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Guardrail&lt;/span&gt;A filter or rule applied to an LLM’s inputs or outputs to keep it inside safe, legal, or on-brand behaviour.
&lt;/span&gt;, access control, cost attribution.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-lifecycle-landscape&quot;&gt;The lifecycle landscape&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Model selection and access via Bedrock. Amazon Bedrock is a managed service that puts a catalogue of foundation models. Anthropic Claude, Meta Llama, Amazon Nova and Titan, Mistral, Cohere, AI21, behind a single API. No infrastructure to provision; access is granted per-model in the Bedrock console (some models require an access request, some are self-serve). Authentication is IAM; calls are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock-runtime:InvokeModel&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModelWithResponseStream&lt;/code&gt;. For a summarisation task with 8,000 tickets a week, this is the shortest path from “we chose a model” to “the model is callable from Lambda.”&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Model selection and access via SageMaker JumpStart. JumpStart is a SageMaker feature that lets you pick an open-weights model from a catalogue (Llama, Falcon, Mistral, and Amazon’s own models) and deploy it to a real-time SageMaker endpoint in your VPC with a few clicks or a CloudFormation-friendly SDK call. You pay for the underlying instance (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml.g5.2xlarge&lt;/code&gt;) whether calls come in or not, but the model lives in your account, talks only to your VPC, and is subject to no per-token pricing. The path when data residency, custom fine-tuning, or steady high throughput push you off on-demand.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Prompt engineering. The cheapest form of adaptation. A prompt is just the text you send to the model, instructions, examples, and the input. “Summarise the following support ticket in one paragraph, using a neutral professional tone” followed by the ticket text is a prompt. Good prompt engineering can take a generic model most of the way to task-specific behaviour without touching a training pipeline. No new AWS service; the work lives in your application code.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Retrieval-augmented generation (RAG). When the model needs facts it wasn’t trained on, internal product documentation, this quarter’s pricing, an engineer’s runbook, you retrieve relevant documents at request time and include them in the prompt. Bedrock Knowledge Bases is the AWS-managed path: point it at an S3 bucket of documents, it chunks them, embeds each chunk into a &lt;label for=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt; (a list of numbers that encodes meaning), stores the vectors in an OpenSearch Serverless or Aurora PostgreSQL index, and at query time retrieves the most relevant chunks and injects them into the model’s prompt. The team can do this themselves with Titan or Cohere &lt;label for=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embedding&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-to-take-a-foundation-model-from-pick-to-production-endpoint-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; models plus their own vector store; Knowledge Bases is the zero-plumbing version.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Fine-tuning. If prompt engineering and retrieval both fall short, typically because the task needs a voice, format, or domain vocabulary the base model doesn’t produce reliably, fine-tuning adjusts the model’s weights on a task-specific dataset. Bedrock supports fine-tuning a subset of its models (Nova, Titan, Llama) via the console and API: upload JSONL training data to S3, start a fine-tuning job, get a custom model that requires provisioned throughput to serve. Fine-tuning is expensive in dollars and in evaluation time; most projects don’t need it.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Deployment. Bedrock offers two throughput models: on-demand (pay per input and output token, no capacity reservation) and provisioned throughput (commit to a number of “model units” for 1 or 6 months in exchange for guaranteed capacity and a different price). Fine-tuned Bedrock models require provisioned throughput. SageMaker endpoints are a third path: provision instances, pay for them continuously, get sub-second predictable latency. The choice depends on whether the workload’s shape is bursty (on-demand wins), steady-high (provisioned throughput wins), or latency-critical (SageMaker endpoint wins).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Governance. Bedrock emits CloudTrail events for every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; call, supports data capture to S3 for input and output logging, and integrates with Bedrock Guardrails (topic denies, PII redaction, profanity filters) configured independently of the model. IAM policies scope which principals can invoke which models; AWS Config and Service Control Policies can prevent unapproved models from being invoked at all. SageMaker endpoints inherit the standard VPC, IAM, and CloudWatch story.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;p&gt;Mapping the seven stages onto the support-ticket-summariser scenario:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Stage&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Needed for this project?&lt;/th&gt;
      &lt;th&gt;AWS service&lt;/th&gt;
      &lt;th&gt;Notes&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Model selection&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td&gt;Bedrock catalogue&lt;/td&gt;
      &lt;td&gt;Claude / Nova for English summarisation&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Access&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td&gt;Bedrock&lt;/td&gt;
      &lt;td&gt;On-demand via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock-runtime:InvokeModel&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Prompt engineering&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td&gt;(application code)&lt;/td&gt;
      &lt;td&gt;One well-crafted prompt carries most of the work&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Retrieval (RAG)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td&gt;Bedrock Knowledge Bases&lt;/td&gt;
      &lt;td&gt;Ticket is self-contained; no external facts needed&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Fine-tuning&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td&gt;Bedrock Custom Models&lt;/td&gt;
      &lt;td&gt;Defer until prompting is measured&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Deployment surface&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td&gt;Bedrock on-demand&lt;/td&gt;
      &lt;td&gt;8k/week is predictable but bursty; not fine-tuned&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Evaluation&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td&gt;Bedrock Evaluation + SageMaker Clarify&lt;/td&gt;
      &lt;td&gt;Sample, label, track drift&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Governance&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td&gt;IAM, CloudTrail, Guardrails&lt;/td&gt;
      &lt;td&gt;PII redaction on input; log everything&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The two stages the team can skip, retrieval and fine-tuning, are the two stages where most “AI projects” burn budget unnecessarily. The ticket is the thing being summarised; the model doesn’t need facts beyond the ticket. Fine-tuning is premature until there’s evidence prompting has plateaued.&lt;/p&gt;

&lt;h3 id=&quot;the-lifecycle-as-a-pipeline&quot;&gt;The lifecycle as a pipeline&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 560&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;The foundation-model lifecycle as seven sequential stages arranged left to right. Stage one, model selection, shows Bedrock catalogue and SageMaker JumpStart as alternatives. Stage two, access, shows Bedrock API and SageMaker endpoint. Stage three, prompt engineering, shown as always required. Stage four, retrieval augmented generation, marked optional with Bedrock Knowledge Bases. Stage five, fine-tuning, marked optional with Bedrock Custom Models. Stage six, deployment, shows on-demand, provisioned throughput, and SageMaker endpoint. Stage seven, evaluation and governance, shows CloudTrail, Guardrails, and Bedrock Evaluation. Below the pipeline, a highlighted path marks the shortest viable route for the support ticket summariser: model selection, access, prompt engineering, on-demand deployment, evaluation and governance.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .fml-stage      { fill: #fff; stroke: #5a7a9a; stroke-width: 1.8; }
      .fml-stage-opt  { fill: #fff; stroke: #999; stroke-width: 1.5; stroke-dasharray: 5 3; }
      .fml-stage-req  { fill: rgba(46, 138, 90, 0.08); stroke: rgb(46, 138, 90); stroke-width: 2; }
      .fml-num        { font-size: 11px; font-weight: 700; fill: #5a7a9a; }
      .fml-num-opt    { font-size: 11px; font-weight: 700; fill: #888; }
      .fml-num-req    { font-size: 11px; font-weight: 700; fill: rgb(36, 108, 70); }
      .fml-title      { font-size: 13px; font-weight: 700; fill: #222; }
      .fml-svc        { font-size: 10px; fill: #444; }
      .fml-opt-label  { font-size: 10px; font-style: italic; fill: #888; }
      .fml-arrow      { fill: none; stroke: #666; stroke-width: 1.5; }
      .fml-path       { fill: none; stroke: rgb(46, 138, 90); stroke-width: 3; opacity: 0.75; }
      .fml-header     { font-size: 16px; font-weight: 700; fill: #222; }
      .fml-caption    { font-size: 12px; fill: #444; }
      .fml-legend-t   { font-size: 11px; fill: #333; }
    &lt;/style&gt;
    &lt;marker id=&quot;fml-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;6&quot; markerHeight=&quot;6&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#666&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;fml-arrow-green&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;rgb(46, 138, 90)&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;40&quot; y=&quot;35&quot; class=&quot;fml-header&quot;&gt;The seven stages&lt;/text&gt;

  &lt;!-- Stage 1 --&gt;
  &lt;rect x=&quot;30&quot; y=&quot;70&quot; width=&quot;140&quot; height=&quot;110&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;42&quot; y=&quot;90&quot; class=&quot;fml-num-req&quot;&gt;STAGE 1&lt;/text&gt;
  &lt;text x=&quot;100&quot; y=&quot;112&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;Model&lt;/text&gt;
  &lt;text x=&quot;100&quot; y=&quot;128&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;selection&lt;/text&gt;
  &lt;text x=&quot;100&quot; y=&quot;148&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;Bedrock catalogue&lt;/text&gt;
  &lt;text x=&quot;100&quot; y=&quot;162&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;SageMaker JumpStart&lt;/text&gt;

  &lt;path d=&quot;M170,125 L200,125&quot; class=&quot;fml-arrow&quot; marker-end=&quot;url(#fml-arrow)&quot; /&gt;

  &lt;!-- Stage 2 --&gt;
  &lt;rect x=&quot;200&quot; y=&quot;70&quot; width=&quot;140&quot; height=&quot;110&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;212&quot; y=&quot;90&quot; class=&quot;fml-num-req&quot;&gt;STAGE 2&lt;/text&gt;
  &lt;text x=&quot;270&quot; y=&quot;120&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;Access&lt;/text&gt;
  &lt;text x=&quot;270&quot; y=&quot;148&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;Bedrock API&lt;/text&gt;
  &lt;text x=&quot;270&quot; y=&quot;162&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;SageMaker endpoint&lt;/text&gt;

  &lt;path d=&quot;M340,125 L370,125&quot; class=&quot;fml-arrow&quot; marker-end=&quot;url(#fml-arrow)&quot; /&gt;

  &lt;!-- Stage 3 --&gt;
  &lt;rect x=&quot;370&quot; y=&quot;70&quot; width=&quot;140&quot; height=&quot;110&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;382&quot; y=&quot;90&quot; class=&quot;fml-num-req&quot;&gt;STAGE 3&lt;/text&gt;
  &lt;text x=&quot;440&quot; y=&quot;112&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;Prompt&lt;/text&gt;
  &lt;text x=&quot;440&quot; y=&quot;128&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;engineering&lt;/text&gt;
  &lt;text x=&quot;440&quot; y=&quot;148&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;application code&lt;/text&gt;
  &lt;text x=&quot;440&quot; y=&quot;162&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;no new service&lt;/text&gt;

  &lt;path d=&quot;M510,125 L540,125&quot; class=&quot;fml-arrow&quot; marker-end=&quot;url(#fml-arrow)&quot; /&gt;

  &lt;!-- Stage 4 (optional) --&gt;
  &lt;rect x=&quot;540&quot; y=&quot;70&quot; width=&quot;140&quot; height=&quot;110&quot; rx=&quot;6&quot; class=&quot;fml-stage-opt&quot; /&gt;
  &lt;text x=&quot;552&quot; y=&quot;90&quot; class=&quot;fml-num-opt&quot;&gt;STAGE 4 · opt&lt;/text&gt;
  &lt;text x=&quot;610&quot; y=&quot;112&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;Retrieval&lt;/text&gt;
  &lt;text x=&quot;610&quot; y=&quot;128&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;(RAG)&lt;/text&gt;
  &lt;text x=&quot;610&quot; y=&quot;148&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;Bedrock Knowledge&lt;/text&gt;
  &lt;text x=&quot;610&quot; y=&quot;162&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;Bases&lt;/text&gt;

  &lt;path d=&quot;M680,125 L710,125&quot; class=&quot;fml-arrow&quot; marker-end=&quot;url(#fml-arrow)&quot; /&gt;

  &lt;!-- Stage 5 (optional) --&gt;
  &lt;rect x=&quot;710&quot; y=&quot;70&quot; width=&quot;140&quot; height=&quot;110&quot; rx=&quot;6&quot; class=&quot;fml-stage-opt&quot; /&gt;
  &lt;text x=&quot;722&quot; y=&quot;90&quot; class=&quot;fml-num-opt&quot;&gt;STAGE 5 · opt&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;120&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;Fine-tuning&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;148&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;Bedrock Custom&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;162&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;Models&lt;/text&gt;

  &lt;path d=&quot;M850,125 L880,125&quot; class=&quot;fml-arrow&quot; marker-end=&quot;url(#fml-arrow)&quot; /&gt;

  &lt;!-- Stage 6 --&gt;
  &lt;rect x=&quot;880&quot; y=&quot;70&quot; width=&quot;140&quot; height=&quot;110&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;892&quot; y=&quot;90&quot; class=&quot;fml-num-req&quot;&gt;STAGE 6&lt;/text&gt;
  &lt;text x=&quot;950&quot; y=&quot;120&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;Deployment&lt;/text&gt;
  &lt;text x=&quot;950&quot; y=&quot;148&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;on-demand / provisioned&lt;/text&gt;
  &lt;text x=&quot;950&quot; y=&quot;162&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;SageMaker endpoint&lt;/text&gt;

  &lt;!-- Stage 7 spans below --&gt;
  &lt;rect x=&quot;30&quot; y=&quot;230&quot; width=&quot;990&quot; height=&quot;80&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;42&quot; y=&quot;252&quot; class=&quot;fml-num-req&quot;&gt;STAGE 7 · continuous&lt;/text&gt;
  &lt;text x=&quot;525&quot; y=&quot;278&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;Evaluation &amp;amp; Governance&lt;/text&gt;
  &lt;text x=&quot;525&quot; y=&quot;296&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;CloudTrail · Bedrock Guardrails · Bedrock Evaluation · SageMaker Clarify · IAM · KMS · Config&lt;/text&gt;

  &lt;!-- Connecting arrow down --&gt;
  &lt;path d=&quot;M525,180 L525,230&quot; class=&quot;fml-arrow&quot; marker-end=&quot;url(#fml-arrow)&quot; /&gt;

  &lt;!-- Highlighted path for the ticket summariser --&gt;
  &lt;text x=&quot;40&quot; y=&quot;370&quot; class=&quot;fml-header&quot;&gt;The ticket-summariser path&lt;/text&gt;
  &lt;text x=&quot;40&quot; y=&quot;392&quot; class=&quot;fml-caption&quot;&gt;Stages 4 and 5 skipped. Stage 6 is Bedrock on-demand. Stage 7 runs alongside from day one.&lt;/text&gt;

  &lt;rect x=&quot;30&quot; y=&quot;410&quot; width=&quot;140&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;100&quot; y=&quot;435&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;Claude Sonnet&lt;/text&gt;
  &lt;text x=&quot;100&quot; y=&quot;452&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;via Bedrock&lt;/text&gt;

  &lt;path d=&quot;M170,440 L200,440&quot; class=&quot;fml-path&quot; marker-end=&quot;url(#fml-arrow-green)&quot; /&gt;

  &lt;rect x=&quot;200&quot; y=&quot;410&quot; width=&quot;140&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;270&quot; y=&quot;435&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;InvokeModel&lt;/text&gt;
  &lt;text x=&quot;270&quot; y=&quot;452&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;IAM-scoped&lt;/text&gt;

  &lt;path d=&quot;M340,440 L370,440&quot; class=&quot;fml-path&quot; marker-end=&quot;url(#fml-arrow-green)&quot; /&gt;

  &lt;rect x=&quot;370&quot; y=&quot;410&quot; width=&quot;140&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;440&quot; y=&quot;435&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;One prompt&lt;/text&gt;
  &lt;text x=&quot;440&quot; y=&quot;452&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;in Lambda&lt;/text&gt;

  &lt;path d=&quot;M510,440 L540,440&quot; class=&quot;fml-path&quot; marker-end=&quot;url(#fml-arrow-green)&quot; /&gt;

  &lt;rect x=&quot;540&quot; y=&quot;410&quot; width=&quot;140&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;fml-stage-opt&quot; /&gt;
  &lt;text x=&quot;610&quot; y=&quot;435&quot; text-anchor=&quot;middle&quot; class=&quot;fml-opt-label&quot;&gt;(skip RAG)&lt;/text&gt;
  &lt;text x=&quot;610&quot; y=&quot;452&quot; text-anchor=&quot;middle&quot; class=&quot;fml-opt-label&quot;&gt;ticket is self-contained&lt;/text&gt;

  &lt;path d=&quot;M680,440 L710,440&quot; class=&quot;fml-path&quot; marker-end=&quot;url(#fml-arrow-green)&quot; /&gt;

  &lt;rect x=&quot;710&quot; y=&quot;410&quot; width=&quot;140&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;fml-stage-opt&quot; /&gt;
  &lt;text x=&quot;780&quot; y=&quot;435&quot; text-anchor=&quot;middle&quot; class=&quot;fml-opt-label&quot;&gt;(skip fine-tune)&lt;/text&gt;
  &lt;text x=&quot;780&quot; y=&quot;452&quot; text-anchor=&quot;middle&quot; class=&quot;fml-opt-label&quot;&gt;measure prompt first&lt;/text&gt;

  &lt;path d=&quot;M850,440 L880,440&quot; class=&quot;fml-path&quot; marker-end=&quot;url(#fml-arrow-green)&quot; /&gt;

  &lt;rect x=&quot;880&quot; y=&quot;410&quot; width=&quot;140&quot; height=&quot;60&quot; rx=&quot;6&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;950&quot; y=&quot;435&quot; text-anchor=&quot;middle&quot; class=&quot;fml-title&quot;&gt;On-demand&lt;/text&gt;
  &lt;text x=&quot;950&quot; y=&quot;452&quot; text-anchor=&quot;middle&quot; class=&quot;fml-svc&quot;&gt;per-token billing&lt;/text&gt;

  &lt;!-- Legend --&gt;
  &lt;rect x=&quot;30&quot; y=&quot;505&quot; width=&quot;16&quot; height=&quot;12&quot; class=&quot;fml-stage-req&quot; /&gt;
  &lt;text x=&quot;52&quot; y=&quot;515&quot; class=&quot;fml-legend-t&quot;&gt;required stage&lt;/text&gt;
  &lt;rect x=&quot;170&quot; y=&quot;505&quot; width=&quot;16&quot; height=&quot;12&quot; class=&quot;fml-stage-opt&quot; /&gt;
  &lt;text x=&quot;192&quot; y=&quot;515&quot; class=&quot;fml-legend-t&quot;&gt;optional stage (skip unless evidence demands it)&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Seven stages in sequence, two of them optional. The summariser touches five of the seven. Evaluation and governance are continuous, not a phase.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-pick-in-depth&quot;&gt;The pick in depth&lt;/h3&gt;

&lt;p&gt;Bedrock on-demand, Claude or Nova, one well-crafted prompt. Bedrock gives a catalogue of models behind a single SDK. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock-runtime:InvokeModel&lt;/code&gt; takes a model ID (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic.claude-sonnet-4-5-20250929-v1:0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;amazon.nova-pro-v1:0&lt;/code&gt;, etc.) and a JSON body whose shape depends on the model family. For Claude, the body is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;anthropic_version&quot;:&quot;bedrock-2023-05-31&quot;,&quot;max_tokens&quot;:1024,&quot;messages&quot;:[{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;...&quot;}]}&lt;/code&gt;. For Nova, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;inferenceConfig&quot;:{&quot;max_new_tokens&quot;:1024},&quot;messages&quot;:[...]}&lt;/code&gt;. The response comes back JSON; the application extracts &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;output.message.content[0].text&lt;/code&gt; (Nova) or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content[0].text&lt;/code&gt; (Claude) and hands it to the UI.&lt;/p&gt;

&lt;p&gt;Model choice is an empirical question, not a reading-specs question. Amazon Bedrock has an Evaluation feature that runs a set of prompts through a chosen model and scores the results on dimensions like accuracy, robustness, and toxicity, or against a custom ground-truth dataset. Run a batch of 50 representative tickets through three candidate models (Claude Sonnet, Nova Pro, Llama 3.3 70B), have the product manager score the summaries, pick the model that wins on the cheapest price-per-token that meets the quality bar. The evaluation is a few hours of work; it saves months of arguing about which model “feels better.”&lt;/p&gt;

&lt;p&gt;Prompt engineering is where the quality lives. A prompt that says “Summarise this ticket” produces mediocre summaries. A prompt that says “You are writing a one-paragraph summary of a customer support ticket for an internal reviewer. Use a neutral professional tone. Mention the customer’s issue, what the agent did, and whether it’s resolved. Do not include the customer’s name or email. If the ticket is in a language other than English, summarise in English.” produces the correct shape, every time. Give the model a few labelled examples in the prompt, “few-shot prompting”, and the consistency tightens further. None of this touches AWS; it’s application code. It’s also where 80% of the lift comes from.&lt;/p&gt;

&lt;p&gt;Deployment-wise, 8,000 tickets a week at roughly 2,000 input tokens and 200 output tokens each works out to 16M input and 1.6M output tokens per week. Claude Sonnet 4.5 on Bedrock bills at roughly $3 per million input tokens and $15 per million output tokens, so: $48 + $24 = $72/week, or about $310/month. On-demand is correct: the workload is small enough that provisioned throughput’s minimum commitment would cost more than the usage, and the traffic bursts to Monday-morning peaks that on-demand handles without capacity planning.&lt;/p&gt;

&lt;p&gt;Governance is an independent track. A Bedrock Guardrail, a configuration object attached to the invocation, redacts PII from the input before it reaches the model, denies specific topics (medical or legal advice, for example), and filters profanity from the output. CloudTrail records every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; call with the model ID and the caller’s IAM principal. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock-runtime&lt;/code&gt; invocation supports passing an invocation log destination (S3) so full input/output pairs land there, KMS-encrypted, for audit and evaluation-set curation. An IAM policy on the Lambda role restricts which models it can invoke: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock:InvokeModel&lt;/code&gt; with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Resource&lt;/code&gt; scoped to specific model ARNs.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-pipeline-one-ticket-end-to-end&quot;&gt;A worked pipeline: one ticket end-to-end&lt;/h3&gt;

&lt;p&gt;The PM wants to see the pipeline work on a real ticket before sign-off. The engineering team has a Lambda wired up.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ aws bedrock-runtime invoke-model \
    --model-id anthropic.claude-sonnet-4-5-20250929-v1:0 \
    --body &apos;{
      &quot;anthropic_version&quot;: &quot;bedrock-2023-05-31&quot;,
      &quot;max_tokens&quot;: 400,
      &quot;messages&quot;: [
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;You are writing a one-paragraph summary of a customer support ticket for an internal reviewer. Use a neutral professional tone. Mention the customers issue, what the agent did, and whether it is resolved. Do not include the customers name or email. Ticket follows:\n\n---\nCustomer: Hi, my dashboard has been stuck on loading for two hours. Im on the Pro plan.\nAgent: Hi there, sorry to hear. Can you try clearing your browser cache?\nCustomer: Tried that, same issue.\nAgent: Ok, Im seeing an issue on our side with the Pro-plan widget rendering. Engineering is deploying a fix; should be resolved in 30 min.\nCustomer: Ok thanks.\nAgent: Deployed. Can you refresh and confirm?\nCustomer: Working now. Thanks!&quot;}
      ]
    }&apos; \
    --guardrail-identifier ticket-summariser-gr \
    --guardrail-version 1 \
    --cli-binary-format raw-in-base64-out \
    out.json

$ jq -r &apos;.content[0].text&apos; out.json
A Pro-plan customer reported that their dashboard was stuck loading for
two hours. The agent diagnosed a server-side rendering issue affecting
Pro-plan widgets, deployed a fix, and the customer confirmed the
dashboard was working again. The ticket is resolved.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What happened behind the scenes:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;IAM authorised the caller for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bedrock:InvokeModel&lt;/code&gt; on the Claude Sonnet model ARN in the target Region.&lt;/li&gt;
  &lt;li&gt;Bedrock applied the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ticket-summariser-gr&lt;/code&gt; guardrail: scanned the input for PII (no matches, because the prompt specifically said not to include names), scanned the output (no matches), passed both through.&lt;/li&gt;
  &lt;li&gt;Bedrock called Anthropic’s model (hosted inside the AWS-Anthropic arrangement, the request never leaves AWS), got the completion, returned it.&lt;/li&gt;
  &lt;li&gt;CloudTrail logged the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InvokeModel&lt;/code&gt; call: principal, model ID, timestamp, and, because invocation logging is enabled, the input and output landed in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s3://ticket-summariser-logs/&lt;/code&gt; under a KMS key that only the platform team holds.&lt;/li&gt;
  &lt;li&gt;The Lambda wrote the summary to the ticket’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;summary&lt;/code&gt; column in RDS. The support UI rendered it at the top of the thread next time the ticket was opened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s the loop. For 8,000 tickets a week, an EventBridge rule fires a SQS message per new ticket, the Lambda processes them at whatever concurrency Bedrock’s on-demand quota allows (request a service-quota increase if the default isn’t enough), and the whole pipeline is roughly 200 lines of code plus the guardrail, the IAM policies, and the log bucket.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Foundation model means a general-purpose pretrained model you adapt, not one you build. Someone else did the training; your job is to choose, access, prompt, and optionally adapt.&lt;/li&gt;
  &lt;li&gt;The lifecycle has seven stages but most projects only need five. Retrieval is for when the model needs external facts. Fine-tuning is for when prompting plateaus. Both add real complexity; neither is free.&lt;/li&gt;
  &lt;li&gt;Bedrock is the default access path for text generation. Managed, IAM-gated, per-token pricing, no GPUs to manage. The path of least resistance for most business-problem-shaped use cases.&lt;/li&gt;
  &lt;li&gt;SageMaker JumpStart is the path when data residency, model choice, or workload shape push you off Bedrock. Your own endpoint with the model you choose; you pay for the instance whether calls come or not.&lt;/li&gt;
  &lt;li&gt;Prompt engineering is where 80% of the quality lift comes from. One well-crafted prompt with instructions, tone guidance, and a few examples beats a mediocre prompt against a more expensive model.&lt;/li&gt;
  &lt;li&gt;Evaluate empirically, not by vibes. Bedrock Evaluation runs a candidate prompt across multiple models on a fixed dataset; the correct model is the one that wins your evaluation, not the one with the newest press release.&lt;/li&gt;
  &lt;li&gt;Deployment surface follows workload shape. Bursty and small: on-demand. Steady-high or fine-tuned: provisioned throughput. Latency-critical or data-residency-bound: SageMaker endpoint.&lt;/li&gt;
  &lt;li&gt;Governance is continuous, not a stage. Guardrails, CloudTrail, invocation logging, and IAM scoping belong in the first version, not the hardening pass. Retrofitting them is harder than starting with them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The path from “use a foundation model” to “a reviewer sees a summary in the ticket UI” isn’t a single step; it’s a pipeline. Most of the stages in that pipeline have obvious AWS-native answers, and most teams that get stuck are the ones who treat adaptation (retrieval or fine-tuning) as compulsory rather than contingent. Start with the shortest path, measure, and only add stages when the evidence says they’d help.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Reranker You Didn't Know You Needed</title>
    <link href="/writing/the-reranker-you-didnt-know-you-needed/"/>
    <updated>2026-05-02T06:00:00+08:00</updated>
    <id>/writing/the-reranker-you-didnt-know-you-needed/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;You shipped a &lt;label for=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-rag&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-rag-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;RAG&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-rag&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-rag-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RAG&lt;/span&gt;A pattern where you retrieve relevant documents at query time and stuff them into the prompt so the model can ground its answer on them.
&lt;/span&gt; chatbot last quarter. &lt;label for=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Embeddings&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt;, &lt;label for=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector database&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt;, prompt template, the lot. Demo went great. Three months in, the support team is finding answers that are technically in the corpus but consistently the wrong ones, close enough on the embedding to rank highly, but not actually what the question was asking. You crank the top-k from 5 to 20, the &lt;label for=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; gets confused by the noise, and the answers get worse. You’re stuck.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The fix is a step you skipped.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;/writing/to-llms-and-beyond/&quot;&gt;To LLMs… and Beyond!&lt;/a&gt; we covered RAG, retrieval-augmented generation, as a two-step pattern: embed the query, retrieve relevant documents, generate the answer. That’s the correct shape for explanation. It’s also the wrong shape for production. Most working RAG systems have &lt;em&gt;three&lt;/em&gt; steps, and the missing middle one is where the quality lives.&lt;/p&gt;

&lt;p&gt;This post is about that middle step.&lt;/p&gt;

&lt;h3 id=&quot;why-a-single-retrieval-pass-isnt-enough&quot;&gt;Why a single retrieval pass isn’t enough&lt;/h3&gt;

&lt;p&gt;The retrieval step in RAG uses what’s called a bi-encoder: an encoder model (usually BERT-family, see &lt;a href=&quot;/writing/the-other-transformers/&quot;&gt;The Other Transformers&lt;/a&gt;) that produces a single vector for each piece of text. The query gets one vector. Each document gets one vector. You compare them by cosine similarity, the closer the angle, the more similar the texts.&lt;/p&gt;

&lt;p&gt;This is fast. Embarrassingly fast. You can pre-compute the document vectors once and store them in a database. At query time, you only need to embed the query (a few milliseconds) and find the nearest neighbours (a few more milliseconds, even across millions of documents). It scales to web-search levels.&lt;/p&gt;

&lt;p&gt;It’s also kind of dumb.&lt;/p&gt;

&lt;p&gt;The bi-encoder embeds the query and the document independently. The model never sees them together. It produces a vector for the query that captures the query’s meaning in general, and a vector for the document that captures the document’s meaning in general, and then you compare those two general representations. There’s no opportunity for the model to notice that this &lt;em&gt;specific&lt;/em&gt; query is asking about a &lt;em&gt;specific&lt;/em&gt; aspect of this &lt;em&gt;specific&lt;/em&gt; document.&lt;/p&gt;

&lt;p&gt;In practice this means bi-encoders are good at finding documents that are &lt;em&gt;topically related&lt;/em&gt; to the query. They’re less good at finding the documents that &lt;em&gt;actually answer&lt;/em&gt; the query. Two documents about the same topic can have very similar embeddings even if only one of them contains the answer.&lt;/p&gt;

&lt;p&gt;For a vague question like “what’s our refund policy?” topical similarity is enough. For a specific question like “can I get a refund on a digital download after 30 days if I haven’t used it?” you need a model that can read the query and the candidate documents &lt;em&gt;together&lt;/em&gt; and decide which one actually addresses the conditions.&lt;/p&gt;

&lt;p&gt;That’s a cross-encoder.&lt;/p&gt;

&lt;h3 id=&quot;what-a-cross-encoder-is&quot;&gt;What a cross-encoder is&lt;/h3&gt;

&lt;p&gt;A cross-encoder is the same architecture (an encoder &lt;label for=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-transformer&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-transformer-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;transformer&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-transformer&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-transformer-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Transformer&lt;/span&gt;The neural network architecture that underpins modern LLMs – stacks of self-attention layers that let every token look at every other token in the context.
&lt;/span&gt;) used a different way. Instead of producing a vector for each text, it takes a pair of texts, query and candidate document, and produces a single relevance score.&lt;/p&gt;

&lt;p&gt;The query and document get concatenated with a separator &lt;label for=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;token&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt;, fed through the model together, and the model’s full &lt;label for=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-attention&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-attention-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;attention&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-attention&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-reranker-you-didnt-know-you-needed-attention-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Attention&lt;/span&gt;The mechanism inside a transformer that lets each token weigh how much every other token in the context matters to it.
&lt;/span&gt; mechanism gets to see every query token attend to every document token and vice versa. The output is one number: how well does this document answer this query?&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[CLS] can I get a refund on a digital download after 30 days [SEP]
Refund policy: physical goods may be returned within 30 days. Digital
downloads are non-refundable once purchased. [SEP]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The model reads that and outputs, say, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.91&lt;/code&gt;, the document is highly relevant because it directly addresses both “digital download” and “refund,” even though the answer is “no.” A different document that only mentions the 30-day window for physical goods might score &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.34&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cross-encoders are dramatically more accurate than bi-encoders for relevance. They’re also dramatically slower. Because the model has to see the query and document together, you can’t pre-compute anything, every query against every candidate is a fresh forward pass. If you have a million documents and you ran the cross-encoder against all of them, you’d be waiting weeks per query.&lt;/p&gt;

&lt;p&gt;Which is why you don’t do that. You do retrieve-then-rerank.&lt;/p&gt;

&lt;h3 id=&quot;the-two-stage-pattern&quot;&gt;The two-stage pattern&lt;/h3&gt;

&lt;p&gt;The standard production RAG pipeline is:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Retrieval (bi-encoder). Embed the query, find the top 50-200 candidate documents from the vector database. Fast, parallel, scalable.&lt;/li&gt;
  &lt;li&gt;Reranking (cross-encoder). Score each of those candidates against the query using a cross-encoder. Pick the top 3-10 by score.&lt;/li&gt;
  &lt;li&gt;Generation (LLM). Pass the top reranked documents into the LLM along with the query. Generate the answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The retrieval stage is “we cast a wide net, fast.” The reranking stage is “we read each catch carefully, slowly, but only the ones in the net.” Together they let you get cross-encoder-quality relevance at bi-encoder-scale corpus sizes.&lt;/p&gt;

&lt;p&gt;The numbers are striking. For a corpus of one million documents:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Bi-encoder only: ~10ms per query, mediocre relevance.&lt;/li&gt;
  &lt;li&gt;Cross-encoder only: ~1,000,000 model calls per query. Untenable.&lt;/li&gt;
  &lt;li&gt;Bi-encoder + cross-encoder: ~10ms retrieval + ~200ms reranking on 100 candidates = ~210ms total, with relevance approaching cross-encoder-only quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That third option is what every serious RAG system is doing. The blog posts that don’t mention it are showing you the demo, not the production system.&lt;/p&gt;

&lt;h3 id=&quot;models-you-can-actually-use&quot;&gt;Models you can actually use&lt;/h3&gt;

&lt;p&gt;Reranker models are a small but mature corner of the open-source ecosystem.&lt;/p&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Model&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Made by&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Open / closed&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Notable for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;BGE Reranker (v2-m3, large)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;BAAI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Strong default, multilingual, well-supported&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Cohere Rerank&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Cohere&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Closed (API)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Easy integration, multilingual, pay-per-call&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Voyage Rerank&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Voyage AI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Closed (API)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;High quality, instruction-tuned variants&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;ms-marco-MiniLM-L-6-v2&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;sentence-transformers&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Tiny (22M params), runs on CPU, fine for English&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Jina Reranker&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Jina AI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open / API&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Long-context variants for document-level reranking&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;The lightweight ones (the MiniLM cross-encoders, around 20-100M parameters) run on a CPU. The heavyweight ones (BGE Reranker v2-m3, around 568M parameters) want a GPU but produce noticeably better rankings. For most projects the correct starting point is the smallest open model that fits your latency budget; you can swap up if quality demands it.&lt;/p&gt;

&lt;h3 id=&quot;when-reranking-earns-its-keep&quot;&gt;When reranking earns its keep&lt;/h3&gt;

&lt;p&gt;Not every retrieval task needs a reranker. The benefit grows with task difficulty:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Vague topical queries against a small corpus: bi-encoder is fine. “Tell me about our company values” against a 50-document handbook will return the correct document on cosine similarity alone.&lt;/li&gt;
  &lt;li&gt;Specific factual queries against a medium corpus: reranker helps. “What’s the SLA for our enterprise tier?” against a thousand-document knowledge base benefits from the cross-encoder noticing that the document mentioning &lt;em&gt;enterprise tier SLAs specifically&lt;/em&gt; is more relevant than the one with the same words in a marketing context.&lt;/li&gt;
  &lt;li&gt;Long-tail queries against a large corpus: reranker is essential. Web-scale search, code search, scientific literature search, the bi-encoder will return a heap of plausible-but-not-quite candidates, and the reranker is what separates them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern: bi-encoders fail by returning plausibly-related but not actually-answering documents. If your eval set is full of cases like that, you need a reranker. If your bi-encoder is missing the correct document entirely (it’s not in the top 200), reranking won’t save you, you need better embeddings or a hybrid retrieval strategy. Different problem.&lt;/p&gt;

&lt;h3 id=&quot;hybrid-retrieval-the-other-thing-you-might-be-missing&quot;&gt;Hybrid retrieval: the other thing you might be missing&lt;/h3&gt;

&lt;p&gt;While we’re here, the second-most-skipped step in RAG explanations: hybrid retrieval.&lt;/p&gt;

&lt;p&gt;Bi-encoders work on semantic meaning. They’re great at handling paraphrase (“how do I cancel?” finds documents about “subscription termination”). They’re weak at exact matches, product codes, person names, error messages, version numbers. The vector for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KB-ERR-2847-fatal&lt;/code&gt; doesn’t necessarily live near the vector for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2847&lt;/code&gt; in embedding space, because the model has never seen that specific string and treats it as a sequence of arbitrary subword tokens.&lt;/p&gt;

&lt;p&gt;Hybrid retrieval combines a semantic search (bi-encoder, dense vectors) with a lexical search (BM25, sparse keyword matching) and merges the results. The semantic search catches paraphrase. The lexical search catches exact matches. The reranker takes the union and sorts it.&lt;/p&gt;

&lt;p&gt;In production:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Semantic retrieval returns top 100 by embedding similarity.&lt;/li&gt;
  &lt;li&gt;Lexical retrieval returns top 100 by BM25 score.&lt;/li&gt;
  &lt;li&gt;Merge, take the union (often 150-200 documents after dedup).&lt;/li&gt;
  &lt;li&gt;Rerank with a cross-encoder, take the top 5-10.&lt;/li&gt;
  &lt;li&gt;Generate with the LLM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern, often called hybrid retrieval with cross-encoder reranking, is the realistic shape of a production RAG system in 2026. The blog-post version with one embedding lookup is the simplification.&lt;/p&gt;

&lt;h3 id=&quot;a-decision-table&quot;&gt;A decision table&lt;/h3&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Symptom&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Likely fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;The correct document is in the top 50 but not the top 5&quot;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Add a reranker&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;The correct document isn&apos;t in the top 50 at all&quot;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Better embeddings, or hybrid retrieval (BM25 + semantic), or chunk differently&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;It can&apos;t find specific product codes / IDs&quot;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Hybrid retrieval, you need lexical matching&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;The LLM is confused by too many candidates&quot;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Lower top-k after reranking; trust the reranker to filter&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;Latency is too high&quot;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Smaller reranker (MiniLM cross-encoders), or fewer candidates into the reranker&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;Quality varies wildly between users&quot;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Likely a chunking or query-rewriting issue, not a reranker issue&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;The shortcut version of RAG, embed, look up, generate, works in the demo because the demo corpus is small and the demo questions are vague. The production version has to handle a thousand specific questions against a million documents, and that’s where the bi-encoder’s independence starts to hurt. Embedding the query and the document separately is what makes retrieval scale, and it’s also what stops the model noticing whether the candidate it returned actually answers the question or merely shares a topic with it. The cross-encoder is the cure for that, because it reads the pair together and lets attention work across both halves. The price is speed, which is why nobody runs a cross-encoder against the whole corpus. They run it against the top hundred the bi-encoder fished out, and they merge in BM25 results so the product codes and error strings don’t get lost in the semantic blur.&lt;/p&gt;

&lt;p&gt;A reranker can only do its job if the correct document already made it into the candidate set. If the bi-encoder misses entirely, no amount of reranking will recover the answer, the fix lives in the chunking, the embeddings, or the lexical search. Worth knowing which symptom you have before you start tuning.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Knife in My Hand</title>
    <link href="/writing/the-knife-in-my-hand/"/>
    <updated>2026-05-01T06:00:00+08:00</updated>
    <id>/writing/the-knife-in-my-hand/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/consulting-and-craft/&quot;&gt;Consulting and Craft&lt;/a&gt; &amp;middot; &lt;a href=&quot;/writing/through-the-kitchen/&quot;&gt;Through the Kitchen&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;My kitchen knives are not beautiful. Blue plastic handles, no rivets, nothing decorative. They look like what they are: tools for working in a kitchen, not display pieces.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;They are, however, extremely good. Heavy, precisely ground, tough enough that I haven’t chipped one in years of real use, sharp enough to cut a tomato under the weight of the blade alone. Sized correctly for my hands. I maintain them. I know them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;They live in a knife roll in a kitchen drawer. Canvas keeps the edges separated, a drawer keeps them away from small prying hands not yet ready to handle a very sharp knife. Six in the roll: a steel, a chef’s knife, a fish knife, a Santoku, a butcher’s dagger, and a paring knife.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is about what I’ve learned through those knives. Most of it turns out to apply to software.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;a-dull-knife-is-more-dangerous-than-a-sharp-one&quot;&gt;A dull knife is more dangerous than a sharp one&lt;/h3&gt;

&lt;p&gt;The first thing any cook learns, and the last thing home cooks seem to believe, is that a dull knife is more dangerous than a sharp one.&lt;/p&gt;

&lt;p&gt;A sharp knife bites into what you’re cutting where you put it. A dull knife slides. It requires more force to do the same work, and force that isn’t going into the cut has to go somewhere, usually into the hand of the person holding the knife, on the day they’re tired and the tomato is firmer than they expected. Dull-knife injuries are the ones that need stitches.&lt;/p&gt;

&lt;p&gt;The analogue in engineering is almost embarrassingly direct. A broken test suite is more dangerous than no test suite. A stale monitoring dashboard is more dangerous than no dashboard. A CI pipeline that’s “mostly green” is more dangerous than one that’s explicitly red. The thing you want is a tool that gives you a clean, honest signal and that you trust enough to act on. A tool you mistrust, that slides off the tomato, that you have to lean on to get through: that tool is the injury waiting to happen.&lt;/p&gt;

&lt;p&gt;Sharpen your knives. Fix your tests. Don’t tolerate dullness. Dull, blunt, less likely to cut: these things aren’t safer, no matter how they look.&lt;/p&gt;

&lt;h3 id=&quot;pick-the-right-tool-then-practise-with-it&quot;&gt;Pick the right tool. Then practise with it.&lt;/h3&gt;

&lt;p&gt;The chef’s knife rocks through dense vegetables. The Santoku slices straight down through soft fruit. The paring knife works in tight space against your thumb. The fish knife flexes to follow a spine. None of them is a “better knife” in the abstract; each one exists because it fits a different job. Trying to bone a fish with a chef’s knife is awkward no matter how skilled you are. The first move in any cut is choosing the right knife for it.&lt;/p&gt;

&lt;p&gt;The second move is having practised with it. When I’m cooking I reach for the chef’s knife without looking. I know its weight, how it rocks, how much pressure it takes through a carrot or a butternut squash. I know the Santoku picked up a spot of rust a few weeks ago (left in the sink too long after a distracted Sunday) and how it felt under the cloth when I worked it out. The muscle memory this develops doesn’t replace choosing the correct knife; it means I’m faster, safer, and more comfortable using the most accurate tool.&lt;/p&gt;

&lt;p&gt;Picking up a new knife costs you. Hand me a hand-forged Japanese knife tomorrow and I’d be slower with it for a week. The handle the wrong shape, the balance different. I’d be thinking about the knife instead of the food. That dip is real, and it’s the price of upgrading, not a reason to skip the upgrade. The tool that promises to make you 20% faster once you’ve learned it will make you 40% slower for the three months you’re learning it, and then 10% faster forever. Nothing ever meets the hype, but the right tool can get close enough. Pay the dip once for the right tool and you recoup it the rest of your career.&lt;/p&gt;

&lt;p&gt;Some tools never fit. A knife with the wrong handle for your hand causes fatigue every cut: no amount of practice fixes that. The learning dip is recoupable; an ill-fitting tool is friction forever. Selection comes before practice, and you can’t practise your way out of a bad selection. Software architecture is the same kind of choice, the framework, the database, the language. Get those right and practice compounds; get them wrong and practice runs into walls.&lt;/p&gt;

&lt;p&gt;The trap isn’t deliberate upgrades; it’s churning. Picking up a new knife, or a new editor, or a new build tool, every month, never quite getting past the dip, never quite cashing in the upside. Pick deliberately. Then put in the practice.&lt;/p&gt;

&lt;h3 id=&quot;practice-is-the-cut-not-the-recipe&quot;&gt;Practice is the cut, not the recipe&lt;/h3&gt;

&lt;p&gt;The way you get good with a knife is that you cut. A lot. You cut deliberately slowly to pay attention to grip and rhythm, and you cut at speed when you’re in a hurry and discover the edges of your technique.&lt;/p&gt;

&lt;p&gt;The technique is the floor, not the ceiling. You learn it in a weekend. Then you spend ten years making it &lt;em&gt;automatic&lt;/em&gt;: so automatic that you stop thinking about the knife and start thinking about the food. The difference between a good home cook and a professional is almost never knowledge; it’s &lt;em&gt;repetition&lt;/em&gt;. Professionals have cut ten thousand onions. You have cut one hundred. They’re not smarter; they’re smoother.&lt;/p&gt;

&lt;p&gt;You don’t “know” SQL after reading a book. You know SQL after a thousand queries and several slow joins you had to rewrite. The book is the technique; the queries are the practice.&lt;/p&gt;

&lt;p&gt;Speed comes from practice, not pressure. Every time I’ve seen an engineer try to move faster by concentrating harder, they’ve moved slower. Every time I’ve seen one move faster by doing something they’d done a hundred times before without thinking about it, they’ve been right.&lt;/p&gt;

&lt;h3 id=&quot;care-as-part-of-the-craft&quot;&gt;Care as part of the craft&lt;/h3&gt;

&lt;p&gt;Every time I pull the knife out of the roll, I run it a few strokes down the honing steel. Ten seconds. I do it so automatically I’d feel wrong starting to cut without it.&lt;/p&gt;

&lt;p&gt;When I’m done, I wash the knife by hand (dishwashers are hostile to good knives), dry it on a tea towel, and slide it back into its slot. Thirty seconds, every time, for so long that it isn’t a chore; it’s just what happens at the end of cooking.&lt;/p&gt;

&lt;p&gt;Every few months I take the knives to a professional sharpener. I hone them every day because that’s use and maintenance in the same motion. But when they need actually &lt;em&gt;sharpening&lt;/em&gt; (a proper regrind) I hand them to someone whose whole craft is sharpening knives. There is no prize for doing everything yourself.&lt;/p&gt;

&lt;p&gt;The care is not separate from the cutting; it’s the same practice. A knife used a lot and cared for consistently gets better over time. A knife used a lot and cared for occasionally gets worse, because damage accumulates faster than attention.&lt;/p&gt;

&lt;p&gt;The single biggest thing separating engineers who get better from engineers who plateau is whether they care for their tools alongside using them. Whether their editor config quietly improves. Whether they know the keyboard shortcut for the thing they do fifty times a day. None of it is glamorous. None shows up on a CV.&lt;/p&gt;

&lt;h3 id=&quot;six-knives&quot;&gt;Six knives&lt;/h3&gt;

&lt;p&gt;Six knives in a roll, a board, a professional sharpener every few months. I can make almost any dinner in the world from those objects plus a pan and some heat.&lt;/p&gt;

&lt;p&gt;I’ve been tempted to buy more. The internet is very good at trying to sell me more: beautifully photographed knives with long waiting lists and three-figure price tags, the kind that live on magnetic strips in other people’s kitchens. My knives cut something every day. They don’t look like anything special. They work.&lt;/p&gt;

&lt;p&gt;The craft is not in the accumulation of tools, and certainly not in the &lt;em&gt;appearance&lt;/em&gt; of them. The craft is in picking the right tools and putting in the work to know them. The tools that get photographed are rarely the tools that get used.&lt;/p&gt;

&lt;p&gt;I look at my terminal and see the same thing. A few commands and shortcuts I’ve used so many times they feel like extensions of my hand. The rest is decoration.&lt;/p&gt;

&lt;p&gt;Pick your tools. Keep them sharp. Use them every day. Practise. Replace one deliberately, when you can name the thing it doesn’t do that you now know you need.&lt;/p&gt;

&lt;p&gt;The practice is the point.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Time Is Weirder Than You Think</title>
    <link href="/writing/time-is-weirder-than-you-think/"/>
    <updated>2026-04-30T06:00:00+08:00</updated>
    <id>/writing/time-is-weirder-than-you-think/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;In &lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;What Time Is It?&lt;/a&gt; we untangled the human mess of the hour. In &lt;a href=&quot;/writing/what-day-is-it/&quot;&gt;What Day Is It?&lt;/a&gt; we did the same for the calendar. In &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;Ticks or Tocks?&lt;/a&gt; we traced the physics of the second from quartz crystals to optical lattice clocks that won’t lose a tick in the lifetime of the universe. All of those stories treated time as something that flows at the same rate everywhere, a backdrop against which clocks are merely more or less accurate. That assumption is wrong. Time itself bends.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;einstein-enters-the-chat&quot;&gt;Einstein enters the chat&lt;/h3&gt;

&lt;p&gt;Einstein’s special theory of relativity, published in 1905, showed that time passes more slowly for objects moving at high speeds relative to an observer. This isn’t a theoretical curiosity; it’s measurable. In 1971, Hafele and Keating flew caesium clocks on commercial airliners around the world and compared them to reference clocks on the ground. The flying clocks disagreed with the ground clocks by exactly the amount relativity predicted (Hafele &amp;amp; Keating, 1972, &lt;em&gt;Science&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;The speed of light as a universal speed limit. Nothing with mass can reach the speed of light. As you approach it, time dilation increases without bound. At the speed of light, time stops entirely. From a photon’s frame of reference (to the extent that’s meaningful), no time passes at all. A photon emitted from a star ten billion light-years away has, from its own perspective, arrived at your eye instantaneously.&lt;/p&gt;

&lt;p&gt;Muon decay provides one of the cleanest experimental demonstrations. Cosmic ray muons are created in the upper atmosphere and should decay in roughly 2.2 microseconds, which at near-light speed would let them travel only about 660 metres. But we detect them at sea level, roughly 15 kilometres below where they were created. How? At 99% of the speed of light, their time is dilated by a factor of roughly seven. They “live” long enough to reach us. Rossi and Hall first confirmed this in 1941 (&lt;em&gt;Physical Review&lt;/em&gt;), and it remains one of the most intuitive demonstrations of special relativity.&lt;/p&gt;

&lt;h3 id=&quot;the-twin-paradox&quot;&gt;The twin paradox&lt;/h3&gt;

&lt;p&gt;Special relativity produces a result so counterintuitive that it has its own name. Take two twins: one stays on Earth, the other takes a round trip to a distant star at near-light speed. When the travelling twin returns, less time has passed for them. They are younger than their sibling. This is not an illusion or an accounting trick; it’s a real, physical difference in elapsed time.&lt;/p&gt;

&lt;p&gt;The “paradox” label is misleading. There’s no logical contradiction. The resolution is that the two twins are not in symmetric situations: one of them accelerated (turned around), and that breaks the symmetry. The twin who stayed home followed an inertial path through spacetime: no acceleration, no turning around. In relativity, the straighter your path through spacetime, the more time you experience. It’s counterintuitive: we’re used to thinking that straight lines are shortest, but in spacetime, a straight path is the one that ages you the most. Any acceleration, any turning around, reduces the elapsed time. This is why the travelling twin ages less.&lt;/p&gt;

&lt;p&gt;The effect doesn’t require a spaceship. The International Space Station orbits at about 7.7 kilometres per second. Astronauts on the ISS age very slightly slower than people on the ground, roughly 0.01 seconds less per year. Scott Kelly, who spent 340 days aboard the ISS in 2015-2016 while his identical twin Mark stayed on Earth, returned about 5 milliseconds younger than he would have been had he stayed home. Not enough to matter biologically. Enough to prove the physics is real.&lt;/p&gt;

&lt;h3 id=&quot;supersonic-time-travel&quot;&gt;Supersonic time travel&lt;/h3&gt;

&lt;p&gt;Concorde, that beautiful, impractical supersonic airliner, offered a surreal temporal experience. You could leave London at 10:30 AM and arrive in New York at 9:30 AM the same day, arriving before you departed by clock time. The crossing took about three and a half hours, but the five-hour time difference meant you gained more than you spent.&lt;/p&gt;

&lt;p&gt;This wasn’t relativity; it was time zones. But the special relativistic effect was real too, if tiny. Concorde flew at roughly Mach 2: twice the speed of sound, about 600 metres per second. At that speed, the time dilation factor is approximately 1 + 2 x 10^-12, which means passengers aged about 0.000000002% less than people on the ground per flight. Over a career of flying Concorde, a pilot might have “saved” a few hundred nanoseconds of biological time. Not enough to notice. Enough to measure.&lt;/p&gt;

&lt;p&gt;The more interesting effect was the experience itself. Westbound on Concorde, the sun appeared to move backwards in the sky. You were flying faster than the Earth rotates at that latitude. For the duration of the flight, you were outrunning the planet’s spin. It’s the closest any commercial passengers ever came to the intuitive experience of time running in an unusual direction.&lt;/p&gt;

&lt;h3 id=&quot;gravity-bends-time&quot;&gt;Gravity bends time&lt;/h3&gt;

&lt;p&gt;Einstein’s general theory of relativity, from 1915, added another twist: time passes more slowly in stronger gravitational fields. The closer you are to a massive object, the slower your clock ticks relative to someone further away. A clock on the floor of your house runs very slightly slower than a clock on your roof. The difference is about 10 nanoseconds per year per metre of altitude, which doesn’t affect your morning routine but absolutely matters for GPS.&lt;/p&gt;

&lt;p&gt;GPS satellites orbit at about 20,200 km above the Earth. Their clocks tick faster than ground clocks by about 45 microseconds per day due to weaker gravity up there. They tick &lt;em&gt;slower&lt;/em&gt; by about 7 microseconds per day due to their orbital speed. The net effect is that satellite clocks gain roughly 38 microseconds per day relative to the ground. If this weren’t corrected, GPS positions would drift by about 10 kilometres per day. Every GPS satellite has its clock rate deliberately adjusted before launch to compensate.&lt;/p&gt;

&lt;p&gt;This means that when you use your phone to navigate to a restaurant, you are relying on corrections derived from general relativity. Einstein helps you find pizza.&lt;/p&gt;

&lt;p&gt;The gravitational effect has been measured with astonishing precision. In 2010, optical clocks at NIST detected the difference in time flow between two clocks separated by just 33 centimetres of altitude (Chou et al., 2010, &lt;em&gt;Science&lt;/em&gt;). Time really does run at different speeds depending on where you are in a gravitational field. There is no single “correct” rate at which time passes. It’s always relative to something.&lt;/p&gt;

&lt;p&gt;This has practical consequences beyond GPS. The definition of UTC itself requires a choice: the clocks that contribute to UTC are at different altitudes and latitudes, so they tick at slightly different rates due to gravity. The BIPM corrects all contributing clocks to the rate they would tick at the “geoid”: the mean sea-level gravitational potential of the Earth. A clock in Boulder, Colorado (1,655 metres above sea level) ticks faster than one in London (near sea level) by roughly 15 microseconds per year. Without the geoid correction, the ensemble average would be meaningless; you’d be averaging clocks that are physically keeping different times. The concept of “a second” on Earth is, in a gravitational sense, a political decision about which altitude to use.&lt;/p&gt;

&lt;h3 id=&quot;the-universes-default-clock-rate&quot;&gt;The universe’s default clock rate&lt;/h3&gt;

&lt;p&gt;The geoid is a local compromise: we picked Earth’s mean sea level and called it “the reference.” But zoom out and the same problem applies everywhere. Every mass in the universe, every star, planet, galaxy cluster, sits in a gravitational well where time runs slower. A clock in deep intergalactic space, far from any significant mass, ticks faster than any clock on any planet. That hypothetical far-from-everything clock is as close as you can get to time running “undiluted”: the fastest rate time can flow.&lt;/p&gt;

&lt;p&gt;There is no single point where gravity’s influence drops to exactly zero. Gravity has infinite range, and the universe is full of mass, so every location experiences &lt;em&gt;some&lt;/em&gt; gravitational time dilation. But the effect falls off sharply with distance. In the great voids between galaxy clusters (regions hundreds of millions of light-years across containing almost nothing) gravitational time dilation is vanishingly small. For all practical purposes, that’s where time runs at its natural rate.&lt;/p&gt;

&lt;p&gt;This creates an odd inversion of perspective. We think of time on Earth as “normal” and relativistic corrections as exotic. But from the universe’s point of view, we’re the anomaly. We live at the bottom of a gravitational well. Our clocks are the slow ones. Imagine you grew up in a swimming pool and thought water resistance was just how movement worked. Then someone drained the pool and you felt what running is like without the drag. Deep space is the drained pool. We’ve been wading our whole lives.&lt;/p&gt;

&lt;p&gt;The practical consequence is that there’s no privileged clock in the universe. UTC is corrected to the geoid, but the geoid is a human choice, not a physical constant. A civilisation on a neutron star would pick a very different reference, one where “a second” on their surface lasts far longer than ours. Neither civilisation’s second is more correct than the other’s. “How fast does time pass?” isn’t a question with an answer until you specify &lt;em&gt;where&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;the-young-heart-of-the-earth&quot;&gt;The young heart of the Earth&lt;/h3&gt;

&lt;p&gt;We don’t need black holes to see gravitational time dilation at work on a grand scale. The core of the Earth, being under more gravitational stress than the surface, has experienced less elapsed time since the planet formed. The centre of the Earth is roughly 2.5 years younger than the surface, not metaphorically but in actual measured atomic clock ticks. Time has passed more slowly down there for 4.5 billion years, and it adds up. Feynman mentioned a version of this calculation; it was rigorously computed by Uggerhoj et al. (2016, &lt;em&gt;European Journal of Physics&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;This is not a thought experiment; it’s a straightforward consequence of general relativity applied to the known density and gravitational profile of the Earth. If you could somehow place a clock at the centre of the planet when it formed and retrieve it today, it would show a date 2.5 years behind a clock that had spent its life on the surface. The rock beneath your feet is, in a physically meaningful sense, younger than the rock you’re standing on.&lt;/p&gt;

&lt;h3 id=&quot;black-holes-and-the-edge-of-time&quot;&gt;Black holes and the edge of time&lt;/h3&gt;

&lt;p&gt;Near a black hole, gravitational time dilation becomes extreme. At the event horizon, the boundary beyond which nothing, not even light, can escape, time, from an outside observer’s perspective, stops entirely. An object falling toward a black hole appears to slow down asymptotically, growing dimmer and redder, never quite crossing the horizon from the viewpoint of someone watching from a safe distance. The object falling in experiences time perfectly normally from its own point of view. Neither observer is wrong. Time is doing something different in each location.&lt;/p&gt;

&lt;p&gt;The mathematics are well-established. Karl Schwarzschild worked out the mathematics of what happens to spacetime around a simple, non-spinning massive object, and he did it in 1916, just months after Einstein published general relativity. His solution predicts that at the event horizon, the gravitational time dilation factor goes to infinity. Time, as experienced by a distant observer, literally ceases to advance for anything at the horizon.&lt;/p&gt;

&lt;p&gt;Inside the horizon, things get stranger still. The physics is hard to describe without the maths, but the gist is this: falling toward the centre becomes as unavoidable as the passage of time itself. You can no more stop falling inward than you can stop moving into the future. The singularity at the centre isn’t a place you travel to; it’s a moment you can’t avoid, the future that everything inside the horizon is headed toward.&lt;/p&gt;

&lt;h3 id=&quot;time-ripples&quot;&gt;Time ripples&lt;/h3&gt;

&lt;p&gt;If gravity bends time, and gravitational fields change, say, when two black holes spiral into each other, then the bending itself should propagate outward as a wave. Einstein predicted this in 1916. It took a century to confirm.&lt;/p&gt;

&lt;p&gt;On 14 September 2015, the LIGO detectors in Livingston, Louisiana, and Hanford, Washington, detected gravitational waves from two black holes merging 1.3 billion light-years away (Abbott et al., 2016, &lt;em&gt;Physical Review Letters&lt;/em&gt;). What LIGO measured was spacetime itself stretching and compressing as the wave passed through. The arms of the detector, each four kilometres long, changed length by roughly one-thousandth the diameter of a proton. That’s the most precise measurement humans have ever made.&lt;/p&gt;

&lt;p&gt;Here’s what that means for time. A gravitational wave doesn’t just stretch space; it stretches spacetime. As the wave from those merging black holes passed through Louisiana, time in the detector was oscillating: running very slightly faster, then very slightly slower, then faster again, hundreds of times per second. The oscillation was absurdly tiny, but it was real. For a fraction of a second, time in Livingston and time in Hanford were running at different rates, because the wave hit them at different moments.&lt;/p&gt;

&lt;p&gt;We usually think of time as the background against which things happen. Gravitational waves show that the background itself vibrates. Time has ripples. They’re passing through you right now: from distant supernovae, from colliding neutron stars, from black holes that merged before the Earth existed. You can’t feel them. LIGO can.&lt;/p&gt;

&lt;h3 id=&quot;so-what-time-is-it&quot;&gt;So what time is it?&lt;/h3&gt;

&lt;p&gt;After all of this (the &lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;human history&lt;/a&gt; of sundials and railways and political time zones, the &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;physics&lt;/a&gt; of caesium atoms and clock ensembles, the relativity that bends time near massive objects and at high speeds) the answer is: it depends.&lt;/p&gt;

&lt;p&gt;It depends on where you are in a gravitational field. It depends on how fast you’re moving. It depends on which timescale you’ve chosen and why. It depends on whether you care about the sun’s position, or the purity of atomic seconds, or the agreement between your timestamp and everyone else’s.&lt;/p&gt;

&lt;p&gt;The phone in your pocket hides all of this heroically. It receives signals from GPS satellites that have been corrected for both special and general relativistic effects. It knows your time zone from your location. It knows about DST transitions from a regularly updated database. It adjusts for leap seconds, or at least it tries to. It presents you with a number that looks simple and authoritative, and you glance at it and get on with your day.&lt;/p&gt;

&lt;p&gt;Underneath, it’s leaning on millennia of astronomy, centuries of mechanical engineering, decades of atomic physics, and Einstein. It’s a tower of clever hacks and hard-won compromises, and it’s a miracle it works at all.&lt;/p&gt;

&lt;p&gt;But we’ve only covered what time &lt;em&gt;does&lt;/em&gt;: how it bends near mass, dilates with motion, ripples across the universe. The harder question is whether time fundamentally &lt;em&gt;exists&lt;/em&gt;. The arrow that distinguishes past from future isn’t in the equations. “Now” isn’t a location in spacetime. The equations of quantum gravity may contain no time variable at all.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/writing/does-time-even-exist/&quot;&gt;Does Time Even Exist?&lt;/a&gt; is next: a tour of the foundations, from the block universe to the holographic principle and the physicists who think time is a shadow of something simpler.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Picking the AWS AI Service Tier for Each Feature</title>
    <link href="/writing/picking-the-aws-ai-service-tier-for-each-feature/"/>
    <updated>2026-04-29T06:00:00+08:00</updated>
    <id>/writing/picking-the-aws-ai-service-tier-for-each-feature/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;AI Practitioner&lt;/strong&gt; · AIF-C01 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A mid-sized B2B SaaS runs a help-desk product. Customers raise support tickets in a web form; the text flows through queues, is triaged by a rules engine, and lands in an &lt;label for=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-ai-agent&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-ai-agent-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;agent&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-ai-agent&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-ai-agent-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Agent&lt;/span&gt;A system that wraps an LLM with tools, memory, and a loop, so it can take multi-step actions toward a goal rather than just answering one prompt.
&lt;/span&gt;’s inbox. The CEO has returned from a conference with a directive: “add AI.” Board presentation in three weeks.&lt;/p&gt;

&lt;p&gt;The PM has a backlog of half-formed ideas, six of them:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Sentiment on inbound tickets, flag angry customers so agents prioritise them.&lt;/li&gt;
  &lt;li&gt;Auto-translate tickets from non-English customers into the agent’s language, and the agent’s reply back.&lt;/li&gt;
  &lt;li&gt;Extract structured fields from attached PDFs, invoices, purchase orders, so agents don’t retype.&lt;/li&gt;
  &lt;li&gt;Moderate screenshots for anything NSFW before a human sees them.&lt;/li&gt;
  &lt;li&gt;Draft suggested replies based on the ticket and the knowledge base.&lt;/li&gt;
  &lt;li&gt;Rank the backlog by predicted priority using last year’s labelled tickets.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Six features. The board wants “AI.” The PM wants a plan that picks the shortest path for each.&lt;/p&gt;

&lt;p&gt;Constraints are harsh but familiar: no ML background on the team (three backend engineers, one front-end, no data scientist); fast time-to-prototype (something running against real tickets within the fortnight); predictable cost (a line item finance can sign off without a model-unit-hour forecast); and no infrastructure the team has to babysit (managed endpoints, not GPU fleets).&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;Before reaching for a service, be honest about what “add AI” actually has to mean for a team without ML staff.&lt;/p&gt;

&lt;p&gt;The first thing is that the cheapest AI feature is one AWS has already built. If the task is “flag angry emails” or “pull fields off an invoice,” those are problems many companies have; there’s a fair chance AWS has shipped a service that does exactly that. Starting at that layer, managed, task-specific, one API call, beats anything bespoke on time-to-prototype, cost, latency, and stability of behaviour. Only if no pre-built answer exists does it make sense to go up a layer.&lt;/p&gt;

&lt;p&gt;The second is what happens the week after launch. A prototype is easy; a prototype the team has to keep alive for a year is harder. A managed service that AWS updates is no-maintenance. A foundation-model &lt;label for=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; on Bedrock is low-maintenance but drifts when the vendor retrains. A bespoke SageMaker model is high-maintenance, training data, drift monitoring, endpoint scaling, retraining cadence. The PM’s fortnight should generate something closer to the first shape than the third.&lt;/p&gt;

&lt;p&gt;The third is cost predictability. Finance wants a line item. Per-request, per-page, per-character, per-token pricing gives a line item; it scales with use, which is usually fine. Per-instance-hour pricing for inference endpoints is a capacity forecast, uncomfortable when the product is new and the load is unknowable. Training jobs are per-compute-hour spikes with no guarantee the output model is actually good. The shape of the bill has to match the predictability of the product.&lt;/p&gt;

&lt;p&gt;The fourth is time-to-quality. “Working prototype” is not “good enough to ship.” A managed service ships with AWS’s quality baseline; a tuned prompt ships with whatever the tuner can squeeze out of a general model; a bespoke model ships with whatever the data supports and the team can evaluate. The team has no evaluation muscle, which means the further up the custom stack they go, the longer they spend &lt;em&gt;not sure if it’s good enough&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The fifth is how many of these features are really the same problem. Sentiment and moderation and field extraction are recognisably distinct; “draft a reply” and “rank by priority” look different from those and different from each other. A programme plan that picks the shortest path per feature, not the same path for all six, gets to shipped faster than one that forces everything through one layer.&lt;/p&gt;

&lt;p&gt;Finally, the correct answer for one is not the correct answer for all. Some of the backlog is Layer 1 (managed service exists); some is Layer 2 (general-purpose &lt;label for=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; with a prompt); some is Layer 3 (bespoke model). The programme has three shapes of work, not one. The PM’s job for the fortnight is sorting the six features into those three buckets and picking the bucket that actually ships.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Task already solved, does AWS ship a service that does this exact thing?&lt;/li&gt;
  &lt;li&gt;No ML expertise needed, team writes application code, doesn’t train models.&lt;/li&gt;
  &lt;li&gt;Predictable usage-based pricing, per-request, per-page, per-token. Scales with use.&lt;/li&gt;
  &lt;li&gt;Fully managed, no EC2, no containers, no endpoints the team provisions.&lt;/li&gt;
  &lt;li&gt;Time to prototype, days, not weeks.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-aws-ai-landscape&quot;&gt;The AWS AI landscape&lt;/h3&gt;

&lt;p&gt;AWS groups its AI offerings into three layers.&lt;/p&gt;

&lt;p&gt;Layer 1. Managed AI services. Pre-built, task-specific: detect sentiment, translate text, extract data from a form, find faces in an image. AWS trained the model, AWS hosts it, AWS updates it. The service &lt;em&gt;is&lt;/em&gt; the feature.&lt;/p&gt;

&lt;p&gt;Layer 2. Amazon Bedrock. A serverless API over a catalogue of general-purpose foundation models from Anthropic, Amazon (Nova and Titan), Meta, Mistral, Cohere, AI21, Stability, and others. One API, many models. Pick the model, write the prompt, pay per token.&lt;/p&gt;

&lt;p&gt;Layer 3. Amazon SageMaker AI. The platform for building, training, and hosting your own models. Notebooks, training jobs, inference endpoints, batch transform, feature stores, model registries. Pay per compute-hour at every stage. Where a data-science team lives when no pre-built answer fits.&lt;/p&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Layer&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Pre-built task&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;No ML expertise&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Predictable pricing&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Fully managed&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Managed AI services&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Bedrock&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;,&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SageMaker AI&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The rule of thumb: for each feature, work down the layers. Start at Layer 1; move to Layer 2 only if no managed service fits; reach Layer 3 only if neither will do.&lt;/p&gt;

&lt;h3 id=&quot;matching-six-features-to-three-layers&quot;&gt;Matching six features to three layers&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 640&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Three AI layers mapped to the six features. Layer 1 managed services for sentiment, translation, document extraction, and moderation. Layer 2 Bedrock for draft reply generation. Layer 3 SageMaker deferred for priority ranking until a data scientist exists.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .bb-bg-l1   { fill: rgba(46, 138, 90, 0.08); stroke: rgba(46, 138, 90, 0.55); stroke-width: 2; }
      .bb-bg-l2   { fill: rgba(46, 108, 170, 0.08); stroke: rgba(46, 108, 170, 0.55); stroke-width: 2; }
      .bb-bg-l3   { fill: rgba(170, 80, 46, 0.08); stroke: rgba(170, 80, 46, 0.55); stroke-width: 2; }
      .bb-card-l1 { fill: #fff; stroke: rgba(46, 138, 90, 0.85); stroke-width: 1.8; }
      .bb-card-l2 { fill: #fff; stroke: rgba(46, 108, 170, 0.85); stroke-width: 1.8; }
      .bb-card-l3 { fill: #fff; stroke: rgba(170, 80, 46, 0.85); stroke-width: 1.8; }
      .bb-pick-l1 { fill: rgba(46, 138, 90, 0.12); stroke: rgba(46, 138, 90, 0.9); stroke-width: 2; }
      .bb-pick-l2 { fill: rgba(46, 108, 170, 0.12); stroke: rgba(46, 108, 170, 0.9); stroke-width: 2; }
      .bb-pick-l3 { fill: rgba(170, 80, 46, 0.12); stroke: rgba(170, 80, 46, 0.9); stroke-width: 2; }
      .bb-gate    { fill: #fff; stroke: #666; stroke-width: 1.3; stroke-dasharray: 4 3; }
      .bb-title   { font-size: 18px; font-weight: 700; fill: #222; }
      .bb-sub     { font-size: 12px; fill: #555; }
      .bb-workload{ font-size: 14px; font-weight: 600; fill: #222; }
      .bb-detail  { font-size: 12px; fill: #333; }
      .bb-gate-text{ font-size: 12px; fill: #333; font-style: italic; }
      .bb-pick-label{ font-size: 15px; font-weight: 700; fill: #222; }
      .bb-pick-sub { font-size: 12px; fill: #333; }
      .bb-arrow    { fill: none; stroke: #555; stroke-width: 1.8; }
    &lt;/style&gt;
    &lt;marker id=&quot;bb-arrowhead&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;bb-bg-l1&quot; /&gt;
  &lt;rect x=&quot;380&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;bb-bg-l2&quot; /&gt;
  &lt;rect x=&quot;740&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;bb-bg-l3&quot; /&gt;

  &lt;text x=&quot;190&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;bb-title&quot;&gt;Layer 1. Managed&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;bb-sub&quot;&gt;task-specific services&lt;/text&gt;

  &lt;text x=&quot;550&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;bb-title&quot;&gt;Layer 2. Bedrock&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;bb-sub&quot;&gt;foundation models, prompt-driven&lt;/text&gt;

  &lt;text x=&quot;910&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;bb-title&quot;&gt;Layer 3. SageMaker&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;bb-sub&quot;&gt;bespoke models, capacity-priced&lt;/text&gt;

  &lt;rect x=&quot;50&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;bb-card-l1&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;bb-workload&quot;&gt;Four features fit here&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;sentiment, translation,&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;field extraction, moderation&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;one SDK call each&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;bb-card-l2&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;bb-workload&quot;&gt;One feature fits here&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;draft suggested reply&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;no DraftReply API exists&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;general LLM plus prompt plus KB&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;bb-card-l3&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;bb-workload&quot;&gt;One feature wants this&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;priority ranking&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;labelled tabular history&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;bb-detail&quot;&gt;classical supervised learning&lt;/text&gt;

  &lt;path d=&quot;M190,190 L190,220&quot; class=&quot;bb-arrow&quot; marker-end=&quot;url(#bb-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M550,190 L550,220&quot; class=&quot;bb-arrow&quot; marker-end=&quot;url(#bb-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,190 L910,220&quot; class=&quot;bb-arrow&quot; marker-end=&quot;url(#bb-arrowhead)&quot; /&gt;

  &lt;rect x=&quot;50&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;bb-gate&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;AWS picked the model&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;bb-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;team picks the model ID&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;bb-gate&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;team builds the model&lt;/text&gt;

  &lt;path d=&quot;M190,264 L190,294&quot; class=&quot;bb-arrow&quot; marker-end=&quot;url(#bb-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M550,264 L550,294&quot; class=&quot;bb-arrow&quot; marker-end=&quot;url(#bb-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,264 L910,470&quot; class=&quot;bb-arrow&quot; marker-end=&quot;url(#bb-arrowhead)&quot; /&gt;

  &lt;rect x=&quot;50&quot; y=&quot;294&quot; width=&quot;280&quot; height=&quot;74&quot; rx=&quot;10&quot; class=&quot;bb-gate&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;318&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;per-character / per-page / per-image&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;336&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;free tiers each service&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;354&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;latency low, behaviour stable&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;294&quot; width=&quot;280&quot; height=&quot;74&quot; rx=&quot;10&quot; class=&quot;bb-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;318&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;per-input / per-output token&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;336&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;Haiku cheap, Sonnet mid, Opus top&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;354&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;Knowledge Bases + Guardrails&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;294&quot; width=&quot;280&quot; height=&quot;74&quot; rx=&quot;10&quot; class=&quot;bb-gate&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;318&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;per-instance-hour training&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;336&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;per-instance-hour endpoints&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;354&quot; text-anchor=&quot;middle&quot; class=&quot;bb-gate-text&quot;&gt;drift monitoring required&lt;/text&gt;

  &lt;path d=&quot;M190,368 L190,470&quot; class=&quot;bb-arrow&quot; marker-end=&quot;url(#bb-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M550,368 L550,470&quot; class=&quot;bb-arrow&quot; marker-end=&quot;url(#bb-arrowhead)&quot; /&gt;

  &lt;rect x=&quot;50&quot; y=&quot;470&quot; width=&quot;280&quot; height=&quot;130&quot; rx=&quot;8&quot; class=&quot;bb-pick-l1&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;498&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-label&quot;&gt;Comprehend / Translate /&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;520&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-label&quot;&gt;Textract / Rekognition&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;548&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;ship in a fortnight&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;568&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;under $50/month combined&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;586&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;zero ML expertise needed&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;470&quot; width=&quot;280&quot; height=&quot;130&quot; rx=&quot;8&quot; class=&quot;bb-pick-l2&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;498&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-label&quot;&gt;Bedrock Converse&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;520&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;start on Claude Haiku&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;548&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;upgrade if quality demands&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;568&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;Knowledge Base for company docs&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;586&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;one prompt, one API call&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;470&quot; width=&quot;280&quot; height=&quot;130&quot; rx=&quot;8&quot; class=&quot;bb-pick-l3&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;498&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-label&quot;&gt;Defer or Canvas&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;528&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;no data scientist today&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;548&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;Autopilot rough prototype&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;568&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;or wait for the hire&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;586&quot; text-anchor=&quot;middle&quot; class=&quot;bb-pick-sub&quot;&gt;not the fortnight plan&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Five features ship in a fortnight across two layers; the sixth waits for the correct team. Three layers, work top-down, and the plan writes itself.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-managed-ai-services-in-depth&quot;&gt;The managed AI services in depth&lt;/h3&gt;

&lt;p&gt;The Layer 1 catalogue is worth knowing by name. Each has a scope, an SDK call, and a unit of billing you can put on a napkin.&lt;/p&gt;

&lt;p&gt;Amazon Comprehend. NLP over text: sentiment, entities, key phrases, language detection, PII, topic modelling. Billed in units of 100 characters, three-unit minimum per request, $0.0001 per unit for the first 10M. Free tier: 50,000 units/month for 12 months. Where ticket sentiment lives.&lt;/p&gt;

&lt;p&gt;Amazon Translate. Machine translation across 75+ languages, real-time and batch. $15 per million characters for standard. Free tier: 2M characters/month for 12 months.&lt;/p&gt;

&lt;p&gt;Amazon Textract. Extracts text, handwriting, tables, and form data from documents. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DetectDocumentText&lt;/code&gt; at $0.0015/page (raw OCR), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AnalyzeDocument&lt;/code&gt; at $0.015/page (tables) or $0.05/page (form key-value pairs). Invoices use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AnalyzeExpense&lt;/code&gt; at $0.01/page. Free tier: 1,000 pages/month for three months.&lt;/p&gt;

&lt;p&gt;Amazon Rekognition. Image and video analysis: label detection (10,000+ categories), face detection and comparison, content moderation, OCR-in-images. $0.001/image for the first million. Free tier: 1,000 images/month, per API group, for 12 months. NSFW moderation is a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DetectModerationLabels&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;Amazon Transcribe. Speech-to-text. Per second (15-second minimum); standard starts at $0.024/minute. Free tier: 60 minutes/month for 12 months.&lt;/p&gt;

&lt;p&gt;Amazon Polly. Text-to-speech. Standard voices $4/M characters; neural $16/M characters.&lt;/p&gt;

&lt;p&gt;Amazon Lex. Conversational bots. $0.004/speech request, $0.00075/text request.&lt;/p&gt;

&lt;p&gt;Amazon Kendra. Enterprise semantic search. Priced per index-hour (GenAI Enterprise from $0.32/hour), uniquely for Layer 1.&lt;/p&gt;

&lt;p&gt;Amazon Personalize. Recommendations. V2 real-time at $0.15/1,000 requests.&lt;/p&gt;

&lt;p&gt;Amazon Fraud Detector and Amazon Forecast: pre-built online-fraud scoring and time-series forecasting.&lt;/p&gt;

&lt;p&gt;Against the backlog, four of the six ideas find a managed-service home:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sentiment to Comprehend &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DetectSentiment&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Auto-translate to Translate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TranslateText&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Extract fields from PDFs to Textract &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AnalyzeDocument&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AnalyzeExpense&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Moderate screenshots to Rekognition &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DetectModerationLabels&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four features, four SDK calls, four line items.&lt;/p&gt;

&lt;h3 id=&quot;when-bedrock-is-the-answer&quot;&gt;When Bedrock is the answer&lt;/h3&gt;

&lt;p&gt;Two ideas don’t match a managed service.&lt;/p&gt;

&lt;p&gt;“Draft suggested replies based on the ticket and the knowledge base.” There’s no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DraftReply&lt;/code&gt; API, the tone, the structure, the policy constraints, and the knowledge base are all specific to this company. But it &lt;em&gt;is&lt;/em&gt; exactly what a general-purpose language model is for.&lt;/p&gt;

&lt;p&gt;Bedrock’s shape: one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Converse&lt;/code&gt; API call, pick a model ID, pass the prompt, get the generation. Per-token pricing, input and output charged separately. No training.&lt;/p&gt;

&lt;p&gt;A sample of 2026 on-demand prices, for scale rather than memorisation:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Claude Haiku, cheap, fast. Roughly $1/$5 per million input/output tokens.&lt;/li&gt;
  &lt;li&gt;Claude Sonnet, mid tier most production &lt;label for=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-rag&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-rag-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;RAG&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-rag&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-rag-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RAG&lt;/span&gt;A pattern where you retrieve relevant documents at query time and stuff them into the prompt so the model can ground its answer on them.
&lt;/span&gt; lands on. Roughly $3/$15.&lt;/li&gt;
  &lt;li&gt;Claude Opus, premium. Roughly $15/$75.&lt;/li&gt;
  &lt;li&gt;Amazon Nova Lite. Amazon’s own cheap tier, roughly $0.06/$0.24.&lt;/li&gt;
  &lt;li&gt;Meta Llama 3.1 70B, open-weight, competitive mid tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things matter for the PM. First, the model is a runtime parameter, not an architectural commitment. Switch Haiku to Sonnet via the model ID. Start cheap, upgrade only if quality doesn’t clear the bar. Second, Bedrock is serverless and per-token, same “no infrastructure, predictable cost” shape as the managed services, just with the model you chose.&lt;/p&gt;

&lt;p&gt;Bedrock’s adjacent features. Knowledge Bases for RAG, &lt;label for=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-guardrail&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-guardrail-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Guardrails&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-guardrail&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-picking-the-aws-ai-service-tier-for-each-feature-guardrail-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Guardrail&lt;/span&gt;A filter or rule applied to an LLM’s inputs or outputs to keep it inside safe, legal, or on-brand behaviour.
&lt;/span&gt; for content safety, Agents for tool-using workflows, are available without leaving the SDK. A first-cut reply drafter is Bedrock plus a Knowledge Base pointed at the docs store.&lt;/p&gt;

&lt;h3 id=&quot;when-sagemaker-is-the-answer&quot;&gt;When SageMaker is the answer&lt;/h3&gt;

&lt;p&gt;One idea fits neither Layer 1 nor Layer 2.&lt;/p&gt;

&lt;p&gt;“Rank the backlog by predicted priority using last year’s labelled tickets.” No managed service exists for priority prediction; priority is company-specific. Bedrock could be asked to assign priority via a prompt, but the input is tabular: structured ticket features plus a labelled history. Classical supervised learning, not generation.&lt;/p&gt;

&lt;p&gt;SageMaker’s parts: Studio/notebooks for exploration (per-instance-hour); training jobs (per-instance-hour); real-time inference endpoints (persistent, per-hour); serverless inference; batch transform; Autopilot and Canvas for teams without deep ML expertise (lower skill bar, not lower infrastructure).&lt;/p&gt;

&lt;p&gt;What makes it distinctively Layer 3, not a fancier Bedrock, is the shape of the work. Training a priority classifier means feature engineering, labelled data, train/test splits, hyperparameter tuning, evaluation metrics, drift monitoring. SageMaker is the toolset for doing that properly. Without the skills, SageMaker is a very expensive Jupyter notebook.&lt;/p&gt;

&lt;p&gt;The honest answer: defer the priority-ranking feature. Ship the other five on Layers 1 and 2, come back when a data scientist exists, or prototype in SageMaker Canvas with Autopilot and accept a rougher quality bar. Don’t stand up a training pipeline just to tick the “we have AI” box.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-trace-through-the-backlog&quot;&gt;A worked trace through the backlog&lt;/h3&gt;

&lt;p&gt;Sentiment. Comprehend &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DetectSentiment&lt;/code&gt;. 500-char ticket = five units at $0.0001 = $0.0005/ticket. 10,000 tickets/month = $5 before the free tier. Layer 1.&lt;/p&gt;

&lt;p&gt;Auto-translate. Translate both directions. 500 chars at $15/M = $0.0075/ticket. A thousand exchanges + replies/month = $15. Layer 1.&lt;/p&gt;

&lt;p&gt;Extract invoice PDFs. Textract &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AnalyzeExpense&lt;/code&gt; at $0.01/page. 500 attachments × 2 pages = $10. Layer 1.&lt;/p&gt;

&lt;p&gt;Moderate screenshots. Rekognition &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DetectModerationLabels&lt;/code&gt; at $0.001/image. 2,000/month = $2. Layer 1.&lt;/p&gt;

&lt;p&gt;Draft replies. Bedrock &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Converse&lt;/code&gt; to Claude Haiku. Ticket (~700 tokens) + KB excerpt (~1,500) + draft (~300 out) = 2,200 in + 300 out. At Haiku’s ~$1/$5 per million: ~$0.004/draft. 1,000/month: ~$4. Upgrade to Sonnet proportionally if quality disappoints. Layer 2.&lt;/p&gt;

&lt;p&gt;Rank backlog. Deferred; or, if a Canvas prototype is acceptable, tens of dollars in training compute plus a small endpoint. Layer 3.&lt;/p&gt;

&lt;p&gt;Total running cost for the five shipped features: well under $50/month.&lt;/p&gt;

&lt;h3 id=&quot;where-bedrock-and-managed-services-overlap&quot;&gt;Where Bedrock and managed services overlap&lt;/h3&gt;

&lt;p&gt;Some features could be done at either Layer 1 or Layer 2. Sentiment is the classic case. Comprehend has a dedicated trained classifier; Bedrock can classify via a prompt. Which is correct?&lt;/p&gt;

&lt;p&gt;Rule of thumb: prefer the managed service when one exists.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Cost. Comprehend at $0.0001/100-character unit beats Bedrock per-token pricing for short-classification tasks by an order of magnitude.&lt;/li&gt;
  &lt;li&gt;Latency. A purpose-built endpoint beats a general-purpose LLM parsing the instruction every time.&lt;/li&gt;
  &lt;li&gt;Behaviour stability. Comprehend’s API doesn’t change when AWS retrains; a Bedrock prompt drifts when the vendor ships a new model version.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The flip side: when the task is bespoke or the classes aren’t standard, Bedrock wins. Classify tickets into seven company-specific categories that don’t map onto Comprehend’s entity types? Prompt the model. The taxonomy is in the prompt, not hidden in a service’s training data.&lt;/p&gt;

&lt;p&gt;Not “always managed” or “always Bedrock”, fixed-schema tasks where the managed service fits, open-schema tasks where instruction-following is the feature.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;AWS AI sorts into three layers. Managed services for pre-built tasks. Bedrock for foundation-model access. SageMaker for custom training and hosting. Work down from the top.&lt;/li&gt;
  &lt;li&gt;The managed-service catalogue. Comprehend (text NLP), Translate, Textract (documents), Rekognition (images/video), Transcribe (speech to text), Polly (text to speech), Lex (chatbots), Kendra (search), Personalize (recommendations), Fraud Detector, Forecast.&lt;/li&gt;
  &lt;li&gt;Per-unit pricing shapes by service. Per-100-character (Comprehend), per-million-character (Translate, Polly), per-page (Textract), per-image (Rekognition), per-second (Transcribe), per-request (Lex), per-index-hour (Kendra), per-token (Bedrock), per-instance-hour (SageMaker).&lt;/li&gt;
  &lt;li&gt;Bedrock is serverless and per-token. One API over many foundation models. The model ID is a runtime parameter.&lt;/li&gt;
  &lt;li&gt;SageMaker is the build layer, not the buy layer. Reserve it for tasks that don’t exist in Layers 1 or 2. Canvas and Autopilot lower the skill bar, not the infrastructure layer.&lt;/li&gt;
  &lt;li&gt;Managed services beat Bedrock for fixed-schema, common tasks. Lower cost, lower latency, more stable. Use Bedrock when bespoke.&lt;/li&gt;
  &lt;li&gt;Most free tiers are per-month, 12 months, new-customer. Enough to prototype every feature without hitting billing.&lt;/li&gt;
  &lt;li&gt;The “no ML background” constraint filters aggressively. It eliminates SageMaker from the default answer for most problems and pushes teams toward Layers 1 and 2.&lt;/li&gt;
&lt;/ol&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Jobs to Be Done: Why Subscribers Actually Stay</title>
    <link href="/writing/jobs-to-be-done-why-subscribers-actually-stay/"/>
    <updated>2026-04-28T06:00:00+08:00</updated>
    <id>/writing/jobs-to-be-done-why-subscribers-actually-stay/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/finding-the-fit/&quot;&gt;Finding the Fit&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Greenbox is a produce-box startup that delivers weekly boxes of local farm produce to subscribers in Perth. They’ve used discovery workshops to build shared understanding and reach 200 subscribers, but now they need to grow to 1,000, and the techniques that got them here won’t answer the questions they’re facing next.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Greenbox has hit 200 subscribers. It took longer than anyone expected and involved more rework than anyone wants to admit, but the number is real. Two hundred people paying real money every week for a box of local produce.&lt;/p&gt;

&lt;p&gt;Maya secured the next funding round. The board’s new target: 1,000 active subscribers within six months. Five times the current base in half a year.&lt;/p&gt;

&lt;p&gt;Maya tells herself this on the coastal track at 5:45am on Monday, feet landing on packed sand, breath steady. The number is real. Two hundred. She’s proved something. She should feel good about it. But the board call last Thursday sits in her chest like a stone she swallowed. The new target isn’t a vote of confidence; it’s a test. Angela had said it kindly enough, “We’re excited about the trajectory, Maya”, but the slide behind Angela’s head had a red line showing where the funding ran out if they didn’t hit it.&lt;/p&gt;

&lt;p&gt;She showers, makes coffee, sits at the kitchen table with her laptop. Nadia is still asleep. The photo of her parents’ farm catches the morning light: her father standing in front of the converted dairy shed, smiling but tired. She knows that look. It’s the face of someone who believes in what they’re building but can’t yet see how it survives.&lt;/p&gt;

&lt;p&gt;She opens the subscriber dashboard. Two hundred and six. Net gain of three last week.&lt;/p&gt;

&lt;p&gt;Three.&lt;/p&gt;

&lt;p&gt;But there’s a problem hiding in the numbers.&lt;/p&gt;

&lt;h3 id=&quot;the-churn-problem&quot;&gt;The churn problem&lt;/h3&gt;

&lt;p&gt;Churn is 8% monthly. For every ten new subscribers the team signs up, they lose three or four existing ones.&lt;/p&gt;

&lt;p&gt;Sam walks the team through it on Monday morning. “We added forty-two new subscribers last month. We lost sixteen. Net gain: twenty-six. If we keep losing sixteen a month, we need to sign up sixty a month just to net the growth we need.”&lt;/p&gt;

&lt;p&gt;Maya does the maths on the whiteboard. Sam’s number assumes the sixteen-a-month loss stays flat. It won’t. Churn is a percentage, not a count: 8% of 200 is sixteen, but 8% of 400 is thirty-two, and 8% of 600 is forty-eight. The bigger the base, the more they have to replace before they grow at all. At 8% monthly churn, even doubling their acquisition rate would only get them to around 600 subscribers in six months. They’d never hit 1,000 on acquisition alone; they had to bring churn down too.&lt;/p&gt;

&lt;p&gt;“We need to understand why people leave,” Maya says.&lt;/p&gt;

&lt;p&gt;Tom nods. “Let’s Event Storm it.”&lt;/p&gt;

&lt;h3 id=&quot;the-wrong-tool-for-the-job&quot;&gt;The wrong tool for the job&lt;/h3&gt;

&lt;p&gt;The team books the meeting room, grabs the sticky notes, and starts mapping the cancellation flow. After an hour, the wall has a clean timeline of what happens when someone cancels. The process is well mapped.&lt;/p&gt;

&lt;p&gt;Lee has been quiet, which is unusual. “This is a good map of the cancellation process,” he says. “But you’re mapping &lt;em&gt;what happens when they leave&lt;/em&gt;. Not &lt;em&gt;why they decided to leave&lt;/em&gt;. Those are different questions.”&lt;/p&gt;

&lt;p&gt;He’s correct. The map says nothing about why subscribers decided to cancel in the first place. That motivation lives outside the system, in the customer’s kitchen on a Tuesday evening, in the moment they decide this subscription isn’t worth it any more.&lt;/p&gt;

&lt;p&gt;Tom is frustrated. “So we wasted an hour?”&lt;/p&gt;

&lt;p&gt;“Not wasted. You now have a clean map of the cancellation flow, which you’ll need when you build retention features. But you need a different lens for the &lt;em&gt;why&lt;/em&gt;.”&lt;/p&gt;

&lt;h3 id=&quot;jobs-to-be-done&quot;&gt;Jobs to Be Done&lt;/h3&gt;

&lt;p&gt;Lee draws a simple diagram on the whiteboard. A stick figure, an arrow, and a box labelled “Greenbox.”&lt;/p&gt;

&lt;p&gt;“Clayton Christensen’s framework. The core idea: customers don’t buy products. They &lt;em&gt;hire&lt;/em&gt; them to do a job in their life. Your product isn’t competing with other produce boxes; it’s competing with whatever else the customer could hire to do the same job.”&lt;/p&gt;

&lt;p&gt;“Isn’t the job obvious?” Priya says. “They want fresh local vegetables.”&lt;/p&gt;

&lt;p&gt;“Maybe. But if that were the job, they could go to a farmers’ market. Or join a food co-op. What does Greenbox do that those alternatives don’t?”&lt;/p&gt;

&lt;p&gt;Nobody answers immediately. It’s a harder question than it sounds.&lt;/p&gt;

&lt;p&gt;Lee tells them about Christensen’s milkshake study: a fast-food chain that couldn’t sell more milkshakes until they watched what actually happened at the counter. Half the milkshakes were sold before 8am, to commuters. The job wasn’t “enjoy a delicious milkshake.” The job was “make my commute less tedious.” Once they understood that, they made the milkshake thicker and added fruit. Sales went up 40%.&lt;/p&gt;

&lt;p&gt;“Greenbox isn’t competing with other produce boxes. It’s competing with whatever else your customers could do to solve the same problem in their lives.”&lt;/p&gt;

&lt;h3 id=&quot;talking-to-actual-humans&quot;&gt;Talking to actual humans&lt;/h3&gt;

&lt;p&gt;Lee suggests interviews. Not surveys, not analytics. Actual conversations with actual people.&lt;/p&gt;

&lt;p&gt;“Three groups. Five active subscribers, five who cancelled, five who considered subscribing but didn’t. Thirty minutes each. The hard part isn’t the time; it’s asking the correct questions. And the first rule, the one everyone breaks: don’t defend. Whatever they say about the product, don’t explain, don’t correct, don’t apologise mid-sentence. Just listen and ask the next question. The moment you start defending, the conversation closes.”&lt;/p&gt;

&lt;p&gt;The other rules: Don’t ask “why do you subscribe?”: people will rationalise. Ask about the timeline: “Walk me through the moment you decided to sign up.” Don’t ask “what features would you like?”: people will invent features they’d never use. Ask about struggles: “Tell me about the last time you were frustrated with dinner.” Listen for the &lt;em&gt;switch&lt;/em&gt;: the moment someone moved from their old solution to Greenbox.&lt;/p&gt;

&lt;p&gt;Maya records each interview on her phone. They use an LLM to transcribe the recordings and identify recurring themes across transcripts, faster than a human reader because it can hold five long conversations in context simultaneously. But Maya reads every transcript herself.&lt;/p&gt;

&lt;p&gt;The interviews are harder than anyone expected. The first two feel stilted. Maya keeps asking leading questions. By the third interview, things go properly sideways.&lt;/p&gt;

&lt;p&gt;His name is Greg. He cancelled six weeks ago. He arrives at the cafe ten minutes late, already irritated.&lt;/p&gt;

&lt;p&gt;“Walk me through the moment you decided to sign up.”&lt;/p&gt;

&lt;p&gt;“I’ll tell you what was happening. My wife found you on Instagram and signed us up without asking me. Then I was the one dealing with the box every week.”&lt;/p&gt;

&lt;p&gt;“And what was the experience like?”&lt;/p&gt;

&lt;p&gt;“Terrible. You sent me beetroot three weeks running. Three weeks. I told your support team after the second time. The third week I opened the box and there it was again. Purple. Staring at me.”&lt;/p&gt;

&lt;p&gt;Maya feels heat rise in her neck. “We track all dietary preferences and, “&lt;/p&gt;

&lt;p&gt;“No you don’t.” Greg puts down his coffee. “Or if you do, your system is broken. I sent two emails. Nobody responded to the second one.”&lt;/p&gt;

&lt;p&gt;“I’m sorry about that. We’ve improved our, “&lt;/p&gt;

&lt;p&gt;“I’m not here for an apology. You asked to talk. I’m talking. You want to know why I left? I spent more money on your box than I would have at Hartland Group and I got ingredients I didn’t want that nobody helped me cook. I switched to Freshly. Seven dollars cheaper and the delivery tracking is better.”&lt;/p&gt;

&lt;p&gt;Maya blinks. “Freshly?”&lt;/p&gt;

&lt;p&gt;“Yeah. The Sydney mob. They launched in Perth last month. The produce isn’t as good but at least I know what I’m getting.”&lt;/p&gt;

&lt;p&gt;Maya writes down “Freshly” on her notepad and underlines it twice.&lt;/p&gt;

&lt;p&gt;“Look, I could tell you cared. The little notes about which farm the carrots came from, that was nice. But nice doesn’t matter when I’m standing in my kitchen at six o’clock with a kohlrabi and no bloody idea what to do with it.”&lt;/p&gt;

&lt;p&gt;The interview ends after twenty minutes. Greg shakes her hand and leaves. Maya sits at the cafe table, staring at her notepad. Lee, who’d been observing from the next table, walks over.&lt;/p&gt;

&lt;p&gt;“That was rough.”&lt;/p&gt;

&lt;p&gt;“He was rude.”&lt;/p&gt;

&lt;p&gt;“He was honest. And you broke the first rule; you got defensive. The moment he said the system was broken, you stopped listening and started defending.”&lt;/p&gt;

&lt;p&gt;“Because what he said wasn’t true. We do track preferences.”&lt;/p&gt;

&lt;p&gt;“Do you track &lt;em&gt;his&lt;/em&gt; preference? Did anyone action his emails?”&lt;/p&gt;

&lt;p&gt;Maya opens her laptop and searches the support inbox. Greg’s first email: Sam had responded with a template. The second email, four days later, has no reply. It sits unread between forty other messages.&lt;/p&gt;

&lt;p&gt;“We missed it,” Maya says quietly.&lt;/p&gt;

&lt;p&gt;“That was the most useful twenty minutes of the whole batch. Greg gave you a system failure, a competitor name, and the clearest articulation of the core problem anyone’s said yet. ‘Standing in my kitchen at six o’clock with a kohlrabi and no idea what to do with it.’ That’s your answer. And you almost missed it because you were defending instead of listening.”&lt;/p&gt;

&lt;p&gt;Maya nods slowly. She writes down Greg’s kohlrabi line and circles it.&lt;/p&gt;

&lt;p&gt;That evening, she goes home and searches for Freshly. A polished website. A slick app with real-time delivery tracking. $18 per week. An Instagram with sixty thousand followers. A twelve-million-dollar Series A.&lt;/p&gt;

&lt;p&gt;Nadia comes in from a late physio session and finds Maya at the kitchen table, laptop open to Freshly’s website, a glass of wine untouched.&lt;/p&gt;

&lt;p&gt;“What’s that?”&lt;/p&gt;

&lt;p&gt;“Competition. Well-funded competition.”&lt;/p&gt;

&lt;p&gt;Nadia looks at the screen. “Their boxes look nice.”&lt;/p&gt;

&lt;p&gt;“They’re not local. They buy wholesale from the markets.”&lt;/p&gt;

&lt;p&gt;“Does that matter?”&lt;/p&gt;

&lt;p&gt;Maya doesn’t answer. At midnight, Nadia finds her in the kitchen, reorganising the cupboards. Tins arranged by expiry date. Spices alphabetised. The jars of preserved lemons that Maya’s mother sent from Margaret River lined up like soldiers.&lt;/p&gt;

&lt;p&gt;Nadia leans against the doorframe. “You’re doing the cupboard thing.”&lt;/p&gt;

&lt;p&gt;“I’m fine.”&lt;/p&gt;

&lt;p&gt;“You’re alphabetising cumin at midnight. You’re not fine.”&lt;/p&gt;

&lt;p&gt;Maya puts down the jar. “The customers don’t care about local sourcing, Nadia. We interviewed fifteen people. Three of them mentioned local as the main reason they subscribe. I built the whole brand around it. The fifty-kilometre promise, the farm stories, all of it. They don’t care.”&lt;/p&gt;

&lt;p&gt;“They care about something, though?”&lt;/p&gt;

&lt;p&gt;“Convenience. They care about not having to think about dinner. That’s it. That’s the product.”&lt;/p&gt;

&lt;p&gt;“Is that a bad thing?”&lt;/p&gt;

&lt;p&gt;“It’s a different thing. It’s a completely different business than the one I thought I was building.”&lt;/p&gt;

&lt;p&gt;Nadia sits down. “You built the brand around what matters to you. Now you’re finding out what matters to them. Those can both be true.”&lt;/p&gt;

&lt;p&gt;Maya looks at the preserved lemons. Her mother made them last summer, in the kitchen of the small house in Margaret River. The recipe is her grandmother’s, from Taiwan. Three generations of women preserving food with their hands.&lt;/p&gt;

&lt;p&gt;“I know,” Maya says. “I just need a minute.”&lt;/p&gt;

&lt;p&gt;She calls her mum the next morning, before the coastal run.&lt;/p&gt;

&lt;p&gt;“Mum, did it bother Dad that people didn’t care about where their food came from? When you were farming?”&lt;/p&gt;

&lt;p&gt;Her mother laughs. “Your father didn’t farm because people cared about farming. He farmed because people needed to eat. The caring was his. The eating was theirs.”&lt;/p&gt;

&lt;p&gt;Maya stands at the kitchen window watching the sky lighten over Fremantle. Her mother’s words land somewhere deep.&lt;/p&gt;

&lt;p&gt;By the fourth interview, Maya finds her rhythm. She learns to sit with silence, the pauses where the interviewee is actually thinking. Those pauses produce the most honest answers.&lt;/p&gt;

&lt;p&gt;One churned subscriber, a man named Patrick, gives them a fifteen-minute story about his Tuesday evenings that becomes the team’s touchstone. He describes getting home at six, opening the Greenbox, seeing ingredients he doesn’t recognise, googling recipes while his kids argue about homework, giving up, ordering pizza, and then feeling guilty about the $25 box of vegetables wilting on the counter. “I was paying twenty-five dollars a week to feel bad about myself.” That sentence ends up on a sticky note in the office.&lt;/p&gt;

&lt;h3 id=&quot;what-the-interviews-reveal&quot;&gt;What the interviews reveal&lt;/h3&gt;

&lt;p&gt;Three days later, the team has fifteen transcripts and a wall of quotes. The room goes quiet.&lt;/p&gt;

&lt;p&gt;Active subscribers barely mention vegetables. They mention &lt;em&gt;relief&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;“I don’t have to think about what to cook on Tuesday. The box arrives and dinner is decided.”&lt;/p&gt;

&lt;p&gt;“It’s one less thing to worry about. I get home, I open the box, and I know what we’re eating.”&lt;/p&gt;

&lt;p&gt;One active subscriber is Mrs Patterson, the same Mrs Patterson whose beetroot aversion Maya has been carrying in her head since the Example Mapping sessions. She’s 63, lives alone on Stirling Highway, subscribed since the second week of the pilot.&lt;/p&gt;

&lt;p&gt;“I just open the box and trust what’s inside,” she says. “Except when there’s beetroot.” She smiles. “I don’t even know what’s in the box most weeks. I just know I don’t have to think about it.”&lt;/p&gt;

&lt;p&gt;Jas is sitting in on this interview. She’s in the corner with her Moleskine open. When Mrs Patterson says “dinner is decided,” Jas sketches a quick napkin-style drawing: a box opening, a recipe card visible on top, and underneath the words &lt;em&gt;dinner decided&lt;/em&gt;. She underlines it. Then she underlines it again.&lt;/p&gt;

&lt;p&gt;The job isn’t “get fresh local produce”; it’s “eliminate the mental load of deciding what to cook.” The produce is the mechanism; the stress relief is the product.&lt;/p&gt;

&lt;p&gt;Churned subscribers tell a starkly different story.&lt;/p&gt;

&lt;p&gt;“The vegetables were great but I’d open the box and have no idea what to do with half of it.”&lt;/p&gt;

&lt;p&gt;“It actually added stress instead of removing it. I had all these beautiful vegetables and the guilt of not knowing how to use them before they went off.”&lt;/p&gt;

&lt;p&gt;The box didn’t do the &lt;em&gt;job&lt;/em&gt;. The mental load wasn’t reduced; it was relocated. “What should I buy?” became “What on earth do I do with this?”&lt;/p&gt;

&lt;p&gt;Two of the five churned subscribers mentioned Freshly by name. Greg wasn’t the only defection. Louise said: “I tried that Freshly thing. It’s not as nice, but it’s easier.” Easier, not better. &lt;em&gt;Easier.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;People who considered but didn’t subscribe rejected the uncertainty, not the product.&lt;/p&gt;

&lt;p&gt;“I looked at the website and I couldn’t tell what I’d actually get.”&lt;/p&gt;

&lt;p&gt;“I was interested but my partner was sceptical. I couldn’t explain what we’d be getting.”&lt;/p&gt;

&lt;p&gt;One non-subscriber, Clare, put it perfectly: “I’m already drowning in decisions. I didn’t want to add another one. If I’d known exactly what was coming and what I could cook with it, I probably would have signed up.” She was describing the same job from the outside looking in. The marketing communicated the mechanism (“local produce”) without the outcome (“dinner, sorted”).&lt;/p&gt;

&lt;h3 id=&quot;the-insight-that-changes-everything&quot;&gt;The insight that changes everything&lt;/h3&gt;

&lt;p&gt;Maya stares at the quotes on the wall. Priya says it first.&lt;/p&gt;

&lt;p&gt;“We’ve been marketing this as ‘fresh local vegetables.’ But that’s not why people stay. They stay because we solve Tuesday night. And they leave because we &lt;em&gt;don’t&lt;/em&gt; solve Tuesday night; we just make it a different kind of hard.”&lt;/p&gt;

&lt;p&gt;Tom leans forward. “So the next feature isn’t better substitution or more variety. It’s…”&lt;/p&gt;

&lt;p&gt;“Recipe cards,” Jas says. She pulls out the Moleskine and opens it to the napkin sketch. “Simple, fast recipes that use exactly what’s in this week’s box. Open the box, pick a card, cook dinner. No thinking required.”&lt;/p&gt;

&lt;p&gt;The room is energised in a way it hasn’t been for weeks. Not because recipe cards are exciting technology; they’re printed cards in a cardboard box. But they directly serve the job.&lt;/p&gt;

&lt;p&gt;Priya pushes further. “Without them, we’re delivering ingredients. With them, we’re delivering dinner.”&lt;/p&gt;

&lt;p&gt;“Patrick’s kohlrabi problem,” Sam says.&lt;/p&gt;

&lt;p&gt;“Exactly. He didn’t need better kohlrabi. He needed someone to tell him what to do with it in twenty minutes.”&lt;/p&gt;

&lt;p&gt;Maya adds a constraint: “Every recipe has to be doable by someone who considers themselves a bad cook. If Patrick can make it, anyone can.”&lt;/p&gt;

&lt;p&gt;The team isn’t designing a feature; they’re designing around a specific human being they’ve actually talked to. Patrick isn’t a persona on a slide deck; he’s a real person who told them about feeling guilty on a Tuesday evening.&lt;/p&gt;

&lt;p&gt;Tom is quiet for a moment. “I was about to spend three weeks improving the substitution algorithm. But it doesn’t serve the job. A better substitution algorithm doesn’t reduce anyone’s dinner stress.”&lt;/p&gt;

&lt;p&gt;Maya asks the LLM to help draft the first set of recipe cards. She pastes in this week’s box contents and asks for three simple recipes, each under thirty minutes, using only box contents plus basic pantry staples. The LLM produces them in seconds. Jas designs a card layout. Sam sends them to the printer.&lt;/p&gt;

&lt;p&gt;Tom builds a prototype that afternoon: a simple script that takes the week’s box contents, sends them to an LLM with recipe constraints, and produces three formatted recipes. The whole pipeline, from box contents to print-ready cards, takes less than ten minutes per week. Without the LLM, it would require a food writer. With it, Maya reviews and approves the output in fifteen minutes.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-jobs-to-be-done&quot;&gt;When to use Jobs to Be Done&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;When churn is high and you don’t know why. Exit surveys give surface reasons. JTBD interviews give the real reason: the job wasn’t being done.&lt;/li&gt;
  &lt;li&gt;When you’re about to invest in a new feature. Does it serve the job customers are hiring you for? If not, you might be building the wrong thing.&lt;/li&gt;
  &lt;li&gt;When acquisition is hard and you don’t know your message. “Fresh local vegetables” is a product description; “Stop stressing about Tuesday dinner” is a job statement. One converts better.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;when-not-to-use-it&quot;&gt;When not to use it&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;When the problem is operational, not motivational. If subscribers leave because deliveries arrive late, fix logistics. JTBD is for understanding &lt;em&gt;why customers hire and fire your product&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;When you already know the job. If the team has a clear, validated understanding of why customers buy, running more interviews is discovery theatre.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;back-to-greenbox&quot;&gt;Back to Greenbox&lt;/h3&gt;

&lt;p&gt;Two weeks after the recipe cards ship, churn drops from 8% to 5.5%. Three of the five churned subscribers re-subscribe after Sam emails them. Patrick, the man with the kohlrabi guilt, signs back up the same day. Greg does not. Louise does not. Maya checked.&lt;/p&gt;

&lt;p&gt;She also checked Freshly’s website again. They’ve added a Perth delivery zone. Launch date: next month. Twelve million dollars, a slick app, and $18 per week. Maya’s boxes cost $25 and come with a recipe card printed on a twelve-cent piece of cardboard.&lt;/p&gt;

&lt;p&gt;The recipe cards are working. The churn is dropping. The direction is correct.&lt;/p&gt;

&lt;p&gt;She doesn’t tell anyone that she spent twenty minutes on Freshly’s sign-up flow that evening, getting as far as the payment page, just to see what the experience felt like. It was smooth. It was fast. It was everything Greenbox’s sign-up flow isn’t. She closed the tab and went for a run on the coastal track, even though it was dark and Nadia told her the path wasn’t lit.&lt;/p&gt;

&lt;p&gt;The path was fine. The run helped. The knot in her chest loosened by half a turn.&lt;/p&gt;

&lt;p&gt;Next week, the team takes that insight and asks an uncomfortable question: what else do we believe about this business that we haven’t actually validated? That’s &lt;a href=&quot;/writing/assumption-mapping-testing-what-you-believe/&quot;&gt;Assumption Mapping&lt;/a&gt;, and the answer is more than anyone wants to admit.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Choosing an S3 Storage Class for Cold Archives</title>
    <link href="/writing/choosing-an-s3-storage-class-for-cold-archives/"/>
    <updated>2026-04-27T06:00:00+08:00</updated>
    <id>/writing/choosing-an-s3-storage-class-for-cold-archives/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;Solutions Architect Associate&lt;/strong&gt; · SAA-C03 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A financial services company holds 50 TB of regulatory archive data in S3. KYC documents, transaction histories, communications retained under FCA / SEC obligations. The retention period is seven years by regulation. The data is accessed twice a year when external auditors request a sample; the company is given at least 24 hours of notice before each retrieval.&lt;/p&gt;

&lt;p&gt;Today it all lives on S3 Standard. That’s a bill in the neighbourhood of $13,800 a year for storage alone, on data that is touched for two days out of every 365 and required to exist for the other 363.&lt;/p&gt;

&lt;p&gt;The team’s ask is straightforward. Lowest possible per-GB storage cost over seven years; retrieval feasible inside the 24-hour audit window; standard multi-AZ durability (single-AZ classes are non-starters for regulated data); and minimum operational overhead, no per-object monitoring services, no custom tiering logic, set it once and let lifecycle rules do the work.&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;Before touring the storage-class menu, look at what the shape of this workload actually rewards and punishes.&lt;/p&gt;

&lt;p&gt;The dominant factor over seven years is $/GB/month. The data lives for eighty-four months and is read on two of them. Every fraction of a cent on storage compounds through the entire retention window, while retrieval-side costs scale with reads, a figure measured in single events per year. Any class whose marginal cost is “storage is cheap, retrieval is expensive” is structurally well-matched; any class that keeps the per-GB number high to guarantee instant access is paying for latency the workload will never exploit.&lt;/p&gt;

&lt;p&gt;The second factor is retrieval latency and, more specifically, what “fast enough” means. Twenty-four hours is an eternity in software terms. Most S3 classes can return an object in milliseconds; paying the premium for that speed is only sensible if the workload exercises it. Here it doesn’t, so we can move all the way down the cost curve until we hit a class whose retrieval SLA threatens the 24-hour window.&lt;/p&gt;

&lt;p&gt;The third factor is durability and availability. Durability, the probability that AWS loses our data, is eleven nines on every S3 class including single-AZ ones. Availability is where single-AZ classes are dangerous: one AZ outage during an audit is not a story that plays well with a regulator. The scenario explicitly rules out single-AZ, and it’s the correct call for regulated data even when it’s allowed.&lt;/p&gt;

&lt;p&gt;The fourth factor is operational model. Some classes watch objects and re-tier them automatically, with a per-object watchdog fee for the discovery. For a predictable, write-once, read-on-schedule archive, paying a service to discover something we already know is a negative-value feature. The class should be chosen explicitly, not discovered by telemetry.&lt;/p&gt;

&lt;p&gt;The fifth factor is the bill-shock traps that don’t show up on the pricing page. The cold-archive classes charge a minimum chargeable object size (plus a metadata sliver stored at warm-tier rates). For an archive of millions of small files that effect is visible on the invoice as billed-GB far higher than stored-GB. The coldest tiers also have long minimum storage durations and limited or no fast-retrieval options, delete-early and need-it-in-three-hours are both expensive surprises. A realistic cost picture has to price these in, not just the headline $/GB number.&lt;/p&gt;

&lt;p&gt;And the sixth factor is a softer one: predictability of the pattern itself. This isn’t a workload where the access shape is going to drift quarter-to-quarter. Regulators will keep asking for the same shape of sample; the retention clock ticks predictably; deletions happen at year seven not year three. The more confident we are that the pattern won’t change, the safer it is to pick the cheapest class that fits, because the expensive classes aren’t buying us flexibility we need, they’re buying latency we don’t.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Distilling the exploration into filters:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Lowest $/GB/month, storage dominates the seven-year bill.&lt;/li&gt;
  &lt;li&gt;Retrieval ≤ 24 hours, anything faster is overkill; anything slower violates the audit SLA.&lt;/li&gt;
  &lt;li&gt;Multi-AZ durability, single-AZ classes are off the table for regulated data.&lt;/li&gt;
  &lt;li&gt;No per-object monitoring fee, predictable access patterns don’t benefit from a watchdog.&lt;/li&gt;
  &lt;li&gt;Survives the small-object trap, if the archive contains millions of tiny files, a minimum chargeable object size can double-digit-multiply the apparent storage.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-s3-storage-class-landscape&quot;&gt;The S3 storage-class landscape&lt;/h3&gt;

&lt;p&gt;S3 ships eight storage classes. Each optimises a different point on the cost / latency / durability curve.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;S3 Standard. ~$0.023/GB/month. Immediate access, no retrieval fees, no minimums. The correct home for active workloads. For 50 TB over seven years, about $96,600 in storage alone, most of which buys us nothing.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;S3 Intelligent-Tiering. Frequent Access matches Standard; 30 days of no access moves objects to Infrequent Access pricing; 90 days to Archive Instant Access; opt-in tiers extend further. No retrieval fees between automatic tiers. The catch: a monitoring fee of ~$0.0025 per 1,000 objects per month. For a predictable archive, we’re paying the watchdog for information we already have.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;S3 Standard-IA. ~$0.0125/GB/month. 30-day minimum storage. 128 KB minimum chargeable size. Per-GB retrieval fee (~$0.01/GB). For “infrequent but not cold” data read monthly or so, an order of magnitude more expensive than the deepest Glacier tier.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;S3 One Zone-IA. ~$0.01/GB/month. Same minimums as Standard-IA but stored in a single AZ. 20% cheaper than Standard-IA. Non-starter for regulated data.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;S3 Glacier Instant Retrieval. ~$0.004/GB/month. Millisecond retrieval, 90-day minimum, 128 KB floor, per-GB retrieval fee. Designed for archive that &lt;em&gt;must&lt;/em&gt; be returned immediately when asked, quarterly dashboards, medical images, catalogues that surface in a UI. The 24-hour window makes “millisecond” an overpay.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;S3 Glacier Flexible Retrieval. ~$0.0036/GB/month. 90-day minimum. Three retrieval tiers: Expedited (1-5 min, premium fee), Standard (3-5 hours, per-GB fee), and Bulk (5-12 hours, free). Within budget, but Deep Archive is cheaper still and the 24-hour window doesn’t reward the flexibility.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;S3 Glacier Deep Archive. ~$0.00179/GB/month. 180-day minimum. Two retrieval tiers (no Expedited): Standard (12 hours) and Bulk (48 hours, free). Purpose-built for data we’ll touch once or twice a year or never.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;S3 Express One Zone. ~$0.16/GB/month. Sub-millisecond latency, single-AZ, optimised for compute-adjacent high-IOPS workloads. The opposite of an archive class.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Storage class&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Lowest $/GB&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Retrieval ≤ 24 h&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Multi-AZ&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;No per-object monitoring&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;S3 Standard&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Intelligent-Tiering&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;,&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Standard-IA&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;One Zone-IA&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Glacier Instant Retrieval&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Glacier Flexible Retrieval&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;,&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Glacier Deep Archive&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Express One Zone&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;matching-the-workload-to-the-class&quot;&gt;Matching the workload to the class&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 640&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;A 50 terabyte regulatory archive flows down through four gates: multi-AZ, retrieval tolerance, predictable access pattern, and deepest acceptable cost. The workload passes every gate and lands at Glacier Deep Archive at approximately $0.00179 per GB per month with a 12-hour standard retrieval window. Two alternative paths are shown failing earlier gates: One Zone-IA fails multi-AZ, Glacier Instant Retrieval fails on cost because millisecond retrieval is not required.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .anr-bg-main     { fill: rgba(46, 138, 90, 0.08); stroke: rgba(46, 138, 90, 0.55); stroke-width: 2; }
      .anr-bg-alt1     { fill: rgba(170, 70, 70, 0.08); stroke: rgba(170, 70, 70, 0.55); stroke-width: 2; }
      .anr-bg-alt2     { fill: rgba(214, 142, 41, 0.08); stroke: rgba(214, 142, 41, 0.55); stroke-width: 2; }
      .anr-card-main   { fill: #fff; stroke: rgba(46, 138, 90, 0.8); stroke-width: 1.8; }
      .anr-card-alt1   { fill: #fff; stroke: rgba(170, 70, 70, 0.85); stroke-width: 1.8; }
      .anr-card-alt2   { fill: #fff; stroke: rgba(214, 142, 41, 0.85); stroke-width: 1.8; }
      .anr-pick        { fill: rgba(46, 138, 90, 0.12); stroke: rgba(46, 138, 90, 0.9); stroke-width: 2.2; }
      .anr-dead        { fill: rgba(200, 200, 200, 0.15); stroke: rgba(130, 130, 130, 0.8); stroke-width: 1.6; stroke-dasharray: 4 3; }
      .anr-gate        { fill: #fff; stroke: #666; stroke-width: 1.3; stroke-dasharray: 4 3; }
      .anr-col-title   { font-size: 17px; font-weight: 700; fill: #222; }
      .anr-col-sub     { font-size: 12px; fill: #555; }
      .anr-workload    { font-size: 14px; font-weight: 600; fill: #222; }
      .anr-detail      { font-size: 12px; fill: #333; }
      .anr-gate-text   { font-size: 12px; fill: #333; font-style: italic; }
      .anr-pick-label  { font-size: 15px; font-weight: 700; fill: #222; }
      .anr-pick-sub    { font-size: 12px; fill: #333; }
      .anr-pick-cost   { font-size: 22px; font-weight: 700; fill: rgb(36, 108, 70); }
      .anr-fail        { font-size: 13px; font-weight: 700; fill: #a33; }
      .anr-arrow       { fill: none; stroke: #555; stroke-width: 1.8; }
      .anr-arrow-dead  { fill: none; stroke: #a55; stroke-width: 1.6; stroke-dasharray: 3 3; }
    &lt;/style&gt;
    &lt;marker id=&quot;anr-arrowhead&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;anr-arrowhead-dead&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#a55&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;anr-bg-alt1&quot; /&gt;
  &lt;rect x=&quot;380&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;anr-bg-main&quot; /&gt;
  &lt;rect x=&quot;740&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;anr-bg-alt2&quot; /&gt;

  &lt;text x=&quot;190&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;anr-col-title&quot;&gt;One Zone-IA&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;anr-col-sub&quot;&gt;cheaper but single-AZ&lt;/text&gt;

  &lt;text x=&quot;550&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;anr-col-title&quot;&gt;50 TB regulatory archive&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;anr-col-sub&quot;&gt;7 years, 2 reads/year, 24h notice&lt;/text&gt;

  &lt;text x=&quot;910&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;anr-col-title&quot;&gt;Glacier Instant Retrieval&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;anr-col-sub&quot;&gt;millisecond retrieval, ~2.2× cost&lt;/text&gt;

  &lt;rect x=&quot;50&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;anr-card-alt1&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;anr-workload&quot;&gt;Stored in 1 AZ&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;~$0.01/GB/month&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;11 nines durability, 99.5% availability&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;one AZ outage = audit miss&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;anr-card-main&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;anr-workload&quot;&gt;Write-once, read-rarely&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;regulated: multi-AZ required&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;pattern predictable for 7 years&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;24h notice before every read&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;anr-card-alt2&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;anr-workload&quot;&gt;Milliseconds on demand&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;~$0.004/GB/month&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;128 KB + per-GB retrieval fee&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;anr-detail&quot;&gt;latency we don&apos;t need&lt;/text&gt;

  &lt;path d=&quot;M190,190 L190,220&quot; class=&quot;anr-arrow&quot; marker-end=&quot;url(#anr-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M550,190 L550,220&quot; class=&quot;anr-arrow&quot; marker-end=&quot;url(#anr-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,190 L910,220&quot; class=&quot;anr-arrow&quot; marker-end=&quot;url(#anr-arrowhead)&quot; /&gt;

  &lt;rect x=&quot;50&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;anr-gate&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;anr-gate-text&quot;&gt;Multi-AZ? no — out&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;anr-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;anr-gate-text&quot;&gt;Multi-AZ durability? yes&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;anr-gate&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;anr-gate-text&quot;&gt;Multi-AZ? yes&lt;/text&gt;

  &lt;path d=&quot;M190,264 L190,470&quot; class=&quot;anr-arrow-dead&quot; marker-end=&quot;url(#anr-arrowhead-dead)&quot; /&gt;
  &lt;path d=&quot;M550,264 L550,294&quot; class=&quot;anr-arrow&quot; marker-end=&quot;url(#anr-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,264 L910,294&quot; class=&quot;anr-arrow&quot; marker-end=&quot;url(#anr-arrowhead)&quot; /&gt;

  &lt;rect x=&quot;410&quot; y=&quot;294&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;anr-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;321&quot; text-anchor=&quot;middle&quot; class=&quot;anr-gate-text&quot;&gt;Retrieval ≤ 24h? yes (12h std)&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;294&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;anr-gate&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;321&quot; text-anchor=&quot;middle&quot; class=&quot;anr-gate-text&quot;&gt;Retrieval ms (we don&apos;t need it)&lt;/text&gt;

  &lt;path d=&quot;M550,338 L550,368&quot; class=&quot;anr-arrow&quot; marker-end=&quot;url(#anr-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,338 L910,368&quot; class=&quot;anr-arrow&quot; marker-end=&quot;url(#anr-arrowhead)&quot; /&gt;

  &lt;rect x=&quot;410&quot; y=&quot;368&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;anr-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;395&quot; text-anchor=&quot;middle&quot; class=&quot;anr-gate-text&quot;&gt;Predictable? yes — skip watchdog&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;368&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;anr-gate&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;395&quot; text-anchor=&quot;middle&quot; class=&quot;anr-gate-text&quot;&gt;Deepest class that still fits? no&lt;/text&gt;

  &lt;path d=&quot;M550,412 L550,442&quot; class=&quot;anr-arrow&quot; marker-end=&quot;url(#anr-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,412 L910,470&quot; class=&quot;anr-arrow-dead&quot; marker-end=&quot;url(#anr-arrowhead-dead)&quot; /&gt;

  &lt;rect x=&quot;50&quot; y=&quot;442&quot; width=&quot;280&quot; height=&quot;158&quot; rx=&quot;8&quot; class=&quot;anr-dead&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;470&quot; text-anchor=&quot;middle&quot; class=&quot;anr-fail&quot;&gt;Fails multi-AZ&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;494&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;cheapest per GB&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;514&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;not durable enough for regulators&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;538&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;one AZ outage = one missed audit&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;wrong trade on the availability axis&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;442&quot; width=&quot;280&quot; height=&quot;158&quot; rx=&quot;8&quot; class=&quot;anr-pick&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;472&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-label&quot;&gt;Glacier Deep Archive&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;498&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-cost&quot;&gt;$0.00179 / GB / mo&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;522&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;12h standard retrieval&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;542&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;180-day minimum (irrelevant at 7 yrs)&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;128 KB floor — pack small files first&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;582&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;direct PUT or Days:0 lifecycle&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;442&quot; width=&quot;280&quot; height=&quot;158&quot; rx=&quot;8&quot; class=&quot;anr-dead&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;470&quot; text-anchor=&quot;middle&quot; class=&quot;anr-fail&quot;&gt;Overpays for latency&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;494&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;~$0.004/GB/month&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;514&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;~2.2× more than Deep Archive&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;538&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;millisecond retrieval unused&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;anr-pick-sub&quot;&gt;correct class for UIs, not audits&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Slide down the cost axis as far as the retrieval SLA allows. Deep Archive is the first class cheaper than Standard-IA whose worst-case (12 h) fits inside 24 h with the correct tier selected.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;glacier-deep-archive-in-depth&quot;&gt;Glacier Deep Archive, in depth&lt;/h3&gt;

&lt;p&gt;What makes Deep Archive cheap is what makes it slow. Objects aren’t on warm disk, they’re on tiered media optimised for sequential access, and a retrieval is a job AWS schedules against tape-class hardware. The 12-hour Standard retrieval window is the SLA AWS commits to; in practice many retrievals come back faster, but that’s not something to depend on.&lt;/p&gt;

&lt;p&gt;The retrieval tiers. Standard costs roughly $0.02/GB and completes within 12 hours. Bulk is free and completes within 48 hours. There is no Expedited tier for Deep Archive, that’s exclusive to Flexible Retrieval. Anything faster than 12 hours means picking a different class.&lt;/p&gt;

&lt;p&gt;The restore shape. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RestoreObject&lt;/code&gt; doesn’t move the Deep Archive object permanently. It creates a temporary copy at S3 Standard rates for a number of days we specify (1-365). During that window the object is readable like any Standard object; when the window closes, the copy disappears and the Deep Archive object remains. The bill includes both the base storage and the temporary copy for the restore duration.&lt;/p&gt;

&lt;p&gt;The 180-day minimum. Deleting before 180 days incurs a charge equal to the remaining days at the Deep Archive rate. For seven-year data this is irrelevant. For a workload that might delete at 60 days, Deep Archive is a trap.&lt;/p&gt;

&lt;p&gt;The 128 KB minimum chargeable size, the most common trap. Glacier classes charge 128 KB per object as a floor, plus ~40 KB of metadata stored at S3 Standard rates so the object stays searchable. For an archive of millions of small files, 50 million 5 KB notification emails, say, the 128 KB floor inflates the apparent storage from 250 GB to 6.4 TB &lt;em&gt;for billing purposes&lt;/em&gt;. The mitigation is to pack small objects into larger archives (zip, tar, or a custom format) before upload. A million 5 KB emails packed into 100 MB archives reduces the chargeable size by orders of magnitude.&lt;/p&gt;

&lt;h3 id=&quot;a-worked-example-one-year-of-bill-shape&quot;&gt;A worked example: one year of bill shape&lt;/h3&gt;

&lt;p&gt;50 TB = 50,000 GB. Assume objects are packed large enough that the 128 KB minimum doesn’t dominate.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Storage at Deep Archive
  50,000 GB × $0.00179 × 12                        =  $1,074

Retrieval (twice a year, 50 TB returned per audit)
  Standard retrieval: 50,000 × $0.02 × 2           =  $2,000

Restore staging (14-day window each audit)
  50,000 × $0.023 × (14/30) × 2                    =  $1,073

Total annual                                          $4,147

Comparable S3 Standard bill:
  50,000 × $0.023 × 12                             =  $13,800
Saving                                                ~70%
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Over seven years that’s roughly a $67,000 difference, paid for by tolerating a 12-hour retrieval window that the 24-hour audit SLA covers with a full day of headroom. If the auditor would accept a 48-hour window, Bulk retrieval drops retrieval cost to zero and the annual bill lands near $2,150, but the scenario’s 24-hour notice doesn’t tolerate Bulk’s worst case.&lt;/p&gt;

&lt;h3 id=&quot;getting-data-into-deep-archive&quot;&gt;Getting data into Deep Archive&lt;/h3&gt;

&lt;p&gt;Three routes in, with different cost shapes.&lt;/p&gt;

&lt;p&gt;Direct PUT to Deep Archive. Set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x-amz-storage-class: DEEP_ARCHIVE&lt;/code&gt; on the upload request. The object lands directly in Deep Archive, no Standard hop, no transition fee. Requires the uploader to know about the storage class. Best when the data is &lt;em&gt;born&lt;/em&gt; archival.&lt;/p&gt;

&lt;p&gt;Lifecycle transition from Standard. Upload to Standard, then a lifecycle rule moves objects to Deep Archive after a configured number of days. Transition costs roughly $0.05 per 1,000 objects. For 50 million objects that’s $2,500 one-off, real money but amortised.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;LifecycleConfiguration&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;Rules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;Id&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ArchiveImmediately&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Enabled&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;Filter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;Prefix&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;kyc/&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;Transitions&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;Days&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;StorageClass&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DEEP_ARCHIVE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Days: 0&lt;/code&gt; transition fires within the next lifecycle evaluation cycle (once per day), so it’s not instant, there’s typically a one-day window where the object sits in Standard first. For workloads where that day matters, prefer direct PUT.&lt;/p&gt;

&lt;p&gt;Intelligent-Tiering with Deep Archive Access opt-in. Let S3 discover the access pattern; untouched objects eventually reach Deep Archive Access after 180+ days of inactivity. Plus the monitoring fee per object. For a workload where the pattern is already known, overkill.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Eight S3 storage classes exist, each optimising a different point on cost / latency / durability / AZ-count. For a seven-year compliance archive touched twice a year, Deep Archive is the one designed for the shape.&lt;/li&gt;
  &lt;li&gt;The 128 KB minimum chargeable object size on IA and Glacier classes is the most common bill-shock trap. Pack small files into larger archives before upload.&lt;/li&gt;
  &lt;li&gt;Glacier Flexible Retrieval has Expedited (1-5 min); Deep Archive doesn’t. If anything faster than 12 hours is required, pick Flexible, not Deep.&lt;/li&gt;
  &lt;li&gt;Minimum storage durations matter. 30 days on IA, 90 on Glacier Instant / Flexible, 180 on Deep Archive. Early deletion charges the remaining days.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RestoreObject&lt;/code&gt; creates a temporary S3 Standard copy for the duration specified. The underlying Glacier object is unchanged; the bill includes both for the restore window.&lt;/li&gt;
  &lt;li&gt;Intelligent-Tiering’s monitoring fee is per object, not per GB. Predictable-access archives don’t benefit from a watchdog, and billions of small objects make the watchdog expensive.&lt;/li&gt;
  &lt;li&gt;Lifecycle transitions have per-1,000-object costs that matter at scale. Direct PUT skips them.&lt;/li&gt;
  &lt;li&gt;Single-AZ classes share the 11-nines durability but drop availability. For regulated data the availability gap is disqualifying even when durability is fine on paper.&lt;/li&gt;
&lt;/ol&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Other Transformers</title>
    <link href="/writing/the-other-transformers/"/>
    <updated>2026-04-25T06:00:00+08:00</updated>
    <id>/writing/the-other-transformers/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;You have a backlog of 80,000 support tickets and you need to tag each one with one of fourteen categories. Someone suggests using an &lt;label for=&quot;sn-writing-the-other-transformers-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt;. You write the prompt, you wire up the API, you run the numbers, and the bill comes back at $1,400 just for the categorisation. You haven’t even started doing anything with the categories yet.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;There’s a better tool for this. It’s also a &lt;label for=&quot;sn-writing-the-other-transformers-transformer&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-transformer-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;transformer&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-transformer&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-transformer-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Transformer&lt;/span&gt;The neural network architecture that underpins modern LLMs – stacks of self-attention layers that let every token look at every other token in the context.
&lt;/span&gt;. It’s just not the one everyone talks about.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;/writing/to-llms-and-beyond/&quot;&gt;To LLMs… and Beyond!&lt;/a&gt; we treated “transformer” as one thing, the engine behind Claude, GPT, Llama. That was useful for a tour of the field, but it elided a real distinction. The transformer architecture comes in three structural shapes, and only one of them is the autoregressive text-generator that the AI conversation has fixated on.&lt;/p&gt;

&lt;p&gt;The other two are still in production at every serious AI shop. They’re cheaper, faster, and often more accurate for the jobs they were designed to do. This post is about when to reach for them instead.&lt;/p&gt;

&lt;h3 id=&quot;three-shapes-from-one-paper&quot;&gt;Three shapes from one paper&lt;/h3&gt;

&lt;p&gt;The 2017 paper &lt;em&gt;&lt;label for=&quot;sn-writing-the-other-transformers-attention&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-attention-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Attention&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-attention&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-attention-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Attention&lt;/span&gt;The mechanism inside a transformer that lets each token weigh how much every other token in the context matters to it.
&lt;/span&gt; Is All You Need&lt;/em&gt; introduced the transformer with a specific job in mind: machine translation. English in, French out. The architecture had two halves, an encoder that read the English sentence and produced an internal representation of its meaning, and a decoder that consumed that representation and produced French one &lt;label for=&quot;sn-writing-the-other-transformers-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;token&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt; at a time.&lt;/p&gt;

&lt;p&gt;Almost immediately, researchers noticed you could use the halves separately.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Encoder-only models keep just the encoder. They take text in and produce a representation, a &lt;label for=&quot;sn-writing-the-other-transformers-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt;, a label, a span. They never generate text. BERT (2018) is the headline example.&lt;/li&gt;
  &lt;li&gt;Decoder-only models keep just the decoder. They take text in and produce more text, one token at a time. GPT, Claude, and Llama are all this shape.&lt;/li&gt;
  &lt;li&gt;Encoder-decoder models keep both halves. They take text in, encode it, and decode something different out. T5 and BART are the headline examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shape determines what the &lt;label for=&quot;sn-writing-the-other-transformers-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; is good at. And it determines what it costs.&lt;/p&gt;

&lt;h3 id=&quot;encoder-only-bert-and-friends&quot;&gt;Encoder-only: BERT and friends&lt;/h3&gt;

&lt;p&gt;BERT stands for Bidirectional Encoder Representations from Transformers. The “bidirectional” is the part that matters. A decoder-only model like GPT processes text left-to-right, one token at a time, when it’s predicting the next token, it can only see what came before. An encoder-only model processes the entire sequence at once, and every token can attend to every other token in both directions.&lt;/p&gt;

&lt;p&gt;This makes encoder-only models worse at generating fluent text, in fact, they don’t generate text at all in the usual sense, but better at &lt;em&gt;understanding&lt;/em&gt; it. When BERT looks at the word “bank” in “I sat by the bank of the river,” it can see “river” three tokens later, and that informs its representation of “bank.” A left-to-right model has to commit to a meaning before it has all the evidence.&lt;/p&gt;

&lt;p&gt;What encoder-only models actually output is a sequence of vectors, one per input token. You can use those vectors directly (as &lt;label for=&quot;sn-writing-the-other-transformers-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embeddings&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; for similarity search) or you can stick a tiny classification head on top (a single linear layer that maps a vector to a label) and get a classifier.&lt;/p&gt;

&lt;p&gt;The big BERT-family models you’ll encounter:&lt;/p&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Model&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Made by&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Notable for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;BERT&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Google, 2018&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;The original. Set state of the art on a dozen benchmarks overnight.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;RoBERTa&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Meta, 2019&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;BERT trained better, more data, longer, with the masking strategy fixed. Usually beats BERT.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;DeBERTa&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Microsoft, 2020-2021&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Disentangled attention. Strong on classification benchmarks, often the default for new projects.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;DistilBERT&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Hugging Face, 2019&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A 40%-smaller BERT that&apos;s 60% faster and keeps 97% of the accuracy. The pragmatic choice.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;ModernBERT&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Answer.AI, 2024&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;BERT with the last six years of architectural improvements bolted on. Long context, fast inference.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;These are all small. BERT-base has 110 million parameters, DistilBERT has 66 million, ModernBERT-large has 395 million. Compare that to a frontier LLM at hundreds of billions. They run on a CPU. They run on your laptop. They run on a Raspberry Pi if you don’t mind waiting.&lt;/p&gt;

&lt;h3 id=&quot;what-encoder-only-models-are-good-at&quot;&gt;What encoder-only models are good at&lt;/h3&gt;

&lt;p&gt;Anything where the answer is shorter than the input. Specifically:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Classification. Sentiment, intent, topic, language detection, content moderation, spam, urgency triage. One label out per input.&lt;/li&gt;
  &lt;li&gt;Multi-label classification. Tagging a document with several categories at once.&lt;/li&gt;
  &lt;li&gt;Named entity recognition (NER). Picking out people, places, organisations, dates from text. One label per token.&lt;/li&gt;
  &lt;li&gt;Span extraction. “Find the answer to this question inside this document.” The model points at the start and end positions of the span. SQuAD-style question answering.&lt;/li&gt;
  &lt;li&gt;Sentence embeddings. Producing a fixed-size vector that represents the meaning of a piece of text. The foundation of semantic search and &lt;label for=&quot;sn-writing-the-other-transformers-rag&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-rag-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;RAG&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-rag&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-rag-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RAG&lt;/span&gt;A pattern where you retrieve relevant documents at query time and stuff them into the prompt so the model can ground its answer on them.
&lt;/span&gt;.&lt;/li&gt;
  &lt;li&gt;Pairwise classification. “Are these two sentences saying the same thing?” “Does sentence A entail sentence B?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For all of these, an LLM will &lt;em&gt;also&lt;/em&gt; work. It will just cost roughly a hundred times more, take roughly ten times longer, and, in many cases, be less accurate.&lt;/p&gt;

&lt;h3 id=&quot;why-an-llm-is-often-worse-not-just-more-expensive&quot;&gt;Why an LLM is often worse, not just more expensive&lt;/h3&gt;

&lt;p&gt;Counterintuitive but real: a fine-tuned BERT often outperforms a frontier LLM at classification tasks the BERT was specifically trained for.&lt;/p&gt;

&lt;p&gt;The reason is task alignment. An LLM is trained to predict the next token across the entirety of internet text. A fine-tuned classifier is trained on labelled examples of exactly the task you care about, ten thousand support tickets with their correct categories, say. The LLM has read the universe and has a vague sense of what “billing” means; the classifier has stared at your specific definition of “billing” for a thousand epochs.&lt;/p&gt;

&lt;p&gt;The LLM also has to &lt;em&gt;speak&lt;/em&gt; its answer, which introduces failure modes the classifier doesn’t have. Will it return “billing” or “Billing” or “billing/payments” or a polite refusal because the ticket mentions a credit card? The classifier returns one of fourteen integers. Always.&lt;/p&gt;

&lt;p&gt;There’s an obvious counter: what if you don’t have ten thousand labelled examples? Genuine constraint, and where LLMs shine, zero-shot or few-shot classification with a prompt is a real superpower when you’re starting from nothing. But the moment you’ve labelled enough data to &lt;label for=&quot;sn-writing-the-other-transformers-fine-tuning&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-other-transformers-fine-tuning-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;fine-tune&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-other-transformers-fine-tuning&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-other-transformers-fine-tuning-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Fine-tuning&lt;/span&gt;Continuing to train an already-trained model on a smaller dataset to adapt its behaviour.
&lt;/span&gt; a small encoder, the cost-quality curve usually flips.&lt;/p&gt;

&lt;h3 id=&quot;encoder-decoder-t5-bart-flan&quot;&gt;Encoder-decoder: T5, BART, FLAN&lt;/h3&gt;

&lt;p&gt;The encoder-decoder shape is for jobs where the output is structured but isn’t a free-form essay, a transformation of the input rather than a continuation of it.&lt;/p&gt;

&lt;p&gt;The flagship example is Google’s T5 (Text-to-Text Transfer Transformer, 2019), which framed &lt;em&gt;every&lt;/em&gt; NLP task as text-in, text-out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Translation: input “translate English to German: That is good.” → output “Das ist gut.”&lt;/li&gt;
  &lt;li&gt;Summarisation: input “summarize: &amp;lt;article&amp;gt;” → output “&amp;lt;summary&amp;gt;”&lt;/li&gt;
  &lt;li&gt;Classification: input “cola sentence: The course is jumping well.” → output “not acceptable”&lt;/li&gt;
  &lt;li&gt;Question answering: input “question: What is the capital of France? context: …” → output “Paris”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shape is well-suited to anything that has a deterministic-ish target, a translation, a summary, a structured output, a SQL query generated from a natural-language question. The encoder reads the whole input once, builds a rich representation, and the decoder produces the (usually short) output guided by that representation.&lt;/p&gt;

&lt;p&gt;The other notable encoder-decoder family is BART (Meta, 2019), which was trained on a denoising objective, corrupt the input, recover the original, and is particularly strong at summarisation.&lt;/p&gt;

&lt;p&gt;The instruction-tuned descendants. FLAN-T5, T5-XXL, BART-large-CNN, are still common backbones for production summarisation and translation pipelines, especially when you want to fine-tune on your own data.&lt;/p&gt;

&lt;h3 id=&quot;what-encoder-decoder-models-are-good-at&quot;&gt;What encoder-decoder models are good at&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Translation. The original use case, still strong.&lt;/li&gt;
  &lt;li&gt;Summarisation. Extractive (copy spans) or abstractive (rewrite). BART-large-CNN was the production default for years.&lt;/li&gt;
  &lt;li&gt;Structured generation. Text-to-SQL, text-to-JSON, text-to-API-call. The encoder grounds the output in the input.&lt;/li&gt;
  &lt;li&gt;Grammar correction. Input: messy sentence. Output: clean sentence.&lt;/li&gt;
  &lt;li&gt;Question answering with generation. Where the answer isn’t necessarily a span in the document and needs to be paraphrased.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The boundary with decoder-only LLMs has blurred. Modern LLMs do all of the above competently, often better than older T5 models, and the simplicity of “one model for everything” has pulled a lot of work toward the decoder-only side. But for pipelines where you need something small, fast, deterministic, and fine-tuneable, T5-family models still pull their weight.&lt;/p&gt;

&lt;h3 id=&quot;a-decision-table&quot;&gt;A decision table&lt;/h3&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;If your task is…&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Reach for…&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Why not an LLM?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Tag each item with one of N categories&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;DeBERTa or DistilBERT, fine-tuned&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;100x cheaper, often more accurate, no parsing of free-text output&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Find people, places, dates in text&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A BERT-family NER model (e.g. spaCy&apos;s transformer)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Token-level precision, no hallucinated entities&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Embed sentences for semantic search&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A sentence-transformers model (BGE, E5, GTE)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;LLMs don&apos;t natively produce sentence embeddings; encoder models do this as their primary job&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Translate between languages at scale&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A T5- or NLLB-family model, fine-tuned if needed&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Per-token cost matters at translation volumes; specialised models still lead&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Convert natural language to SQL or JSON&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A code-fine-tuned T5, or an LLM if accuracy matters more than cost&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Mixed. LLMs win on hard cases, encoder-decoders win on cost at scale&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Decide if a comment is toxic&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A fine-tuned encoder classifier (e.g. Detoxify)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Real-time moderation needs millisecond latency, not 800ms API round-trips&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Have a free-form conversation&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An LLM&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Encoder models cannot generate fluent multi-turn text&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Reason through a multi-step problem&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An LLM, ideally a reasoning model&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Encoder models have no chain-of-thought; they produce one answer in one pass&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-pragmatic-stack&quot;&gt;The pragmatic stack&lt;/h3&gt;

&lt;p&gt;In production AI systems, you’ll often see encoder, encoder-decoder, and decoder-only models working together rather than competing.&lt;/p&gt;

&lt;p&gt;A typical retrieval-augmented chat application:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Bi-encoder (BERT-family) embeds the user’s query and finds the top 100 candidate documents from the vector database. Cheap, parallel, fast.&lt;/li&gt;
  &lt;li&gt;Cross-encoder (BERT-family) re-ranks those 100 down to the top 5 by reading each query-document pair carefully. We’ll cover this in the next post.&lt;/li&gt;
  &lt;li&gt;Decoder-only LLM consumes the top 5 documents alongside the query and writes a fluent answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each stage uses the right tool for its job. The encoder does the cheap, high-throughput retrieval and ranking. The LLM does the expensive, low-throughput generation, but only after the encoder has narrowed the search space by three orders of magnitude.&lt;/p&gt;

&lt;p&gt;This is the pattern that matters. It’s not “LLM vs BERT.” It’s “use BERT to make the LLM step efficient enough to be worth doing.”&lt;/p&gt;

&lt;h3 id=&quot;where-to-find-them&quot;&gt;Where to find them&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Hugging Face is the de facto registry. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bert-base-uncased&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;roberta-large&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;microsoft/deberta-v3-large&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;distilbert-base-uncased&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;answerdotai/ModernBERT-large&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t5-base&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;facebook/bart-large-cnn&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;google/flan-t5-xl&lt;/code&gt;, all available, all free to download.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sentence-transformers&lt;/code&gt; is the library for using BERT-family models as embedding models. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all-MiniLM-L6-v2&lt;/code&gt; is the gateway drug, 22 million parameters, runs on a phone, and is the correct starting point for 80% of semantic-search projects.&lt;/li&gt;
  &lt;li&gt;spaCy wraps fine-tuned encoder models for NER, POS tagging, and similar pipelines, with an API designed for production use rather than research.&lt;/li&gt;
  &lt;li&gt;Cohere, OpenAI, Voyage sell hosted embedding APIs if you want the model without the operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The word “transformer” hides three quite different machines under one name. The decoder-only shape is what everyone means when they say LLM, and it’s the one that has to speak its answer aloud, one token at a time. That mouth is what makes it generative, and it’s also what makes the bill arrive. The encoder-only shape never opens its mouth: it reads, it understands, it points at a label or a span or hands back a vector. The encoder-decoder shape sits in between, reading once and producing a short, structured response.&lt;/p&gt;

&lt;p&gt;If your job has a stable target, one of fourteen categories, a span in a document, an embedding for retrieval, a SQL query, there’s almost always a smaller, older, cheaper model that does it better than a frontier LLM, especially once you have labelled data to fine-tune on. The serious AI shops know this. Their production stacks don’t pick between transformer shapes; they chain them. The encoder narrows the search space by three orders of magnitude so the decoder’s expensive generation step is worth paying for. “Should I use an LLM?” is the wrong framing; the useful framing is where in the pipeline an LLM actually earns its cost.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: User Story Mapping</title>
    <link href="/writing/the-workshop-user-story-mapping/"/>
    <updated>2026-04-24T06:00:00+08:00</updated>
    <id>/writing/the-workshop-user-story-mapping/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;A flat backlog hides the journey. User Story Mapping unrolls it across a wall and slices it into a thinnest-honest first release. Worked example: &lt;a href=&quot;/writing/user-story-mapping-seeing-the-whole/&quot;&gt;Seeing the Whole&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;user-story-mapping&quot;&gt;User Story Mapping&lt;/h3&gt;

&lt;p&gt;User Story Mapping lays out the full user experience as a left-to-right narrative, then slices it horizontally into releases, so the team can see the whole journey and commit to the thinnest honest version of it first. Often just called story mapping. Sometimes confused with customer journey mapping (journey mapping is research-led and emotional; story mapping is build-led and functional) and with flat backlogs (a backlog is a list; a story map is a grid). Invented and named by Jeff Patton in 2005, published as a book in 2014 that is still the canonical reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; a facilitator, the product owner, two or three developers, a designer, and someone who talks to real users (support, sales, ops). Five to eight people, two to three hours.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; a backbone of six to twelve activities with task columns beneath, and at least one release line marking the walking skeleton, an end-to-end-but-thin first slice where every activity has at least one task above the line.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; a new product or major feature area where the backlog has grown long, an MVP argument is going in circles, or the team has different mental models of the end-to-end experience. Not for a single well-understood story (use &lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt;), purely technical work with no user-facing narrative, or strategy-level scope decisions (run &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt; first).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first slice is what Cockburn (and Patton, who adopted the name) call a walking skeleton: Alistair Cockburn’s term for an end-to-end-but-thin first slice of the system, alive but ugly. End-to-end, thin, ugly, but alive. Every activity has at least one task in release 1; the journey is whole even when the polish isn’t.&lt;/p&gt;

&lt;h3 id=&quot;whats-it-for&quot;&gt;What’s It For&lt;/h3&gt;

&lt;p&gt;A team has a flat backlog of eighty-seven stories. They’ve been working through it for six weeks. Last week they shipped a beautiful payment form. This week they’re building subscription upgrade logic. Next week they’re building referrals. The product owner writes up a release announcement and notices, with a growing sense of unease, that nothing the team has built actually lets a new visitor sign up, choose a box, and receive their first delivery. The payment form doesn’t connect to anything yet. The upgrade logic assumes a subscriber who can’t yet exist. The referrals are for a product that has no users to refer. Every story delivered was a good story. The sum of the stories is not a product.&lt;/p&gt;

&lt;p&gt;This is the flat-backlog failure mode. A list tells you what’s next; it doesn’t tell you what fits together. Priority orders stories by perceived value but loses the journey shape. Teams optimise each story locally and discover, six weeks in, that they’ve been building disconnected parts.&lt;/p&gt;

&lt;p&gt;User Story Mapping exists to put the journey back. The wall is the shape. The vertical axis is priority; the horizontal axis is the user walking through the product end to end. A release is a horizontal line across the map, and the question the line forces is: &lt;em&gt;what is the thinnest version of this journey that still works as a journey?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Reach for it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You’re planning a new product or a major new feature area&lt;/li&gt;
  &lt;li&gt;The backlog has grown large and nobody can see the big picture&lt;/li&gt;
  &lt;li&gt;You need to define an MVP or a first release and the arguments keep going in circles&lt;/li&gt;
  &lt;li&gt;Different team members have different mental models of what the product does end-to-end&lt;/li&gt;
  &lt;li&gt;You’ve finished Impact Mapping or Event Storming and now need to turn the insights into a buildable plan&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-its-not-for&quot;&gt;What It’s Not For&lt;/h3&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You’re mapping a single, well-understood story. Use &lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt; instead.&lt;/li&gt;
  &lt;li&gt;You don’t have a clear user or set of users to map for. Story Mapping is anchored to a persona; without one, the wall has no shape.&lt;/li&gt;
  &lt;li&gt;The work is purely technical with no user-facing narrative. Infrastructure, internal tools, refactoring: those want a different artefact.&lt;/li&gt;
  &lt;li&gt;The scope is so broad that you’re really trying to decide strategy. Run &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt; first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop a session that’s already started if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The team can’t agree which persona to map: you’re mapping two journeys, not one&lt;/li&gt;
  &lt;li&gt;Scope has quadrupled two hours in and the wall is full but incomplete&lt;/li&gt;
  &lt;li&gt;The walk-the-map narration reveals that nobody in the room actually understands the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stopping when the scope isn’t right is not failure. Producing a beautiful map of the wrong journey is.&lt;/p&gt;

&lt;h3 id=&quot;definitions--background&quot;&gt;Definitions &amp;amp; Background&lt;/h3&gt;

&lt;p&gt;The map is the source; the backlog is a flattening of release 1 for execution. When the map and the backlog disagree, the map wins and the backlog gets re-flattened. Treating the backlog as the source, and the map as a once-off artefact, is the failure mode that wrecks teams about six months in.&lt;/p&gt;

&lt;p&gt;Patton’s release-slicing convention is now / next / later: now is the committed walking skeleton, next is the slice you’d build immediately after, later is everything you’re keeping on the wall but explicitly not committing to. The fuzziness of &lt;em&gt;later&lt;/em&gt; is the point; it stops the team pretending later slices are plans.&lt;/p&gt;

&lt;p&gt;The vocabulary of the wall:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Persona: pinned to the left end. The user whose journey is being mapped. One per wall.&lt;/li&gt;
  &lt;li&gt;Backbone: the row of blue activity notes across the top. Six to twelve big chunks of the user’s journey, left to right.&lt;/li&gt;
  &lt;li&gt;User tasks: yellow notes hanging vertically below each activity, ordered top (most essential) to bottom (nice to have).&lt;/li&gt;
  &lt;li&gt;Release lines: horizontal slices across the wall. Above each line is what’s committed for that release; below is later.&lt;/li&gt;
  &lt;li&gt;Walking skeleton: the first release line. End-to-end, thin, ugly, but alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;inputs&quot;&gt;Inputs&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;One clear user persona written on a card and pinned to the left of the wall. If you don’t have one, spend ten minutes writing one before any other note goes up.&lt;/li&gt;
  &lt;li&gt;A rough agreed scope for this map: the full product, or one journey within it. Write the end-point on a card and stick it at the right end of the wall. That’s the boundary.&lt;/li&gt;
  &lt;li&gt;A long wall, sticky notes in at least two colours (blue for the backbone, yellow for tasks), tape for the release lines, and a room that can accommodate people standing and moving for hours.&lt;/li&gt;
  &lt;li&gt;Any existing research you can reference without putting it on the wall: user interviews, support tickets, analytics. Bring it as evidence, not as voices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t yet know &lt;em&gt;what&lt;/em&gt; outcomes you’re chasing, run &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt; first. If you don’t yet understand the system the user is moving through, run &lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming&lt;/a&gt; first. Story Mapping turns those upstream insights into a buildable plan; it doesn’t generate them.&lt;/p&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;What lands on the wall at the end:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A backbone of six to twelve activities describing the user’s journey end-to-end.&lt;/li&gt;
  &lt;li&gt;User tasks stacked vertically under each activity, ordered by importance.&lt;/li&gt;
  &lt;li&gt;Release lines: at least one (the walking skeleton), often three (now / next / later), making the trade-offs explicit.&lt;/li&gt;
  &lt;li&gt;A defensible MVP because the journey above the first line is visibly complete.&lt;/li&gt;
  &lt;li&gt;A backlog organised by both priority (vertical) and journey stage (horizontal), so new work has a place to go.&lt;/li&gt;
  &lt;li&gt;The “what about…” questions caught on the wall rather than mid-sprint.&lt;/li&gt;
  &lt;li&gt;An artefact that works as a communication tool for stakeholders who weren’t in the room.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Photograph the wall before the notes come down: panoramic shots of the full wall with good lighting and enough resolution to read every note, plus close-up shots of each activity section so the detail is preserved even if the panorama isn’t sharp enough.&lt;/p&gt;

&lt;p&gt;These outputs feed straight into:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt;: the release-1 tasks from the wall are the input to Example Mapping. Story Mapping gives you the list of stories; Example Mapping decides whether each one is ready to build.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-sprint-planning/&quot;&gt;Sprint Planning&lt;/a&gt;: once the release-1 slice exists and Example Mapping has run on the top stories, Sprint Planning turns the map into committed sprints.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt;: the release-1 slice is a stack of assumptions about what users need. Assumption Mapping pulls the slice apart before you commit to building it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whos-needed&quot;&gt;Who’s Needed&lt;/h3&gt;

&lt;p&gt;Five to eight people, two to three hours:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Facilitator. Keeps the map growing in the right direction, catches backbone items that are really tasks, and runs the walk-the-map ritual.&lt;/li&gt;
  &lt;li&gt;Product owner. Mandatory. They’re the narrator during the walk-the-map phase: the person who tells the user’s story out loud while everyone else listens for gaps. They also make the final release-slicing call when the team disagrees.&lt;/li&gt;
  &lt;li&gt;Developers. At least two. They’ll ground the map in what’s actually buildable and catch tasks that look small but hide weeks of infrastructure work.&lt;/li&gt;
  &lt;li&gt;Designers. They think in journeys natively. A designer will reshape the backbone halfway through the session in ways a developer or product owner wouldn’t have thought to.&lt;/li&gt;
  &lt;li&gt;People who talk to real users. Support, sales, operations, account managers. They’ll add the unhappy paths the golden-path team forgot: the subscriber whose card declined, the pause that went wrong, the box that arrived damaged.&lt;/li&gt;
  &lt;li&gt;Operations / SRE (Site Reliability Engineering, the operations-and-reliability discipline). For products where operations are part of the user experience (on-call engineers, deployment pipelines, support agents using internal tools) the user being mapped might be &lt;em&gt;them&lt;/em&gt;, and ops is the domain expert. Don’t tuck them in as afterthoughts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fewer than five and you miss perspectives; more than eight and the wall becomes a crowd. If you’re forced above 10, split into two sessions with overlapping attendance and reconcile the maps afterwards.&lt;/p&gt;

&lt;p&gt;Who to leave out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Real end users. Their presence warps what the insiders will say. Interview users separately and bring their words into the room as evidence, not as voices.&lt;/li&gt;
  &lt;li&gt;Senior leaders who will turn the session into a requirements meeting. Story Mapping is discovery; requirements come from it, not into it.&lt;/li&gt;
  &lt;li&gt;Spectators. Anyone “just observing” is absorbing attention without contributing. Either they participate or they read the output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Budget three hours for a first session on a new product. Budget ninety minutes for a map of a single feature area inside an existing product. Do not try to map two different personas on the same wall in the same session; split them.&lt;/p&gt;

&lt;h3 id=&quot;how-to-run-it&quot;&gt;How To Run It&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Duration&lt;/th&gt;
      &lt;th&gt;Notes colour&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Orient, persona, scope&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;“Who is this map for and how far does it go?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Backbone&lt;/td&gt;
      &lt;td&gt;20 min&lt;/td&gt;
      &lt;td&gt;Blue&lt;/td&gt;
      &lt;td&gt;“What does the user do at the highest level?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;User tasks&lt;/td&gt;
      &lt;td&gt;30 min&lt;/td&gt;
      &lt;td&gt;Yellow&lt;/td&gt;
      &lt;td&gt;“What specific things do they do at each step?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Walk the map&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;(review)&lt;/td&gt;
      &lt;td&gt;“Does this journey make sense end-to-end?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Slice releases&lt;/td&gt;
      &lt;td&gt;30 min&lt;/td&gt;
      &lt;td&gt;Tape lines&lt;/td&gt;
      &lt;td&gt;“What’s the thinnest complete journey?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Wrap-up&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;“What’s in release 1?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;~2 hours inside a 2–3 hour block&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 900 470&quot; style=&quot;max-width: 100%; height: auto; display: block; margin: 1.5rem auto;&quot; role=&quot;img&quot; aria-label=&quot;A user story map skeleton. The top row is the backbone, five blue activity notes for a subscriber&apos;s journey: Discover, Sign up, Pick a box, Receive, Manage. Below each activity, vertical columns of yellow task notes. Three horizontal release lines slice the map: Now (the walking skeleton, just enough to make the journey work end-to-end), Next, and Later.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .usm-slice { stroke: #C85A1F; stroke-width: 2; stroke-dasharray: 6 4; fill: none; }
      .usm-slice-label { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 12px; font-weight: 700; fill: #C85A1F; text-transform: uppercase; letter-spacing: 0.05em; }
      .usm-backbone { fill: #b9d4f0; stroke: #1B1916; stroke-width: 1.2; }
      .usm-task { fill: #fff1a1; stroke: #1B1916; stroke-width: 1; }
      .usm-skeleton { fill: #C85A1F; stroke: #1B1916; stroke-width: 1.2; opacity: 0.85; }
      .usm-text { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 11px; fill: #1B1916; text-anchor: middle; }
      .usm-text-skel { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 11px; fill: #ffffff; font-weight: 700; text-anchor: middle; }
      .usm-row-label { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 11px; font-weight: 700; fill: #4a4540; text-transform: uppercase; letter-spacing: 0.04em; }
      .usm-narrate { font-family: Georgia, &apos;Times New Roman&apos;, serif; font-size: 11px; fill: #4a4540; font-style: italic; }
    &lt;/style&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;80&quot; y=&quot;22&quot; class=&quot;usm-row-label&quot;&gt;Backbone, the journey, left to right&lt;/text&gt;
  &lt;text x=&quot;820&quot; y=&quot;22&quot; text-anchor=&quot;end&quot; class=&quot;usm-narrate&quot;&gt;narrate aloud →&lt;/text&gt;

  &lt;g transform=&quot;translate(80, 35)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;50&quot; class=&quot;usm-backbone&quot; rx=&quot;3&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Discover&lt;/text&gt;&lt;text x=&quot;65&quot; y=&quot;38&quot; class=&quot;usm-text&quot; font-style=&quot;italic&quot;&gt;find the box&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(230, 35)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;50&quot; class=&quot;usm-backbone&quot; rx=&quot;3&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Sign up&lt;/text&gt;&lt;text x=&quot;65&quot; y=&quot;38&quot; class=&quot;usm-text&quot; font-style=&quot;italic&quot;&gt;become a customer&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(380, 35)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;50&quot; class=&quot;usm-backbone&quot; rx=&quot;3&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Pick a box&lt;/text&gt;&lt;text x=&quot;65&quot; y=&quot;38&quot; class=&quot;usm-text&quot; font-style=&quot;italic&quot;&gt;choose, schedule&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(530, 35)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;50&quot; class=&quot;usm-backbone&quot; rx=&quot;3&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Receive&lt;/text&gt;&lt;text x=&quot;65&quot; y=&quot;38&quot; class=&quot;usm-text&quot; font-style=&quot;italic&quot;&gt;get the box&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(680, 35)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;50&quot; class=&quot;usm-backbone&quot; rx=&quot;3&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Manage&lt;/text&gt;&lt;text x=&quot;65&quot; y=&quot;38&quot; class=&quot;usm-text&quot; font-style=&quot;italic&quot;&gt;pause, swap, cancel&lt;/text&gt;&lt;/g&gt;

  &lt;line x1=&quot;60&quot; y1=&quot;140&quot; x2=&quot;830&quot; y2=&quot;140&quot; class=&quot;usm-slice&quot; /&gt;
  &lt;text x=&quot;65&quot; y=&quot;133&quot; class=&quot;usm-slice-label&quot;&gt;Now, the walking skeleton&lt;/text&gt;

  &lt;g transform=&quot;translate(80, 100)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-skeleton&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text-skel&quot;&gt;Browse landing page&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(230, 100)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-skeleton&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text-skel&quot;&gt;Email + card&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(380, 100)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-skeleton&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text-skel&quot;&gt;Pick one default&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(530, 100)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-skeleton&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text-skel&quot;&gt;Standard delivery&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(680, 100)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-skeleton&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text-skel&quot;&gt;Cancel from email&lt;/text&gt;&lt;/g&gt;

  &lt;line x1=&quot;60&quot; y1=&quot;240&quot; x2=&quot;830&quot; y2=&quot;240&quot; class=&quot;usm-slice&quot; /&gt;
  &lt;text x=&quot;65&quot; y=&quot;233&quot; class=&quot;usm-slice-label&quot;&gt;Next&lt;/text&gt;

  &lt;g transform=&quot;translate(80, 150)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Filter by region&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(80, 195)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;See sample boxes&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(230, 150)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Apple / Google pay&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(380, 150)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Pick frequency&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(380, 195)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Substitution prefs&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(530, 150)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Delivery window&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(680, 150)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Pause for a week&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(680, 195)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Swap an item&lt;/text&gt;&lt;/g&gt;

  &lt;line x1=&quot;60&quot; y1=&quot;370&quot; x2=&quot;830&quot; y2=&quot;370&quot; class=&quot;usm-slice&quot; /&gt;
  &lt;text x=&quot;65&quot; y=&quot;363&quot; class=&quot;usm-slice-label&quot;&gt;Later&lt;/text&gt;

  &lt;g transform=&quot;translate(80, 255)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Quiz: which box?&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(80, 300)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Reviews&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(230, 255)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Referral codes&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(380, 255)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Themed boxes&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(530, 255)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Track in transit&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(530, 300)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Driver photo&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(680, 255)&quot;&gt;&lt;rect width=&quot;130&quot; height=&quot;35&quot; class=&quot;usm-task&quot; rx=&quot;2&quot; /&gt;&lt;text x=&quot;65&quot; y=&quot;22&quot; class=&quot;usm-text&quot;&gt;Gift a friend&lt;/text&gt;&lt;/g&gt;

  &lt;text x=&quot;445&quot; y=&quot;405&quot; text-anchor=&quot;middle&quot; class=&quot;usm-narrate&quot;&gt;Now is the walking skeleton: every activity has at least one task above the first slice line --&lt;/text&gt;
  &lt;text x=&quot;445&quot; y=&quot;421&quot; text-anchor=&quot;middle&quot; class=&quot;usm-narrate&quot;&gt;the journey is whole even when the polish isn&apos;t.&lt;/text&gt;
&lt;/svg&gt;

&lt;p&gt;Story Mapping is a standing-up, walking-around, wall-based ritual. Nobody sits. Notes move. The shape of the room matches the shape of the map: wide, layered, with people moving along it as the conversation moves.&lt;/p&gt;

&lt;p&gt;The rhythm is backbone, then vertical detail, then narrate, then slice:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;During the backbone phase, one person, ideally the product owner, tells the high-level story in order. Everyone else listens and places blue activity notes. It’s a single voice telling the shape of the journey; the facilitator’s job is to catch details that slip into the backbone before they belong there.&lt;/li&gt;
  &lt;li&gt;During the user tasks phase, the conversation opens up. Everyone works on multiple activities at once, writing yellow task notes and placing them vertically. People move around the wall. The facilitator circulates and catches tasks that are really implementation details or screen specs.&lt;/li&gt;
  &lt;li&gt;The walk-the-map phase is a ritual interruption. Stop adding notes. One person narrates the entire journey aloud, left to right, using only the notes on the wall. Everyone else listens for gaps. Then gaps get filled.&lt;/li&gt;
  &lt;li&gt;The slice releases phase is the most political. A piece of tape goes across the wall. Every “above the line” decision is someone committing to ship something and someone &lt;em&gt;not&lt;/em&gt; committing to ship something else. The facilitator holds the space for that trade to happen honestly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-1-orient-persona-scope-15-minutes&quot;&gt;Phase 1: Orient, persona, scope (15 minutes)&lt;/h4&gt;

&lt;p&gt;Before any note goes up, pin the persona card to the left end of the wall and read it aloud:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Today we’re mapping the experience of a first-time subscriber. Let’s call her Anna. She’s health-conscious, she’s busy, and she’s just heard about us from a friend. The map we build is her journey, from the moment she hears about us to…”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then agree the end-point explicitly:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“…where does the journey end? First delivery? Three months of subscribing? Cancellation and win-back? Pick one. We’ll map that scope and call it done.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Scope drift is the single most common Story Mapping failure. A team starts mapping “sign up to first delivery” and an hour in discovers they’re also mapping pause, substitution, and cancellation. The wall fills up and the release slice becomes impossible. Agree the end-point now, write it on a card, stick it at the right end of the wall. That’s the boundary.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Two personas trying to share a wall. &lt;em&gt;“But the supplier does X…”&lt;/em&gt; If the conversation keeps switching personas, you’re mapping two journeys. Split them into two sessions.&lt;/li&gt;
  &lt;li&gt;Scope that’s too broad. &lt;em&gt;“The whole product.”&lt;/em&gt; That’s six maps, not one. Pick the first-time subscriber journey, or the pause journey, or the renewal journey. One at a time.&lt;/li&gt;
  &lt;li&gt;Missing persona. The team can’t quite describe who the map is for. Pause: &lt;em&gt;“Let’s spend ten minutes writing a persona card before we draw anything.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-2-backbone-20-minutes&quot;&gt;Phase 2: Backbone (20 minutes)&lt;/h4&gt;

&lt;p&gt;The backbone is the user’s journey at the highest level: six to twelve big activities from the start of the journey to the end. These go along the top of the wall, left to right.&lt;/p&gt;

&lt;p&gt;Ask the product owner:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Walk me through what Anna does, from the very beginning. Not in detail. Big chunks. What’s the first thing that happens?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write each activity on a blue note and place it. Keep the granularity high: &lt;em&gt;“Discover the service,” “Sign up,” “Choose a first box,” “Receive first delivery,” “Manage ongoing subscription,” “Refer a friend.”&lt;/em&gt; Not &lt;em&gt;“Click the sign-up button,”&lt;/em&gt; which is a task, not an activity.&lt;/p&gt;

&lt;p&gt;Aim for 6–12 activities across the backbone. More than that and you’re at the wrong granularity; fewer and you’re missing stages.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Starting with the system, not the user. &lt;em&gt;“The system sends a welcome email.”&lt;/em&gt; Reframe: &lt;em&gt;“What does Anna do? She opens the email and reads it. That’s the activity.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Skipping discovery. Teams start the backbone at &lt;em&gt;“Sign up,”&lt;/em&gt; forgetting that Anna has to hear about the service first. Prompt: &lt;em&gt;“What happens before she knows we exist?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Skipping the end. Teams end the backbone at &lt;em&gt;“First delivery,”&lt;/em&gt; forgetting ongoing management, cancellation, win-back. Prompt: &lt;em&gt;“What happens after three months? After she tries to pause? After she cancels?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Tasks leaking upward. Someone places &lt;em&gt;“Enter email address”&lt;/em&gt; on the backbone. That’s a yellow task under the “Sign up” activity. Gently move it down: &lt;em&gt;“Great detail. Let’s put it below, under ‘Sign up,’ when we get to tasks.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Operations backbones. For an SRE-flavoured map (say, the journey of a deployment from commit to verified rollout) the backbone activities might be &lt;em&gt;“Developer pushes,”&lt;/em&gt; &lt;em&gt;“CI runs,”&lt;/em&gt; &lt;em&gt;“Artefact built,”&lt;/em&gt; &lt;em&gt;“Staging deployed,”&lt;/em&gt; &lt;em&gt;“Production rolled out,”&lt;/em&gt; &lt;em&gt;“Rollback decision available.”&lt;/em&gt; Same shape, different domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-3-user-tasks-30-minutes&quot;&gt;Phase 3: User tasks (30 minutes)&lt;/h4&gt;

&lt;p&gt;For each activity on the backbone, the team writes yellow task notes describing what the user does during that step. Tasks go vertically below their parent activity, ordered roughly top (most essential) to bottom (nice to have).&lt;/p&gt;

&lt;p&gt;Open the phase:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“For each blue note on the backbone, I want specific things the user does. Not UI details: intents. ‘Browse available boxes.’ ‘See what’s in each box this week.’ ‘Pick a delivery day.’ Write them on yellow, place them below the activity they belong to, and roughly stack them by importance, most essential at the top.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let the conversation flow across the wall. People will jump between activities as they think of related tasks. Let them. The facilitator circulates and catches problems.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;UI details dressed up as tasks. &lt;em&gt;“Click the dropdown.”&lt;/em&gt; Not a user task; a UI interaction. The task is &lt;em&gt;“Pick a delivery day.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Missing unhappy paths. The team maps the golden path. Prompt explicitly: &lt;em&gt;“What if her card is declined? What if the box she wants is sold out? What if she signs up, then immediately changes her mind?”&lt;/em&gt; Unhappy paths are often where the release line is hardest to draw.&lt;/li&gt;
  &lt;li&gt;One activity with twenty tasks, another with two. The dense activity probably needs splitting into two activities; the sparse one might be fine, or might be missing work.&lt;/li&gt;
  &lt;li&gt;Arguments about horizontal order. Within an activity, vertical order (priority) matters more than horizontal order. If two people disagree about what comes first horizontally, there might be two valid paths; capture both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-4-walk-the-map-15-minutes&quot;&gt;Phase 4: Walk the map (15 minutes)&lt;/h4&gt;

&lt;p&gt;Stop adding notes. Everyone takes three steps back from the wall.&lt;/p&gt;

&lt;p&gt;Ask the product owner to narrate the full journey:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Walk me through Anna’s experience. Left to right. Use only the notes on the wall. Tell her story as if I’d never heard it.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everyone else listens. The facilitator’s job is to catch the stumbles:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;“And then she… um, signs up, and then somehow ends up with a box…”&lt;/em&gt; There’s a missing activity or a missing task.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“She picks a box and pays…”&lt;/em&gt; Wait, is the payment activity there? Or is it hiding inside “sign up”?&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“…and she receives her first delivery.”&lt;/em&gt; What happens if she doesn’t? Where’s the failed-delivery path?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the narrator stumbles, pause. Ask the room:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What’s missing here?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fill the gap. Continue the walk. The walk-the-map phase catches more problems than any other single phase in the session. It is the quality check.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Nobody challenging the narrator. The room is polite. Name people by the slice of reality they own: &lt;em&gt;“From what you see in deployment tickets, does this match? From what support hears on the phones, does this match?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Duplicate tasks. The same task appearing under two activities. Is it genuinely part of both, or is one misplaced?&lt;/li&gt;
  &lt;li&gt;The narrator skipping sections. &lt;em&gt;“And then all the usual stuff happens, and…”&lt;/em&gt; Interrupt: &lt;em&gt;“Walk me through the usual stuff.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-5-slice-releases-30-minutes&quot;&gt;Phase 5: Slice releases (30 minutes)&lt;/h4&gt;

&lt;p&gt;This is the most valuable and the most political phase. Take a piece of tape or draw a horizontal line across the wall. Above the line: release 1. Below the line: later.&lt;/p&gt;

&lt;p&gt;The rule of the slice is simple:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Release 1 is the thinnest horizontal slice that still tells a complete story left to right. Anna can walk from the leftmost activity to the rightmost and achieve her goal. There will be fewer options, less polish, and more manual work, but the journey has to be whole.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For each activity, the question is the same:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What’s the absolute minimum version of this step that lets Anna get through it?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the “Choose a box” activity, the minimum might be: &lt;em&gt;“Browse available boxes. Select a size.”&lt;/em&gt; Everything else (substitutions, weekly previews, family-size recommendations, gift wrap) goes below the line.&lt;/p&gt;

&lt;p&gt;You can draw multiple lines for multiple releases. Release 1 is the MVP. Release 2 is the next thinnest slice. And so on. The point is that every release above the line is a complete journey, not a complete &lt;em&gt;activity&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Release 1 is “everything above the line for every activity.” The most common mistake. If release 1 is the full golden path for every step, it’s not an MVP; it’s the whole product. Push: &lt;em&gt;“Can a subscriber complete this step with just one option instead of five?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Cutting entire activities. If an activity has nothing above the line, the journey has a hole. Every activity needs at least one task in release 1, even if the task is manual or minimal.&lt;/li&gt;
  &lt;li&gt;“We can’t launch without…” Some things genuinely can’t be cut (payment processing, for instance). Some things feel essential but aren’t (substitution preferences for launch). Challenge each claim individually.&lt;/li&gt;
  &lt;li&gt;Uneven slices. One activity has eight release-1 tasks, another has one. Sometimes that’s correct (payment really does need more than preferences) but check that the dense activity isn’t hiding overbuild.&lt;/li&gt;
  &lt;li&gt;Operations-flavoured slicing. For a deployment-pipeline map, the release 1 slice might be &lt;em&gt;“Manual rollback, alerts go to one channel, health checks are basic, observability is minimal”&lt;/em&gt;: a pipeline that works end-to-end but isn’t polished. Later releases add automation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href=&quot;/writing/user-story-mapping-seeing-the-whole/&quot;&gt;User Story Mapping: Seeing the Whole&lt;/a&gt; for the Greenbox team’s first mapping session, including the moment the walk-the-map phase reveals a gap between “pays for the subscription” and “receives first box” that nobody had noticed, and the release-slicing conversation that saves a month of scope.&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What Can Go Wrong&lt;/h3&gt;

&lt;p&gt;The architect. Someone keeps mapping system architecture instead of user journey.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“What does Anna experience at this point? We’ll figure out the technical flow later.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; They can’t hold the distinction after three prompts. They belong in a design session that happens after the map.&lt;/p&gt;

&lt;p&gt;The everything-is-essential person. Someone argues every task is critical for release 1.
  &lt;em&gt;Recovery:&lt;/em&gt; Impose a constraint: &lt;em&gt;“We have six weeks to launch. What can Anna live without until release 2?”&lt;/em&gt; Constraints force prioritisation in a way that abstract discussions don’t.
  &lt;em&gt;Stop if:&lt;/em&gt; The person won’t accept any constraint. Escalate; they need a separate conversation about scope with the product owner.&lt;/p&gt;

&lt;p&gt;The map gets too big. The wall is full and the team is still adding activities.
  &lt;em&gt;Recovery:&lt;/em&gt; Scope is too broad. Pick the most important journey (e.g., first-time subscriber sign-up to first delivery) and park the rest for separate sessions.
  &lt;em&gt;Stop if:&lt;/em&gt; The team can’t agree on which journey to focus on. That’s a strategy problem, not a mapping problem.&lt;/p&gt;

&lt;p&gt;Two users on one map. The conversation keeps switching personas.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“I’m hearing both subscriber tasks and supplier tasks. Those are two maps. Let’s finish the subscriber one today and schedule the supplier session for next week.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The team insists the personas share a journey. Check: do they really? Or are you trying to save a session?&lt;/p&gt;

&lt;p&gt;The design session. People start sketching screens.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“Park the screens. We’re mapping what Anna needs to do, not how the screen looks. The design comes after we agree on the journey.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; Screens keep creeping back in. Pair the designer with a developer to hold each other honest.&lt;/p&gt;

&lt;p&gt;The silent release-slicing. The team is quietly placing tasks above or below the line without any debate.
  &lt;em&gt;Recovery:&lt;/em&gt; Slow it down: &lt;em&gt;“Before we go further, can someone explain out loud why the substitution preferences are above the line? I want to hear the reasoning.”&lt;/em&gt; The point of the slice is the conversation about the trade-off.
  &lt;em&gt;Stop if:&lt;/em&gt; The team still won’t engage. Something else is going on; maybe the release date is imposed and the slice is performative. Name it.&lt;/p&gt;

&lt;p&gt;The map goes stale. The map is produced and photographed but nobody updates it. Within weeks, the backlog and the map disagree, and the team starts trusting the backlog.
  &lt;em&gt;Recovery:&lt;/em&gt; Re-flatten the backlog from the map. Schedule a thirty-minute map-update at the end of every release.
  &lt;em&gt;Stop if:&lt;/em&gt; The team won’t keep the map alive. Story Mapping is the wrong artefact for them; a flat backlog with explicit MVP scoping may be enough.&lt;/p&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;

&lt;p&gt;The session ends; the work begins.&lt;/p&gt;

&lt;p&gt;Same day, the facilitator:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Takes panoramic photographs of the full wall. Good lighting, sharp focus, enough resolution to read every note.&lt;/li&gt;
  &lt;li&gt;Takes close-up shots of each activity section, so the detail is preserved even if the panorama isn’t sharp enough.&lt;/li&gt;
  &lt;li&gt;Transcribes release-1 tasks into the backlog with the activity as context, so every story knows which journey stage it belongs to.&lt;/li&gt;
  &lt;li&gt;Sends a message to all participants with the photographs and the release line clearly marked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This week, the product owner:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Turn release-1 tasks into backlog items. Each yellow note above the line becomes a backlog item, with the activity as context. The shape of the wall becomes the shape of the sprint plan over the next several iterations.&lt;/li&gt;
  &lt;li&gt;Protect the release-1 slice. The single hardest follow-up work. Every time a stakeholder asks for “just one more thing in the first release,” check the wall. Either it moves above the line (and something else moves below) or it waits for release 2. The map is the reason you can say that and have it be defensible.&lt;/li&gt;
  &lt;li&gt;Begin Example Mapping on the top release-1 tasks. Story Mapping produces tasks; &lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt; makes them buildable. Start on the most essential tasks first.&lt;/li&gt;
  &lt;li&gt;Walk the map to anyone who couldn’t attend. Their perspective may reveal gaps the original group missed, or validate the slice. Either outcome is valuable.&lt;/li&gt;
  &lt;li&gt;Schedule any discovery work. Tasks on the wall that are guesses (&lt;em&gt;“we think Anna will want this”&lt;/em&gt;) become research or experiment proposals. Don’t let the guesses graduate into commitments without being tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ongoing, the team:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Keeps the map visible while the work is active. Print it, photograph it, pin it near the team’s desks. Teams that can glance at the map during standups and planning make better decisions than teams working from memory.&lt;/li&gt;
  &lt;li&gt;Updates the map after each release. Move the line to the next slice. Add new tasks learned from real user feedback. Remove tasks that turned out not to matter.&lt;/li&gt;
  &lt;li&gt;When new work is proposed, places it on the map. If it doesn’t fit, either the map needs updating or the work doesn’t belong. That’s a valuable filter.&lt;/li&gt;
  &lt;li&gt;Treats the map as the source and the backlog as a flattening of release 1 for execution. When they disagree, the map wins and the backlog gets re-flattened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where the map feeds next:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming&lt;/a&gt;: Event Storming maps the system from the inside; Story Mapping maps the user experience from the outside. Running Event Storming first can reveal the hotspots; Story Mapping turns the insights into a release plan.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt;: Impact Mapping picks the deliverables; Story Mapping arranges those deliverables into a user journey and slices them into releases. Impact first, then story.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt;: the release-1 tasks from a Story Mapping session are the input to Example Mapping. Story Mapping gives you the list of stories; Example Mapping decides whether each one is ready to build.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-sprint-planning/&quot;&gt;Sprint Planning&lt;/a&gt;: once the release-1 slice exists and Example Mapping has run on the top stories, Sprint Planning turns the map into committed sprints.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt;: the release-1 slice is a stack of assumptions about what users need. Assumption Mapping pulls the slice apart before you commit to building it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;variants&quot;&gt;Variants&lt;/h3&gt;

&lt;p&gt;Product Level (default). A new product or a major new feature area, two to three hours, five to eight people, one persona. Output: a backbone, vertical task columns, and at least one release line marking the walking skeleton. This is what most teams need, and the rest of this post describes it.&lt;/p&gt;

&lt;p&gt;Feature Level. A single feature area inside an existing product, ninety minutes, four to six people. The persona and scope are tighter; the backbone is shorter (often four to six activities); the release lines often collapse to a single now/next split. Reach for it when a product map already exists and you’re zooming into one journey within it.&lt;/p&gt;

&lt;p&gt;Multi-persona. Two or more personas whose journeys overlap. Don’t try to share a wall: run two sessions on consecutive days with overlapping attendance, then reconcile in a third short session that compares the maps and surfaces the shared activities. Trying to run multi-persona on one wall in one session is the most common reason a Story Mapping session collapses.&lt;/p&gt;

&lt;p&gt;Operations / SRE. The user being mapped is an on-call engineer, a deploy pipeline operator, or a support agent using internal tools. The backbone is a workflow rather than a customer journey; release 1 is the manual-but-end-to-end version of the workflow; later releases add automation. Same shape, different domain.&lt;/p&gt;

&lt;p&gt;Remote. A Miro or Mural board with the persona pinned to the left and a horizontal lane for the backbone. Slightly slower than in-person (the rhythm of standing-up-and-moving is faster physically), but the structure transfers cleanly. Use one shared cursor: only the facilitator places notes, prompted by the team, to keep the layout legible. Walk-the-map still works, the narrator shares their screen and scrolls left to right while everyone listens.&lt;/p&gt;

&lt;p&gt;Map-update. A thirty-minute recurring session at the end of every release. Move the line to the next slice. Add new tasks learned from real user feedback. Remove tasks that turned out not to matter. Keeps the map alive instead of letting it go stale.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Ticks or Tocks?</title>
    <link href="/writing/ticks-or-tocks/"/>
    <updated>2026-04-23T06:00:00+08:00</updated>
    <id>/writing/ticks-or-tocks/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;In &lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;What Time Is It?&lt;/a&gt; we covered the human mess of the hour, sundials, railways, time zones, daylight saving, and the volunteer-maintained database that keeps your phone from lying to you. &lt;a href=&quot;/writing/what-day-is-it/&quot;&gt;What Day Is It?&lt;/a&gt; did the same for the date. Gregorian switchovers, lunisolar calendars, the date line, and the year numbers that don’t agree. All of that assumes we know what a “second” actually is. But what is a second? How do you count one? And what happens when you count very, very carefully?&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;what-are-we-actually-counting&quot;&gt;What are we actually counting?&lt;/h3&gt;

&lt;p&gt;A second used to be defined as 1/86,400 of a mean solar day. Simple enough, divide the day into hours, the hours into minutes, the minutes into seconds, done. The problem is that the Earth’s rotation isn’t constant. Tidal friction from the moon is gradually slowing us down. “Gradually” here means roughly 2.3 milliseconds per century, which sounds negligible until you’re trying to land a spacecraft or synchronise financial transactions across continents.&lt;/p&gt;

&lt;p&gt;In 1967, the 13th General Conference on Weights and Measures decoupled the second from the Earth entirely. A second is now defined as 9,192,631,770 periods of the radiation corresponding to the transition between two hyperfine levels of the ground state of the caesium-133 atom. This is a mouthful. In plain terms: a caesium atom can exist in two very slightly different energy states, think of it like a coin that can be heads or tails, and when it flips between them, it emits radiation at one very specific frequency. Count those oscillations and you’re counting seconds. The advantage is enormous: this frequency is the same everywhere in the universe (with a caveat we’ll get to in a later post), and it’s measurable to extraordinary precision.&lt;/p&gt;

&lt;p&gt;The trouble is that atomic seconds and solar days are now measuring different things. Atomic time marches on with metronomic precision. Solar time wobbles and drifts. They disagree, and the disagreement grows over time.&lt;/p&gt;

&lt;h3 id=&quot;from-quartz-to-caesium&quot;&gt;From quartz to caesium&lt;/h3&gt;

&lt;p&gt;Before atomic clocks, the most precise portable timekeepers were quartz crystal oscillators. Quartz has a neat trick: squeeze it and it generates a tiny voltage. Run a voltage through it and it vibrates. This property is called piezoelectricity, and it’s why quartz became the heart of modern timekeeping. A quartz crystal cut to the right shape and size vibrates at a very stable frequency. In a wristwatch, that frequency is typically 32,768 Hz. That’s 2 to the power of 15, chosen because it can be divided down to exactly one pulse per second using a simple binary counter circuit, fifteen halvings and you’re there.&lt;/p&gt;

&lt;p&gt;The first quartz clock was built at Bell Telephone Laboratories in 1927 by Warren Marrison and J.W. Horton. It was roughly the size of a large refrigerator. By the late 1960s, Seiko had miniaturised the technology enough to fit it on a wrist, the Seiko Astron, released on Christmas Day 1969, was the world’s first commercially available quartz wristwatch. It cost as much as a small car. Within a decade, quartz watches were cheap enough to give away as promotional items. The Swiss watch industry, which had dominated mechanical horology for centuries, was nearly destroyed in what’s now called the Quartz Crisis. Accuracy that had once required master craftsmen and hand-finished movements was suddenly available from a factory in Japan for a few dollars.&lt;/p&gt;

&lt;p&gt;Quartz watches were accurate to within a few seconds per month, far better than any mechanical watch. But quartz crystals aren’t perfect. Their frequency drifts with temperature, age, and mechanical stress. Engineers have pushed quartz further by controlling temperature. A temperature-compensated oscillator can hold stability to within a second or two per year. An oven-controlled version, the crystal sits in a tiny heated enclosure to keep its temperature rock-steady, does better still, a few milliseconds per day. For everyday timekeeping, even basic quartz is more than adequate. For science, navigation, and telecommunications, it matters enormously that “a few seconds per month” isn’t zero.&lt;/p&gt;

&lt;p&gt;The leap to atomic timekeeping came from the insight that atoms are, in a sense, nature’s own frequency standards. The idea was first proposed by Isidor Rabi at Columbia University in 1945, building on his Nobel Prize-winning work on how atoms behave in magnetic fields. Every caesium-133 atom in the universe vibrates at exactly the same frequency when it transitions between two specific energy states. No manufacturing variation. No wear. No temperature drift (at least, not in the transition itself). If you can build a device that locks onto that frequency and counts the vibrations, you have a clock that’s stable to a degree that mechanical and quartz clocks can’t approach.&lt;/p&gt;

&lt;p&gt;Why caesium specifically? Several reasons converged. Caesium has only one stable isotope (caesium-133), which eliminates ambiguity about which atom you’re measuring. Its hyperfine transition frequency, the frequency at which it flips between two energy states, falls in the microwave range at roughly 9.2 GHz, which in the 1950s was a frequency that existing electronics could already generate and measure accurately. Hydrogen has a simpler spectrum but its transition frequency is lower (1.4 GHz), giving coarser time slices. Rubidium was a strong candidate and is still used in cheaper atomic clocks, but its transition is harder to isolate cleanly because rubidium has two stable isotopes whose spectra overlap. Caesium’s combination of a single isotope, a conveniently high microwave frequency, and a strong well-separated spectral line made it the practical choice. The physics didn’t mandate caesium, it was the best available compromise between atomic properties and 1950s-era engineering.&lt;/p&gt;

&lt;p&gt;The first working caesium beam clock, built by Louis Essen and Jack Parry at the National Physical Laboratory in Teddington, England, began operating in 1955. Within two years it had demonstrated accuracy of one second in 300 years, already orders of magnitude better than any quartz oscillator. By 1967, it was good enough that the international scientific community decided to redefine the second itself based on the caesium atom rather than the Earth’s rotation. The atom had become more reliable than the planet.&lt;/p&gt;

&lt;h3 id=&quot;atomic-clocks-and-their-limits&quot;&gt;Atomic clocks and their limits&lt;/h3&gt;

&lt;p&gt;A caesium beam clock works by exposing a beam of caesium-133 atoms to microwave radiation and tuning the frequency until the maximum number of atoms change energy states. That peak frequency, 9,192,631,770 Hz exactly, by definition, &lt;em&gt;is&lt;/em&gt; the second. Hydrogen maser clocks, a maser is a laser that works at microwave frequencies, use a similar principle with hydrogen atoms and are more stable over short periods, making them excellent for applications that need precise frequency over hours rather than years.&lt;/p&gt;

&lt;p&gt;Optical lattice clocks represent the current frontier. They use atoms (often strontium or ytterbium) trapped in a lattice of laser light and interrogated with optical-frequency lasers rather than microwaves. The higher frequency means finer measurement. The best optical lattice clocks at NIST and JILA in the US, and at the University of Tokyo, have demonstrated accuracy of roughly one second in 15 billion years, longer than the age of the universe (Bloom et al., 2014, &lt;em&gt;Nature&lt;/em&gt;). In 2024, the BIPM began formally considering redefining the second based on optical clocks.&lt;/p&gt;

&lt;p&gt;But even they drift. Every clock, no matter how precise, has some uncertainty. Caesium beam clocks drift by roughly one second in 300 million years. Optical lattice clocks are better by orders of magnitude, but “better” isn’t “perfect”. No clock is perfect. This is a fundamental consequence of quantum mechanics: measurement always has uncertainty.&lt;/p&gt;

&lt;p&gt;To address that uncertainty, UTC is kept not by a single clock but by an ensemble of clocks, a weighted average of approximately 450 atomic clocks in laboratories across more than 80 countries. The Bureau International des Poids et Mesures (BIPM) in Paris collects data from all of them, weights each clock by its past performance and stability, and computes a combined timescale called UTC. The results are published retrospectively in a document called Circular T, which means that UTC is, strictly speaking, only known &lt;em&gt;after the fact&lt;/em&gt;. The UTC that your phone shows you is actually an approximation, steered to match the BIPM’s post-hoc calculation as closely as possible.&lt;/p&gt;

&lt;h3 id=&quot;leap-years-and-leap-seconds&quot;&gt;Leap years and leap seconds&lt;/h3&gt;

&lt;p&gt;Most people know about leap years. The Earth takes approximately 365.2422 days to orbit the sun, so every four years we add a day to February to stop the calendar drifting away from the seasons. Except every 100 years we skip the leap year. Except every 400 years we don’t skip it. So 1900 wasn’t a leap year, but 2000 was. This approximation is good to about one day in 3,236 years, which is close enough that nobody currently alive needs to worry about the next correction.&lt;/p&gt;

&lt;p&gt;Leap seconds are a much more recent and much messier invention. Since atomic clocks and the Earth’s rotation disagree, the International Earth Rotation and Reference Systems Service (IERS) adds a leap second to UTC whenever the difference approaches 0.9 seconds from solar time, not on a fixed schedule, but when observed drift demands it. They’ve done this 27 times since 1972, always on the last day of June or December. All 27 have been positive, adding a second because the Earth is slowing down. But in recent years the Earth has unexpectedly sped up slightly, and for a while there was serious discussion about whether we’d need a &lt;em&gt;negative&lt;/em&gt; leap second, removing a second, something that has never been done and that most software has certainly never been tested for. The prospect of 23:59:58 being followed directly by 00:00:00, skipping 23:59:59 entirely, was enough to give the timekeeping community genuine anxiety.&lt;/p&gt;

&lt;p&gt;This sounds harmless but it drives software engineers to quiet despair. A leap second means that the sequence 23:59:59 is followed by 23:59:60 before 00:00:00. Most software doesn’t expect a minute to have 61 seconds. When a leap second was inserted in 2012, it crashed Reddit, Gawker, LinkedIn, FourSquare, and Yelp because of a Linux kernel bug in the way NTP interacted with the high-resolution timer system.&lt;/p&gt;

&lt;p&gt;Google’s approach is to “smear” the leap second, they slightly slow down their clocks over a period of hours so the extra second is absorbed gradually. Amazon does something similar, though with a different smear profile. This is practical but means that during the smear window, Google’s clocks disagree with Amazon’s, and both disagree with everyone else’s, and a timestamp generated on one platform during that window doesn’t mean quite the same thing as a timestamp generated on another. If you’re processing financial transactions that cross cloud providers during a leap second smear, you’d best not think too hard about what “the same time” means.&lt;/p&gt;

&lt;p&gt;The good news, or bad news depending on your perspective, is that in 2022 the General Conference on Weights and Measures voted to abolish leap seconds by 2035. UTC and solar time will be allowed to drift apart, with a correction planned at some larger threshold, perhaps a “leap minute” in a century or so. Astronomers who need solar time will adjust. The rest of us will stop having to worry about 61-second minutes.&lt;/p&gt;

&lt;h3 id=&quot;describing-a-moment&quot;&gt;Describing a moment&lt;/h3&gt;

&lt;p&gt;Given all of this, how do you actually specify an exact moment in time?&lt;/p&gt;

&lt;p&gt;You might think a timestamp like “2026-04-28T14:30:00Z” does the job. And it does, &lt;em&gt;mostly&lt;/em&gt;. The “Z” means UTC, which is a specific timescale maintained by a weighted average of atomic clocks around the world. But UTC includes leap seconds, which makes the relationship between any two UTC timestamps ambiguous unless you know how many leap seconds occurred between them.&lt;/p&gt;

&lt;p&gt;This is where TAI. International Atomic Time, comes in. TAI is a pure count of standard atomic seconds (the internationally defined SI second, based on caesium) since an epoch in 1958, with no leap seconds. It’s the “true” atomic timescale. UTC is defined as TAI minus some whole number of seconds (currently 37). If you want to measure the exact duration between two events, TAI is what you want. If you want to know roughly what angle the sun is at, UTC is what you want.&lt;/p&gt;

&lt;p&gt;Then there’s GPS time, which started counting at the same moment as UTC in January 1980 and has never inserted a leap second since. GPS time is currently 18 seconds ahead of UTC.&lt;/p&gt;

&lt;p&gt;And there are others. TDB, Barycentric Dynamical Time, used for solar system ephemerides. TCG, Geocentric Coordinate Time, which ticks slightly faster than clocks on Earth’s surface because it’s defined for a clock at rest and infinitely far from the Earth’s gravitational field. Each serves a different purpose, each disagrees with the others by small but significant amounts.&lt;/p&gt;

&lt;p&gt;The point is that “what time is it?” is never a single question. It’s really “what time is it, &lt;em&gt;in which timescale&lt;/em&gt;, as measured by &lt;em&gt;which clock&lt;/em&gt;, &lt;em&gt;where&lt;/em&gt;?”&lt;/p&gt;

&lt;p&gt;For most software, the practical answer is: use UTC, store it as an ISO 8601 string or a Unix timestamp (seconds since midnight on 1 January 1970, UTC, not counting leap seconds), and convert to local time for display only. This works for the vast majority of applications. But if you need to compute precise durations across leap second boundaries, or compare timestamps from different systems that may have been smearing at different rates, or handle historical dates in jurisdictions that have changed their timezone rules, “just use UTC” stops being simple fast. The rabbit hole is always deeper than it looks.&lt;/p&gt;

&lt;h3 id=&quot;ntp-and-time-synchronisation&quot;&gt;NTP and time synchronisation&lt;/h3&gt;

&lt;p&gt;Having an accurate clock is only half the problem. You also need to get that accuracy to the devices that need it. This is the job of the Network Time Protocol, NTP.&lt;/p&gt;

&lt;p&gt;NTP was designed by David Mills at the University of Delaware in 1985, and its descendants still synchronise nearly every clock on the internet. The protocol works by exchanging timestamps between a client and a server, measuring the round-trip delay, and using the result to estimate the offset between the two clocks. The clever bit is in the statistics. NTP uses filtering algorithms to reject noisy measurements and converge on the best estimate of the true time.&lt;/p&gt;

&lt;p&gt;The system is hierarchical. Stratum 0 sources are the reference clocks themselves, caesium standards, GPS receivers, radio stations like DCF77 in Germany or WWVB in the US that broadcast time signals. Stratum 1 servers are directly connected to a Stratum 0 source. Stratum 2 servers synchronise to Stratum 1, and so on. Your laptop or phone is typically Stratum 3 or 4, synchronised to a pool of public NTP servers.&lt;/p&gt;

&lt;p&gt;That pool, the &lt;a href=&quot;https://www.ntppool.org/&quot;&gt;NTP Pool Project&lt;/a&gt;, is another piece of critical internet infrastructure run almost entirely by volunteers. Over 4,000 servers donated by individuals and organisations around the world, serving billions of time queries per day. When your phone synchronises its clock, it’s probably talking to a server that someone is running in their spare time, on their own hardware, at their own expense. Like the tz database, like the DNS root servers, like so much of the infrastructure the modern world depends on, it works because people choose to make it work. There’s no contract. There’s no SLA. There’s just a community that thinks accurate time matters enough to donate the resources.&lt;/p&gt;

&lt;p&gt;The accuracy you can achieve depends on your network. On a local network, NTP can keep clocks within a few hundred microseconds. Over the internet, a few milliseconds is typical. For applications that need tighter synchronisation, financial trading, for instance, or telecommunications. Precision Time Protocol (PTP, IEEE 1588) operates at the hardware level, timestamping packets as they enter and leave the network interface card, and can achieve sub-microsecond accuracy.&lt;/p&gt;

&lt;p&gt;GPS is also a time-distribution system, not just a positioning one. In fact, positioning &lt;em&gt;is&lt;/em&gt; time distribution, a GPS receiver determines its position by measuring the time it takes signals to arrive from multiple satellites, then solving for the intersection. Each GPS satellite carries multiple atomic clocks, some using caesium, others using rubidium, a cheaper and lighter alternative that trades a bit of long-term accuracy for portability and broadcasts precise time signals. A GPS receiver on the ground can determine the time to within roughly 10 nanoseconds. Many NTP Stratum 1 servers use GPS as their reference source.&lt;/p&gt;

&lt;p&gt;But GPS is a &lt;a href=&quot;https://www.gps.gov/gps&quot;&gt;US military system&lt;/a&gt;. It was built by the Department of Defense, it’s operated by the US Space Force, and the US government retains the right to degrade or deny the civilian signal at will. They did exactly that until May 2000, a deliberate error called &lt;a href=&quot;https://archive.gps.gov/systems/gps/modernization/sa/&quot;&gt;Selective Availability&lt;/a&gt; that made civilian GPS accurate to about 100 metres instead of 10. The military got the good signal. Everyone else got the blurred one.&lt;/p&gt;

&lt;p&gt;That dependency on a single nation’s military made other countries nervous. The European Union built &lt;a href=&quot;https://www.euspa.europa.eu/eu-space-programme/galileo&quot;&gt;Galileo&lt;/a&gt;, which became fully operational in 2016, a civilian-controlled system from the start, with no equivalent of Selective Availability. Russia has &lt;a href=&quot;https://www.glonass-iac.ru/en/&quot;&gt;GLONASS&lt;/a&gt;, operational since 1993. China has &lt;a href=&quot;http://www.beidou.gov.cn/&quot;&gt;BeiDou&lt;/a&gt;, globally operational since 2020. India has &lt;a href=&quot;https://www.isro.gov.in/Navic.html&quot;&gt;NavIC&lt;/a&gt; covering the Indian subcontinent.&lt;/p&gt;

&lt;p&gt;Modern receivers use multiple constellations simultaneously. Your phone probably tracks GPS, Galileo, and GLONASS at once. More satellites in view means better geometry, faster fixes, and improved accuracy, from roughly 3-5 metres with GPS alone to under 1 metre with multi-constellation receivers. For timing applications, using multiple independent constellations also provides redundancy: if one system has a problem, the others keep you synchronised.&lt;/p&gt;

&lt;p&gt;When synchronisation fails, the consequences are real. In 2016, a GPS ground station error introduced a &lt;a href=&quot;https://insidegnss.com/gps-experiences-utc-timing-iif-satellite-launcher-problems/&quot;&gt;13-microsecond timing glitch&lt;/a&gt; that propagated to GPS-disciplined clocks worldwide. Telecommunications networks that relied on GPS for synchronisation experienced disruptions. In 2019, a Galileo outage left receivers without a valid time signal for &lt;a href=&quot;https://www.euspa.europa.eu/newsroom-events/news-archive/update-availability-some-galileo-initial-services&quot;&gt;several days&lt;/a&gt;. Having multiple constellations didn’t prevent the Galileo outage, but it meant that receivers tracking GPS and GLONASS simultaneously kept working while Galileo was down. Redundancy isn’t a theoretical benefit, it’s the difference between “the system degraded” and “the system failed.”&lt;/p&gt;

&lt;p&gt;Radio time signals offer a terrestrial alternative. MSF in the UK broadcasts from Anthorn in Cumbria on 60 kHz. DCF77 in Germany broadcasts from Mainflingen near Frankfurt on 77.5 kHz. WWVB in the US broadcasts from Fort Collins, Colorado on 60 kHz. These long-wave signals can reach hundreds of kilometres and are used by “radio-controlled” clocks and watches, the ones that seem to magically stay accurate without any intervention. They receive the signal, typically at night when propagation is best, and correct themselves against it. The system is elegant and low-tech compared to GPS, but limited in precision to roughly a millisecond and in range to whatever the transmitter can cover.&lt;/p&gt;

&lt;p&gt;The dependency chain is worth noting: your phone’s clock depends on NTP, which depends on Stratum 1 servers, which depend on atomic clocks or GPS, which depends on the satellites’ onboard atomic clocks, which depend on the ground control system that monitors and corrects them against the master clock at the US Naval Observatory. Every link in the chain adds a tiny bit of uncertainty. The time on your phone is an estimate, steering toward a post-hoc average of 450 clocks, computed in Paris, distributed through a hierarchy of servers and satellites, and corrected for relativistic effects that Einstein predicted in 1915. It’s close enough. It’s never exact.&lt;/p&gt;

&lt;h3 id=&quot;time-in-the-financial-markets&quot;&gt;Time in the financial markets&lt;/h3&gt;

&lt;p&gt;Nowhere is the practical importance of precise time synchronisation more visible than in financial trading. The EU’s MiFID II regulation, which came into force in January 2018, requires that timestamps on financial transactions be accurate to within 100 microseconds of UTC for most trading activities, and within one microsecond for high-frequency trading. The US SEC has similar requirements. This isn’t paranoia, it’s about being able to reconstruct the exact order of events when disputes arise or markets crash.&lt;/p&gt;

&lt;p&gt;High-frequency trading firms spend millions on low-latency connections and precise clock synchronisation. A difference of a few microseconds can determine who gets a trade filled and who doesn’t. Some firms use rubidium or caesium oscillators at their trading sites, disciplined by GPS, to ensure their timestamps are as close to UTC as hardware allows. Others lease dedicated fibre connections to minimise and stabilise network latency between their servers and the exchange.&lt;/p&gt;

&lt;p&gt;The irony is that all this infrastructure. GPS-disciplined atomic clocks, PTP synchronisation, nanosecond-accurate timestamps, exists to coordinate an activity (buying and selling financial instruments) that is fundamentally a human invention. We built clocks precise enough to measure relativistic effects, and we use them to work out who pressed “buy” first.&lt;/p&gt;

&lt;h3 id=&quot;the-clock-inside-everything&quot;&gt;The clock inside everything&lt;/h3&gt;

&lt;p&gt;We’ve gone from sticks in the ground to laser-trapped atoms oscillating hundreds of trillions of times per second. The precision is breathtaking. But precision brings its own strange problems. When your clocks are accurate enough to detect the difference in gravity between the floor and the ceiling, “what time is it?” stops being a simple question and starts being a question about the structure of spacetime itself.&lt;/p&gt;

&lt;p&gt;That’s where things get weird. In &lt;a href=&quot;/writing/time-is-weirder-than-you-think/&quot;&gt;Time Is Weirder Than You Think&lt;/a&gt;, we’ll see what happens when Einstein enters the picture, why GPS satellites need relativistic corrections, why the core of the Earth is younger than the surface, and why time might not flow at all.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Routing to the Closest Healthy Region</title>
    <link href="/writing/routing-to-the-closest-healthy-region/"/>
    <updated>2026-04-22T06:00:00+08:00</updated>
    <id>/writing/routing-to-the-closest-healthy-region/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;&lt;strong&gt;Solutions Architect Associate&lt;/strong&gt; · SAA-C03 · part of &lt;a href=&quot;/writing/exam-room/&quot;&gt;The Exam Room&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-situation&quot;&gt;The situation&lt;/h3&gt;

&lt;p&gt;A company operates a web application with regional deployments in three AWS Regions, us-east-1, eu-west-1, and ap-southeast-2. Each Region has an Application Load Balancer fronting an Auto Scaling group of EC2 instances.&lt;/p&gt;

&lt;p&gt;The team wants &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.example.com&lt;/code&gt; to route each user to the Region with the lowest network latency for their resolver, and, when the preferred Region is unhealthy, to fall over to the next-lowest-latency healthy Region automatically. They do not want to rely on client-side retries or third-party DNS, and they are allergic to extra health-check resources to configure, bill, and maintain.&lt;/p&gt;

&lt;p&gt;Today every region is independently reachable via its own hostname; there is no intelligent DNS layer in front of them. This scenario is the work of picking one.&lt;/p&gt;

&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;

&lt;p&gt;Before opening the Route 53 console, ask what this routing layer is actually supposed to do, because “closest region with failover” hides a cluster of decisions.&lt;/p&gt;

&lt;p&gt;The first question is what “closest” even means. Physical distance is the intuitive answer, and it’s usually wrong. A user in Istanbul is physically close to Frankfurt but might take a faster network path to Dublin depending on peering; a user in Perth is physically close to Singapore but the undersea cable topology can put Sydney at a similar RTT. The honest definition of “closest” for a web application is “lowest measured latency”, and the routing layer has to have some way of knowing that. A policy that picks on continent-code geography will be wrong for every user whose network path doesn’t match their passport.&lt;/p&gt;

&lt;p&gt;The second question is how the routing layer learns that a Region is unhealthy. There are two paths in Route 53: a dedicated health-check resource that we configure, point at an endpoint, and pay for, or a derived signal lifted from an AWS resource we already own, an ALB’s own view of its target-group health. The second path is cheaper and less likely to drift out of sync with reality, because the ALB is the thing that knows whether any backend is serving traffic. The first path is more flexible (it can check any URL anywhere) but it’s another resource in the inventory.&lt;/p&gt;

&lt;p&gt;The third question is what happens when &lt;em&gt;every&lt;/em&gt; region is unhealthy. DNS does not have a clean way to say “the service is globally offline”. Returning an empty answer sounds honest but breaks clients that can’t distinguish “resolver broken” from “service broken”. Returning a wrong answer gives the client something to try. Route 53’s own choice here, return everything when nothing is healthy, is worth knowing because it’s the default we inherit, not a knob we tune.&lt;/p&gt;

&lt;p&gt;The fourth question is whether the policy composes. Real traffic-flow graphs rarely fit inside one routing decision. “Closest region that’s healthy, but EU users only ever go to EU regions”, “closest region that’s healthy, with a per-region active/passive inner layer”, these are common shapes, and picking a policy that refuses to nest underneath or on top of another one writes the team into a corner later. Route 53 supports up to ten levels of nesting; the tool is ready even if the first problem only needs one level.&lt;/p&gt;

&lt;p&gt;And finally there’s operational overhead. Each of these routing policies is a set of record configurations plus, sometimes, a Traffic Flow policy document plus, sometimes, health-check resources plus, sometimes, CloudWatch alarms wiring those checks to SNS. The cheapest answer on paper isn’t the cheapest answer in the on-call rotation if the pager goes off because someone edited the Traffic Flow JSON by hand.&lt;/p&gt;

&lt;h3 id=&quot;what-well-filter-on&quot;&gt;What we’ll filter on&lt;/h3&gt;

&lt;p&gt;Distilling that exploration into filters we can score each routing policy against:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Locality-aware. The policy picks on measured network latency, not continent code, not weight, not physical distance.&lt;/li&gt;
  &lt;li&gt;Health-aware. Unhealthy records drop out of the candidate set without a human editing DNS.&lt;/li&gt;
  &lt;li&gt;Supports three or more records. Two-record policies are structurally insufficient for this three-Region shape.&lt;/li&gt;
  &lt;li&gt;Low operational overhead. The health signal comes from a resource we already own, ideally the ALB itself, rather than a separate Route 53 health check configured, monitored, and billed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-route-53-routing-policy-landscape&quot;&gt;The Route 53 routing-policy landscape&lt;/h3&gt;

&lt;p&gt;Route 53 ships seven routing policies. Each picks an answer on a different axis.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Simple routing. One record, no decision logic. Route 53 returns whatever is configured, regardless of who asked. Useful for single-Region services and static aliases. Can’t filter by health. Can’t do locality.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Weighted routing. Distributes answers across N records in proportion to integer weights. Supports health checks, unhealthy records drop from the pool. Ignores latency entirely. A resolver in Sydney with three equal-weight records would get a random continent on every new query. Useful for canary rollouts and gradual blue/green traffic shifts, not locality.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Latency-based routing. Returns the record pointing to the AWS Region with the lowest measured round-trip time for the resolver’s location. Supports one record per AWS Region. Supports health checks, including the lightweight &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EvaluateTargetHealth&lt;/code&gt; path on alias records, which reuses an existing resource’s health signal instead of configuring a separate health-check resource.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Geolocation routing. Returns the record matching the resolver’s geographic location. Granularity runs continent → country → US state, with a mandatory default record for resolvers that don’t match any configured entry. A resolver in Istanbul sits in Asia by IANA continent code, so it would be routed to the APAC record even when eu-west-1 has a lower network RTT. Failover is weaker too, a failing geographic record falls through only to the default, not to the next-nearest neighbour.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Geoproximity routing. Returns the record “closest” to the resolver by physical distance from a user-configured origin, with optional bias (-99 to +99) that stretches or shrinks each origin’s pull. Distance is physical, not network. Requires Route 53 Traffic Flow (a visual policy-editor layer on top of DNS records) to configure, so the setup is no longer a simple record set.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Failover routing. Two records, primary and secondary. Route 53 returns the primary while its health check passes, the secondary when it doesn’t. Supports exactly N = 2. Ignores latency, a Sydney user would hit us-east-1 at 220 ms when ap-southeast-2 would give them 5 ms. Useful for active/passive DR and as the &lt;em&gt;inner&lt;/em&gt; layer of a nested policy, not as a standalone answer.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Multi-value answer routing. Returns up to eight healthy records per query; the client’s resolver picks one (typically the first). Doesn’t consider latency, weight, or geography, it’s DNS-level load balancing for clients that retry if the first answer fails.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;side-by-side&quot;&gt;Side by side&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Routing policy&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Locality-aware&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Health-aware&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;N ≥ 3&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Low ops overhead&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Simple&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Weighted&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Latency-based (alias + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EvaluateTargetHealth&lt;/code&gt;)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Geolocation&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓ (weak)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Geoproximity&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Failover&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Multi-value answer&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✗&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;✓&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;One row is all ticks, latency-based routing with alias records and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EvaluateTargetHealth = true&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;matching-shape-to-policy&quot;&gt;Matching shape to policy&lt;/h3&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1100 640&quot; style=&quot;max-width: 100%; height: auto; font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Three candidate routing policies, latency-based, geolocation, and failover, flow down through gates for locality, health, N greater than or equal to 3, and low operational overhead. Latency-based alias records with EvaluateTargetHealth pass every gate and land at the pick. Geolocation fails locality because continent code is not latency. Failover fails the N greater than or equal to 3 gate because it is structurally two-record.&quot;&gt;
  &lt;defs&gt;
    &lt;style&gt;
      .crsa-bg-latency     { fill: rgba(46, 138, 90, 0.08); stroke: rgba(46, 138, 90, 0.55); stroke-width: 2; }
      .crsa-bg-geo         { fill: rgba(214, 142, 41, 0.08); stroke: rgba(214, 142, 41, 0.55); stroke-width: 2; }
      .crsa-bg-failover    { fill: rgba(170, 70, 70, 0.08); stroke: rgba(170, 70, 70, 0.55); stroke-width: 2; }
      .crsa-card-latency   { fill: #fff; stroke: rgba(46, 138, 90, 0.8); stroke-width: 1.8; }
      .crsa-card-geo       { fill: #fff; stroke: rgba(214, 142, 41, 0.85); stroke-width: 1.8; }
      .crsa-card-failover  { fill: #fff; stroke: rgba(170, 70, 70, 0.85); stroke-width: 1.8; }
      .crsa-pick           { fill: rgba(46, 138, 90, 0.12); stroke: rgba(46, 138, 90, 0.9); stroke-width: 2.2; }
      .crsa-dead           { fill: rgba(200, 200, 200, 0.15); stroke: rgba(130, 130, 130, 0.8); stroke-width: 1.6; stroke-dasharray: 4 3; }
      .crsa-gate           { fill: #fff; stroke: #666; stroke-width: 1.3; stroke-dasharray: 4 3; }
      .crsa-col-title      { font-size: 17px; font-weight: 700; fill: #222; }
      .crsa-col-sub        { font-size: 12px; fill: #555; }
      .crsa-workload       { font-size: 14px; font-weight: 600; fill: #222; }
      .crsa-detail         { font-size: 12px; fill: #333; }
      .crsa-gate-text      { font-size: 12px; fill: #333; font-style: italic; }
      .crsa-pick-label     { font-size: 15px; font-weight: 700; fill: #222; }
      .crsa-pick-sub       { font-size: 12px; fill: #333; }
      .crsa-fail           { font-size: 13px; font-weight: 700; fill: #963; }
      .crsa-fail-red       { font-size: 13px; font-weight: 700; fill: #a33; }
      .crsa-arrow          { fill: none; stroke: #555; stroke-width: 1.8; }
      .crsa-arrow-dead     { fill: none; stroke: #a55; stroke-width: 1.6; stroke-dasharray: 3 3; }
    &lt;/style&gt;
    &lt;marker id=&quot;crsa-arrowhead&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#555&quot; /&gt;
    &lt;/marker&gt;
    &lt;marker id=&quot;crsa-arrowhead-dead&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;
      &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#a55&quot; /&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;

  &lt;!-- Column backgrounds --&gt;
  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;crsa-bg-latency&quot; /&gt;
  &lt;rect x=&quot;380&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;crsa-bg-geo&quot; /&gt;
  &lt;rect x=&quot;740&quot; y=&quot;20&quot; width=&quot;340&quot; height=&quot;600&quot; rx=&quot;10&quot; class=&quot;crsa-bg-failover&quot; /&gt;

  &lt;text x=&quot;190&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-col-title&quot;&gt;Latency-based&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-col-sub&quot;&gt;measures RTT to each AWS Region&lt;/text&gt;

  &lt;text x=&quot;550&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-col-title&quot;&gt;Geolocation&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-col-sub&quot;&gt;picks on continent / country code&lt;/text&gt;

  &lt;text x=&quot;910&quot; y=&quot;55&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-col-title&quot;&gt;Failover&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-col-sub&quot;&gt;primary / secondary, N = 2&lt;/text&gt;

  &lt;!-- Policy cards --&gt;
  &lt;rect x=&quot;50&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;crsa-card-latency&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-workload&quot;&gt;Alias records, one per Region&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;3 Regions, 3 alias targets&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;EvaluateTargetHealth = true&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;no separate health checks&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;crsa-card-geo&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-workload&quot;&gt;Continent / country rules&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;+ mandatory default record&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;Istanbul → AS (IANA) → APAC&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;even when eu-west-1 is faster&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;100&quot; width=&quot;280&quot; height=&quot;90&quot; rx=&quot;6&quot; class=&quot;crsa-card-failover&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-workload&quot;&gt;Primary + secondary&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;exactly 2 records&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;Sydney user → us-east-1 @ 220 ms&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;183&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-detail&quot;&gt;no locality concept&lt;/text&gt;

  &lt;path d=&quot;M190,190 L190,220&quot; class=&quot;crsa-arrow&quot; marker-end=&quot;url(#crsa-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M550,190 L550,220&quot; class=&quot;crsa-arrow&quot; marker-end=&quot;url(#crsa-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,190 L910,220&quot; class=&quot;crsa-arrow&quot; marker-end=&quot;url(#crsa-arrowhead)&quot; /&gt;

  &lt;!-- Gate 1: locality --&gt;
  &lt;rect x=&quot;50&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;crsa-gate&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-gate-text&quot;&gt;Picks on measured latency? yes&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;crsa-gate&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-gate-text&quot;&gt;Picks on continent code, not RTT&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;220&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;crsa-gate&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;247&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-gate-text&quot;&gt;No locality awareness&lt;/text&gt;

  &lt;path d=&quot;M190,264 L190,294&quot; class=&quot;crsa-arrow&quot; marker-end=&quot;url(#crsa-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M550,264 L550,470&quot; class=&quot;crsa-arrow-dead&quot; marker-end=&quot;url(#crsa-arrowhead-dead)&quot; /&gt;
  &lt;path d=&quot;M910,264 L910,294&quot; class=&quot;crsa-arrow&quot; marker-end=&quot;url(#crsa-arrowhead)&quot; /&gt;

  &lt;!-- Gate 2: health --&gt;
  &lt;rect x=&quot;50&quot; y=&quot;294&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;crsa-gate&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;321&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-gate-text&quot;&gt;Health-aware via ALB alias? yes&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;294&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;crsa-gate&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;321&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-gate-text&quot;&gt;Health-aware? yes (primary only)&lt;/text&gt;

  &lt;path d=&quot;M190,338 L190,368&quot; class=&quot;crsa-arrow&quot; marker-end=&quot;url(#crsa-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,338 L910,368&quot; class=&quot;crsa-arrow&quot; marker-end=&quot;url(#crsa-arrowhead)&quot; /&gt;

  &lt;!-- Gate 3: N &gt;= 3 --&gt;
  &lt;rect x=&quot;50&quot; y=&quot;368&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;crsa-gate&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;395&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-gate-text&quot;&gt;Supports 3+ records? yes&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;368&quot; width=&quot;280&quot; height=&quot;44&quot; rx=&quot;22&quot; class=&quot;crsa-gate&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;395&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-gate-text&quot;&gt;Structurally 2 records only&lt;/text&gt;

  &lt;path d=&quot;M190,412 L190,442&quot; class=&quot;crsa-arrow&quot; marker-end=&quot;url(#crsa-arrowhead)&quot; /&gt;
  &lt;path d=&quot;M910,412 L910,470&quot; class=&quot;crsa-arrow-dead&quot; marker-end=&quot;url(#crsa-arrowhead-dead)&quot; /&gt;

  &lt;!-- Final outcome cards --&gt;
  &lt;rect x=&quot;50&quot; y=&quot;442&quot; width=&quot;280&quot; height=&quot;158&quot; rx=&quot;8&quot; class=&quot;crsa-pick&quot; /&gt;
  &lt;text x=&quot;190&quot; y=&quot;470&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-label&quot;&gt;Latency + alias + EvalTargetHealth&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;494&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;one record set, three regions&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;514&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;no separate health-check resources&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;534&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;cascade: closest healthy Region wins&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;558&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;empty-set fallback returns all&lt;/text&gt;
  &lt;text x=&quot;190&quot; y=&quot;582&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;ready to nest if needs grow&lt;/text&gt;

  &lt;rect x=&quot;410&quot; y=&quot;442&quot; width=&quot;280&quot; height=&quot;158&quot; rx=&quot;8&quot; class=&quot;crsa-dead&quot; /&gt;
  &lt;text x=&quot;550&quot; y=&quot;470&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-fail&quot;&gt;Fails locality&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;494&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;continent code ≠ latency&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;514&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;Istanbul → APAC by IANA&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;538&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;failover falls to default only&lt;/text&gt;
  &lt;text x=&quot;550&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;not the next-nearest neighbour&lt;/text&gt;

  &lt;rect x=&quot;770&quot; y=&quot;442&quot; width=&quot;280&quot; height=&quot;158&quot; rx=&quot;8&quot; class=&quot;crsa-dead&quot; /&gt;
  &lt;text x=&quot;910&quot; y=&quot;470&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-fail-red&quot;&gt;Fails N ≥ 3&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;494&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;primary + secondary only&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;514&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;no concept of &quot;third-closest&quot;&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;538&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;useful inside latency (nested)&lt;/text&gt;
  &lt;text x=&quot;910&quot; y=&quot;562&quot; text-anchor=&quot;middle&quot; class=&quot;crsa-pick-sub&quot;&gt;not on its own here&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption style=&quot;font-size: 0.9em; color: var(--color-ink-secondary);&quot;&gt;Each candidate falls out of the funnel on a different attribute. Latency-based with alias records and EvaluateTargetHealth is the one that reaches the bottom intact.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;latency-based-routing-in-depth&quot;&gt;Latency-based routing, in depth&lt;/h3&gt;

&lt;p&gt;What Route 53 calls “latency” is not the latency from the resolver to the ALB. It’s the latency from Route 53’s own measurement points to each AWS Region, not to the service, not to the ALB, to the Region. When a resolver queries, Route 53 looks up which Region has the lowest RTT for that resolver’s network position (based on its IP and Route 53’s internal map of measurement points) and returns the record pointing at that Region.&lt;/p&gt;

&lt;p&gt;Two practical consequences. First, the latency readings are independent of the application’s performance. If the ALB is slow but the Region’s network paths are fast, Route 53 still treats the Region as fast, the health signal is what compensates. Second, the measurements cover AWS’s public-internet paths to its edge locations and Regions, which are the ones that matter for &lt;em&gt;reaching&lt;/em&gt; the Region; per-service latency inside the Region is a separate problem.&lt;/p&gt;

&lt;p&gt;The health-awareness wiring is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EvaluateTargetHealth&lt;/code&gt; on an alias record. Aliases are an AWS extension to DNS that let a record point directly at an AWS resource. ALB, NLB, CloudFront distribution, S3 website endpoint, another Route 53 record, instead of a hard-coded IP. When a latency record is an alias to an ALB with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EvaluateTargetHealth = true&lt;/code&gt;, Route 53 consults the ALB’s own health signal as part of the routing decision. For an ALB, “healthy” means at least one target is healthy in at least one of the ALB’s target groups. The ALB already knows this; there is nothing new to configure.&lt;/p&gt;

&lt;p&gt;Compared to the alternative, a separate Route 53 health check pointing at an HTTP endpoint or IP, alias + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EvaluateTargetHealth&lt;/code&gt; is cheaper, has one fewer moving part, and can’t drift out of sync with the actual backend health the way that a separately-configured health check can.&lt;/p&gt;

&lt;p&gt;When the latency-preferred record’s alias target reports unhealthy, Route 53 excludes it from the candidate set for the response and falls through to the next-lowest-latency record whose target &lt;em&gt;is&lt;/em&gt; healthy. No client retry, no application-level awareness of the failover, no DNS record change from the administrator’s side.&lt;/p&gt;

&lt;p&gt;One last-resort behaviour worth knowing. If every record in the set is unhealthy, Route 53 does not return an empty answer. It returns all of them, regardless of health. The reasoning is that a broken DNS response (NXDOMAIN or empty answer set) is strictly worse than a long-shot answer, the client might still succeed via retry, via a health check that’s lagging reality, or via an in-flight recovery. “Try something” beats “refuse to answer.”&lt;/p&gt;

&lt;h3 id=&quot;a-worked-example-madrid-through-four-states&quot;&gt;A worked example: Madrid through four states&lt;/h3&gt;

&lt;p&gt;Madrid resolver, DNS TTL of 60 seconds on the latency record set.&lt;/p&gt;

&lt;p&gt;State 1, all three regions healthy. Route 53 evaluates candidates: eu-west-1 (~28 ms), us-east-1 (~95 ms), ap-southeast-2 (~210 ms). All three alias targets healthy. Response: eu-west-1. Client connects at ~28 ms.&lt;/p&gt;

&lt;p&gt;State 2, eu-west-1 ALB has no healthy targets. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EvaluateTargetHealth = true&lt;/code&gt; on the eu-west-1 record excludes it from the candidate set. Next-lowest healthy: us-east-1. Response: us-east-1. Client connects at ~95 ms. End-to-end cutover is roughly the DNS TTL (60 s) plus Route 53’s internal health-propagation delay (~30 s).&lt;/p&gt;

&lt;p&gt;State 3, eu-west-1 and us-east-1 both unhealthy. Candidate set: only ap-southeast-2. Response: ap-southeast-2. Madrid connects at ~210 ms. Painful, but the application is intact.&lt;/p&gt;

&lt;p&gt;State 4, all three ALBs unhealthy. Candidate set empty. Route 53 returns all three records regardless of health. The client receives three addresses. First attempt fails; resolver behaviour varies from there. The point is the system doesn’t hand out DNS failures when the whole routing set is down.&lt;/p&gt;

&lt;h3 id=&quot;where-this-nests&quot;&gt;Where this nests&lt;/h3&gt;

&lt;p&gt;Route 53 supports up to ten levels of nesting, and two-level patterns show up in the same scenario shape repeatedly. Latency → Failover puts a per-Region active/passive inside each latency leg, one record set giving “closest region AND in-region active/passive”. Geolocation → Latency pins GDPR-scoped users to the EU continent, then latency-selects among EU Regions inside that rule. Weighted → Latency runs a 10% canary globally with locality preserved inside both cohorts.&lt;/p&gt;

&lt;p&gt;The useful skill is spotting the primary axis (the one the scenario optimises on) and the secondary axis (the one it constrains on). Once those are clear, the nesting writes itself.&lt;/p&gt;

&lt;h3 id=&quot;whats-worth-remembering&quot;&gt;What’s worth remembering&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Seven routing policies exist, simple, weighted, latency, geolocation, geoproximity, failover, multi-value answer. Each optimises a different axis and most real setups nest two or more.&lt;/li&gt;
  &lt;li&gt;“Latency” means Route 53’s measured RTT from its probes to each AWS Region, not resolver-to-ALB, not physical distance. Continent-code geolocation is not a substitute.&lt;/li&gt;
  &lt;li&gt;Alias + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EvaluateTargetHealth&lt;/code&gt; is the lightweight health-awareness path. It reuses the ALB’s own view of target health instead of a separately-configured Route 53 health-check resource.&lt;/li&gt;
  &lt;li&gt;Unhealthy records drop out of the candidate set silently. Route 53 falls through to the next-lowest-latency healthy record with no DNS edit and no client retry.&lt;/li&gt;
  &lt;li&gt;When every record is unhealthy, Route 53 returns them all. Empty-set fallback is the least-worst choice, a long-shot answer beats NXDOMAIN.&lt;/li&gt;
  &lt;li&gt;Nesting goes up to ten levels deep. Latency-outer with Failover-inner is a common two-level shape for “closest region AND in-region active/passive”.&lt;/li&gt;
  &lt;li&gt;DNS TTL plus propagation sets the cutover floor. A 60-second TTL and Route 53’s ~30-second propagation means clients cache the old answer for up to a minute and a half.&lt;/li&gt;
&lt;/ol&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Accessibility: A Product Decision, Not a Compliance Tick</title>
    <link href="/writing/accessibility-a-product-decision/"/>
    <updated>2026-04-21T06:00:00+08:00</updated>
    <id>/writing/accessibility-a-product-decision/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Greenbox has sixty-eight subscribers. The signup flow works, mostly. Two sprints into the new delivery, a subscriber called Helen sends a polite email that lands like a small earthquake under the kitchen table where Maya does her reading.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Helen’s email is three paragraphs long. The first paragraph thanks Maya for the boxes and mentions that her daughter-in-law recommended Greenbox. The second paragraph asks a practical question about substitutions. The third paragraph is the one Maya reads twice.&lt;/p&gt;

&lt;p&gt;“I should mention,” Helen writes, “that I’m registered blind. I use a screen reader called JAWS to use my computer. I managed to sign up to Greenbox, but it took me about forty minutes. The checkout form had some fields my screen reader couldn’t identify, and I had to guess what some of the buttons did. I got there in the end, but I wanted to let you know in case you can fix it for others. I’m not complaining; I’m glad I found you. Just letting you know.”&lt;/p&gt;

&lt;p&gt;Maya reads it a third time. Then she forwards it to Tom, Priya, and Lee with one line: “We need to talk about this.”&lt;/p&gt;

&lt;h3 id=&quot;the-monday-morning-conversation&quot;&gt;The Monday morning conversation&lt;/h3&gt;

&lt;p&gt;By Monday, Tom has opened the signup form on his own laptop, turned on VoiceOver, and tried to complete it with his eyes closed.&lt;/p&gt;

&lt;p&gt;He gets to the delivery frequency dropdown and stops. VoiceOver reads it as “button, pop-up button.” It doesn’t say &lt;em&gt;what&lt;/em&gt; the button is for. It doesn’t read the current selection. If Tom hadn’t known he was on the delivery frequency field, he would have no idea what was happening.&lt;/p&gt;

&lt;p&gt;He tries the substitution preferences checklist next. VoiceOver reads “checkbox, unchecked” three times in a row and then stops. There are twelve checkboxes on that page. Helen would have had to tab through all twelve without knowing what any of them were for.&lt;/p&gt;

&lt;p&gt;Tom puts his headphones down.&lt;/p&gt;

&lt;p&gt;“We failed her,” he says to Priya. “She got through it because she’s determined. Not because we did our job.”&lt;/p&gt;

&lt;p&gt;Priya has been reading the &lt;a href=&quot;https://www.w3.org/WAI/WCAG22/Understanding/&quot;&gt;Web Content Accessibility Guidelines&lt;/a&gt; on her laptop. “WCAG 2.2,” she says. “There are three conformance levels: A, AA, AAA. AA is the standard most regulators use. It covers perceivable, operable, understandable, and robust. Four principles, thirteen guidelines, seventy-eight success criteria.”&lt;/p&gt;

&lt;p&gt;“Seventy-eight.”&lt;/p&gt;

&lt;p&gt;“Some of them are easy. Colour contrast, alt text on images, form labels. Some of them are harder, like making sure that every interactive element works with a keyboard, or that the order the screen reader reads things in matches the visual order. The hard ones are the ones we’re failing.”&lt;/p&gt;

&lt;p&gt;Maya is listening from the other end of the kitchen. She asks the question she always asks when she’s trying to decide how seriously to take something. “What does it cost to fix?”&lt;/p&gt;

&lt;h3 id=&quot;compliance-or-product&quot;&gt;Compliance or product?&lt;/h3&gt;

&lt;p&gt;This is the moment the conversation could go two different ways.&lt;/p&gt;

&lt;p&gt;One version: Greenbox treats accessibility as a compliance checklist. Tom and Priya spend a week grinding through the WCAG criteria, ticking boxes, running automated scans with &lt;a href=&quot;https://www.deque.com/axe/&quot;&gt;axe&lt;/a&gt; and &lt;a href=&quot;https://developer.chrome.com/docs/lighthouse/overview/&quot;&gt;Lighthouse&lt;/a&gt;, fixing the failures the scans flag. At the end of the week, the automated scans are green. They declare victory. They write a blog post about being WCAG AA compliant. Nobody tests with an actual screen reader again for the rest of the year.&lt;/p&gt;

&lt;p&gt;The other version: Greenbox treats accessibility as a product quality decision. The signup flow has to work for everyone, because every person who can’t complete signup is a subscriber Greenbox loses, and a person whose experience of Greenbox is worse than their experience of the shops. The team doesn’t just run automated scans. They test the flow with a screen reader, with keyboard-only navigation, with high-contrast mode, and with somebody whose vision is actually impaired.&lt;/p&gt;

&lt;p&gt;Maya has been in enough tech companies to know which version happens by default. She’s seen “WCAG AA compliant” stickers on websites that are unusable with a screen reader. The compliance checklist is seductive because it’s finite. You do the list, you’re done. The product quality framing is harder because there’s no finish line, just a commitment to keep checking.&lt;/p&gt;

&lt;p&gt;She picks the harder framing.&lt;/p&gt;

&lt;p&gt;“We’re not going to be WCAG AA compliant,” Maya says. “We’re going to be a subscription box that actually works for people who can’t see the box on the website. Those are different goals, and I want to be sure we know which one we’re solving.”&lt;/p&gt;

&lt;p&gt;Tom nods slowly. He can feel the difference in his stomach. Compliance is a sprint: a week of furious fixing, then done. Product quality is an orientation, something you carry into every future sprint.&lt;/p&gt;

&lt;h3 id=&quot;what-works-for-helen-looks-like&quot;&gt;What “works for Helen” looks like&lt;/h3&gt;

&lt;p&gt;Lee takes out a marker and goes to the whiteboard. “Okay. If we’re going to treat this as a product decision, we need to be specific about what we’re deciding. What does ‘works for Helen’ mean?”&lt;/p&gt;

&lt;p&gt;The team spends an hour working through it. The outcome isn’t a WCAG checklist; it’s a set of user story map updates, concrete journey steps that have to work regardless of how the subscriber is accessing the site.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A subscriber who cannot see the screen can sign up, choose a box, set preferences, and complete checkout using only a screen reader, without asking for help.&lt;/li&gt;
  &lt;li&gt;A subscriber who cannot use a mouse can do the same using only a keyboard.&lt;/li&gt;
  &lt;li&gt;A subscriber with low vision can read every piece of content on the site with the browser’s text size set to 200%.&lt;/li&gt;
  &lt;li&gt;A subscriber who is colour-blind can distinguish every button, status indicator, and error message without relying on colour alone.&lt;/li&gt;
  &lt;li&gt;A subscriber with cognitive difficulties can understand the delivery schedule, the substitution rules, and the cancellation process on first reading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these feeds back into the user story map as a row of slices that cut across every feature. They’re not a separate “accessibility backlog” to be done later. They’re the same stories the team has already mapped, with a new dimension added.&lt;/p&gt;

&lt;p&gt;“This is how we avoid the compliance trap,” Lee says. “Accessibility isn’t a feature; it’s a quality attribute of every feature. If we build a new delivery scheduler, it has to work for Helen. If we build a new substitution flow, it has to work for Helen. We don’t ship a feature unless it works for Helen.”&lt;/p&gt;

&lt;p&gt;Tom writes “Helen” on a sticky note and puts it next to the Definition of Done on the team’s process wall. Next to “passes tests,” “code reviewed,” and “deployed to staging,” they add “tested with keyboard and screen reader.”&lt;/p&gt;

&lt;p&gt;It doesn’t fix everything. The existing signup flow still has the problems Helen described. Tom and Priya spend the rest of the week going through it systematically, not by running automated scans, but by closing their eyes and trying to use it. They find things the scans missed. The checkout button is a styled &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;div&amp;gt;&lt;/code&gt; instead of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, so screen readers don’t announce it as clickable. The error messages appear as text near the broken field, but the screen reader doesn’t read them when the field is focused. The delivery day picker is a custom component that traps keyboard focus.&lt;/p&gt;

&lt;p&gt;They fix each one. Priya writes a short internal doc, &lt;em&gt;Greenbox Accessibility Standards&lt;/em&gt;, that lists the decisions they’ve made and why. It has four sections: semantic HTML first, keyboard navigation always, visible focus indicators, and test with actual assistive technology. It’s shorter than the WCAG spec, and more useful, because it’s specific to the decisions they make every day.&lt;/p&gt;

&lt;h3 id=&quot;the-email-back-to-helen&quot;&gt;The email back to Helen&lt;/h3&gt;

&lt;p&gt;On Friday, Maya sends Helen a reply.&lt;/p&gt;

&lt;p&gt;“Helen, thank you for writing to us. Your email changed how we’re building Greenbox. This week, Tom and Priya went through the entire signup flow with a screen reader and fixed the problems you described. We’ve also added keyboard and screen reader testing to our Definition of Done, which means every new feature we ship has to work before it goes live. I’d love to send you a free box as an apology for the forty minutes, and to thank you for teaching us something we should have known. If you’re willing, we’d also love to ask you a few questions about the rest of the site, not to interrogate you, but because you’ll notice things we won’t.”&lt;/p&gt;

&lt;p&gt;Helen writes back within the hour. She says yes to the box, and yes to the questions. She adds: “Most of the time when I tell a company their site is broken for me, they apologise and nothing changes. Thank you for being different.”&lt;/p&gt;

&lt;p&gt;Maya pins the email to the wall next to the sticky note with Helen’s name on it.&lt;/p&gt;

&lt;h3 id=&quot;why-this-matters-beyond-helen&quot;&gt;Why this matters beyond Helen&lt;/h3&gt;

&lt;p&gt;Here’s the thing about building the product this way: it doesn’t just benefit Helen. The changes Tom and Priya made also benefit:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The subscriber who has broken their arm and is navigating the site one-handed with a keyboard.&lt;/li&gt;
  &lt;li&gt;The subscriber on a train with a bad connection and a small phone screen.&lt;/li&gt;
  &lt;li&gt;The subscriber whose first language isn’t English, who uses a screen reader to slow down the text.&lt;/li&gt;
  &lt;li&gt;The subscriber who is sixty-eight and wears reading glasses and needs the text bigger than the default.&lt;/li&gt;
  &lt;li&gt;The subscriber whose ADHD makes it hard to parse a cluttered interface and needs clear headings and clean structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a well-known result in accessibility research: building for the edges improves the middle. Captions were designed for deaf users and are now used by everyone watching TV in a noisy pub. Kerb cuts were designed for wheelchair users and now serve parents with prams, cyclists, and people wheeling suitcases. The term for this is the &lt;a href=&quot;https://ssir.org/articles/entry/the_curb_cut_effect&quot;&gt;curb-cut effect&lt;/a&gt;, and it’s real and measurable.&lt;/p&gt;

&lt;p&gt;Maya didn’t know the term when she made the decision. She just knew that Helen was a subscriber, Helen had been failed, and the right response wasn’t a compliance sticker.&lt;/p&gt;

&lt;h3 id=&quot;the-lesson-maya-writes-down&quot;&gt;The lesson Maya writes down&lt;/h3&gt;

&lt;p&gt;In her notebook, the one she uses to capture the decisions she wants to remember. Maya writes:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Accessibility is a product quality decision. The compliance frame makes it finite and lets you declare victory. The product frame makes it ongoing and lets you keep improving. Choose the product frame. The cost is real but small. The benefit is that every subscriber can actually use what you built. That is the whole point of building something.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;She closes the notebook. The email from Helen is still pinned to the wall.&lt;/p&gt;

&lt;p&gt;Next to it, she adds a second sticky note. On it, in her careful handwriting: &lt;em&gt;Helen is a subscriber. Every subscriber matters. Every subscriber counts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It isn’t a WCAG criterion; it’s better.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>What Day Is It?</title>
    <link href="/writing/what-day-is-it/"/>
    <updated>2026-04-20T06:00:00+08:00</updated>
    <id>/writing/what-day-is-it/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;&lt;a href=&quot;/writing/what-time-is-it/&quot;&gt;What Time Is It?&lt;/a&gt; dealt with the hour, a fragile compromise between the sun and politics. The date next to it is fragile too, built from a different cast of characters: monks miscalculating epochs, popes deleting weeks, traders trying to align their working days with their trading partners, and software developers stuck with whichever epoch their operating system chose decades ago.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-fundamental-problem&quot;&gt;The fundamental problem&lt;/h3&gt;

&lt;p&gt;Calendars are hard for the same reason time zones are hard: the universe didn’t supply round numbers.&lt;/p&gt;

&lt;p&gt;The Earth’s orbital period is roughly 365.2422 days. The lunar month is roughly 29.5306 days. Neither divides neatly into the other, and neither divides neatly into a day. Every calendar humanity has ever built is a compromise, some prioritise the sun, some the moon, some try to honour both, and a few just give up and count days from a fixed point.&lt;/p&gt;

&lt;p&gt;There is no clean answer. There is only a choice about what to round, what to ignore, and what to patch with the occasional extra day or month bolted on.&lt;/p&gt;

&lt;h3 id=&quot;the-gregorian-calendar-and-the-eleven-missing-days&quot;&gt;The Gregorian calendar and the eleven missing days&lt;/h3&gt;

&lt;p&gt;The Gregorian calendar, the one most of the world uses for civil purposes, counts years from an epoch chosen in the 6th century by a monk named Dionysius Exiguus, who was trying to calculate the birth year of Jesus and got it wrong by several years. Modern scholarship places the actual birth somewhere between 6 and 4 BCE, which means the year on your phone is off by half a decade from the event it’s nominally counting from. We are stuck with Dionysius’s guess, because nobody is going to renumber every document, gravestone, and database in the world to fix it.&lt;/p&gt;

&lt;p&gt;The Gregorian calendar uses a solar year: 365 days, with a leap day every four years, except every hundred years, except every four hundred years. So 1900 was not a leap year, but 2000 was. The rule keeps the calendar aligned with the seasons to within roughly one day every 3,236 years, close enough that nobody currently alive needs to worry about the next correction.&lt;/p&gt;

&lt;p&gt;It was introduced by Pope Gregory XIII in 1582 to fix the drift of the Julian calendar, which had been gaining roughly three days every four hundred years against the seasons. The fix required a one-time correction: ten days were simply &lt;em&gt;deleted&lt;/em&gt;. In the countries that adopted the new calendar in 1582, October 4 was followed by October 15. Ten days that never happened.&lt;/p&gt;

&lt;p&gt;The switchover was not smooth. Catholic countries adopted it immediately. Protestant and Orthodox countries dragged their feet for centuries. Britain didn’t switch until 1752, by which point the discrepancy had grown to eleven days. September 2, 1752 was followed by September 14. Rents, wages, and birthdays had to be renegotiated. There’s a persistent legend that mobs rioted in the streets shouting “give us back our eleven days!”, the truth is more prosaic; the political turmoil was real but largely quiet.&lt;/p&gt;

&lt;p&gt;Russia held out until 1918. Greece until 1923. For centuries, different countries were on different dates &lt;em&gt;at the same time&lt;/em&gt;. The October Revolution? It happened on 25 October by Russia’s Julian calendar, 7 November by the Gregorian calendar everyone else was using. An October revolution that happened in November.&lt;/p&gt;

&lt;h3 id=&quot;lunar-solar-lunisolar&quot;&gt;Lunar, solar, lunisolar&lt;/h3&gt;

&lt;p&gt;Beyond the Gregorian, the variety is dizzying.&lt;/p&gt;

&lt;p&gt;The Islamic (Hijri) calendar is purely lunar, 354 or 355 days per year, so its months rotate through the Gregorian seasons over a 33-year cycle. Ramadan slowly walks through the year, falling in summer for a while, then spring, then winter. Months traditionally begin when a crescent moon is &lt;em&gt;physically sighted&lt;/em&gt; by human observers. Not calculated, observed. Saudi Arabia and Morocco sometimes start Ramadan on different days because one country’s observers spotted the crescent and the other’s didn’t. The date of the most important month in the Islamic calendar is, in the strictest traditional sense, unknowable in advance. Software that has to schedule Islamic holidays falls back on calculated approximations and accepts that it will sometimes be a day off.&lt;/p&gt;

&lt;p&gt;The Hebrew and Chinese calendars are &lt;em&gt;lunisolar&lt;/em&gt;: lunar months adjusted with the occasional leap month to stay aligned with the solar year. The Hebrew calendar uses a 19-year cycle in which seven of the years contain an extra month. The Chinese calendar uses astronomical calculation to decide when to insert a leap month based on solar terms. The result is months that follow the moon and years that follow the sun, glued together by a rule that sounds simple and is anything but.&lt;/p&gt;

&lt;p&gt;The Hindu calendars, there are several regional variants, are also lunisolar but use different epoch dates and different rules. The Bengali calendar starts its year in mid-April. The Tamil calendar uses a 60-year cycle of named years. None of them agree with each other, and most have to be reconciled with the Gregorian calendar for civil purposes.&lt;/p&gt;

&lt;h3 id=&quot;the-year-itself-is-negotiable&quot;&gt;The year itself is negotiable&lt;/h3&gt;

&lt;p&gt;The number on your screen depends on which tradition you ask.&lt;/p&gt;

&lt;p&gt;The Ethiopian calendar runs seven to eight years behind the Gregorian, a result of using a different calculation for the Annunciation. Ethiopia entered the third millennium in 2007 by Western reckoning. The country celebrated. The Western press, already several years past its own millennium, mostly missed it.&lt;/p&gt;

&lt;p&gt;The Thai Buddhist calendar counts from the death of the Buddha in 543 BCE, which is why Thai expiry dates look like they’re from the future. A bottle of water bought in Bangkok in 2026 might be stamped with an expiry of 2570. The product hasn’t time-travelled. The calendar just starts somewhere else.&lt;/p&gt;

&lt;p&gt;The Juche calendar in North Korea counts from Kim Il-sung’s birth year (1912), introduced in 1997, three years after his death, retroactively renumbering the entire country’s history. The calendar exists alongside the Gregorian in official documents, with the Juche year cited first.&lt;/p&gt;

&lt;p&gt;The Hebrew calendar counts from a calculated date for the creation of the world. We’re currently in the year 5786 by that reckoning. The Islamic calendar counts from the Hijra. Muhammad’s migration from Mecca to Medina in 622 CE, placing us in the 1440s. The Republic of China calendar still in official use in Taiwan counts from the founding of the republic in 1912, making 2026 the year 115. None of these traditions is wrong. They are answering a slightly different question.&lt;/p&gt;

&lt;h3 id=&quot;calendars-without-numbers&quot;&gt;Calendars without numbers&lt;/h3&gt;

&lt;p&gt;Not all calendars count days at all in the way Western calendars do.&lt;/p&gt;

&lt;p&gt;Here in Western Australia, the Nyoongar people, the traditional custodians of the south-west, use &lt;a href=&quot;https://www.bom.gov.au/iwk/nyoongar/index.shtml&quot;&gt;six seasons&lt;/a&gt; based on ecological indicators rather than calendar dates. Djilba (first rains) starts when the first rains come, which might be August or September depending on the year. Bunuru (the hot, dry time) starts when the weather turns, not when February begins. You know what season you’re in by looking at the land, not at a calendar. It’s a fundamentally different relationship with time: not “what date is it?” but “what is country doing right now?” (“Country” in Aboriginal English means the land itself, the living landscape, not a nation state.) It’s a calendar that’s always in sync with the actual ecology, at the cost of being impossible to print on a wall planner.&lt;/p&gt;

&lt;p&gt;Other Indigenous calendars across Australia work similarly. The Yolngu people of Arnhem Land recognise six seasons based on wind direction, plant flowering, and animal behaviour. The D’harawal of the Sydney basin recognise six. None of them line up with the Gregorian quarters because the Gregorian quarters describe northern-hemisphere agriculture, not the actual rhythms of the southern continent.&lt;/p&gt;

&lt;h3 id=&quot;roman-counting-and-revolutionary-weeks&quot;&gt;Roman counting and revolutionary weeks&lt;/h3&gt;

&lt;p&gt;Not all calendars even count the same direction.&lt;/p&gt;

&lt;p&gt;Roman calendars counted backwards from fixed points in each month, the Kalends (the first), the Nones (around the fifth or seventh), and the Ides (around the thirteenth or fifteenth). Caesar was assassinated on the Ides of March. March 15, but a Roman would have referred to the day before that as “the day before the Ides” rather than “the fourteenth.” Days were named relative to the next landmark, not numbered absolutely.&lt;/p&gt;

&lt;p&gt;The Maya Long Count tracked elapsed days from a mythological creation date in 3114 BCE, using a base-20 system with one quirky base-18 layer. It generated the apocalypse hysteria around December 21, 2012, when the count rolled over from one &lt;em&gt;b’ak’tun&lt;/em&gt; to the next. The Maya themselves did not predict the world would end. They predicted the counter would tick. Western tabloids did the rest.&lt;/p&gt;

&lt;p&gt;The French Republican Calendar (1793-1805) was a deliberate attempt to scrub Christianity and royalty out of the year. It introduced a ten-day week, three weeks per month, twelve months of thirty days, plus five or six “complementary days” tacked on at the end of the year. Months got new poetic names, &lt;em&gt;Brumaire&lt;/em&gt; (mist), &lt;em&gt;Thermidor&lt;/em&gt; (heat), &lt;em&gt;Floreal&lt;/em&gt; (flowers). It was abolished after twelve years partly because workers only got one day off in ten, partly because nobody outside France used it, and partly because the calendar’s astronomical rules required astronomical observations from the Paris Observatory, which made it deeply impractical for shipping and trade.&lt;/p&gt;

&lt;p&gt;The Soviet Union tried something similar in 1929 with a five-day week, with workers divided into five colour-coded groups so that production could continue uninterrupted, one fifth of the workforce was always on rest. Family members in different colour groups never had a day off together. The experiment was abandoned within a few years.&lt;/p&gt;

&lt;h3 id=&quot;the-international-date-line-and-the-disappearing-day&quot;&gt;The International Date Line and the disappearing day&lt;/h3&gt;

&lt;p&gt;Once the world agreed on Greenwich as the prime meridian, an awkward consequence followed: somewhere on the opposite side of the world, the calendar date had to change. That somewhere is the International Date Line, which roughly follows the 180-degree meridian but zigzags wildly to avoid splitting countries.&lt;/p&gt;

&lt;p&gt;It’s not defined by any treaty. It’s a convention, and nations choose which side they sit on.&lt;/p&gt;

&lt;p&gt;The earliest time zone on Earth is UTC+14 (Kiribati’s Line Islands). The latest is UTC-12 (Baker Island and Howland Island, both uninhabited). The gap is 26 hours, which means any given calendar date exists somewhere on Earth for a total of &lt;em&gt;fifty hours&lt;/em&gt;. New Year’s Eve starts in Kiribati and finishes more than two days later, by clock time, somewhere in the Pacific.&lt;/p&gt;

&lt;p&gt;Kiribati earned its UTC+14 the hard way. Until 1995, the country straddled the date line: the western Gilbert Islands were on Tuesday while the eastern Line Islands were still on Monday. A government on the wrong side of its own date line struggles to function. Civil servants in the capital couldn’t telephone the eastern islands during normal business hours because the eastern islands were closed for the day before, or open for the day after. In 1995 the country redrew its time zone so the whole nation sat on the same side of the line, which meant the Line Islands skipped a day. December 30, 1994 simply did not happen there.&lt;/p&gt;

&lt;p&gt;Samoa did the reverse in 2011. For more than a century Samoa had been on the American side of the date line (UTC-11) because most of its 19th-century trade was with California. By 2011, most of its trade was with Australia and New Zealand, which were a full day ahead. The mismatch meant Samoan businesses had only four overlapping working days per week with their main partners. The government decided to jump across the line. December 29, 2011 was followed directly by December 31. Friday December 30, 2011 simply ceased to exist in Samoa. People born on December 30 in earlier years had no birthday that year. The country switched from UTC-11 to UTC+13.&lt;/p&gt;

&lt;p&gt;The neighbouring American Samoa, on the other hand, stayed where it was. The two Samoas are 100 kilometres apart and now live a full day apart by clock.&lt;/p&gt;

&lt;h3 id=&quot;when-computers-count-days&quot;&gt;When computers count days&lt;/h3&gt;

&lt;p&gt;Every operating system, every database, every programming language has had to make peace with all of this. The compromise is usually a fixed epoch, a reference moment from which time is counted as a single number (typically seconds or milliseconds), and a separate library that knows how to translate that number back into a human-readable date in a human-chosen calendar.&lt;/p&gt;

&lt;p&gt;The choices are deeply arbitrary.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Unix counts seconds from 1 January 1970 UTC. This is the most widely deployed epoch on Earth, inside almost every server, every smartphone, every embedded device. It was chosen because it was a recent round date when Unix was being designed, and nobody expected the choice to matter for very long. It will overflow a signed 32-bit integer in 2038, the Year 2038 Problem, also known as the Unix Millennium Bug, which is currently a slow-burning crisis for any 32-bit system that hasn’t been updated.&lt;/li&gt;
  &lt;li&gt;Windows uses 1 January 1601 (the start of the previous Gregorian 400-year cycle, chosen so that calendar arithmetic was simpler).&lt;/li&gt;
  &lt;li&gt;macOS Cocoa uses 1 January 2001.&lt;/li&gt;
  &lt;li&gt;GPS counts weeks from 6 January 1980. The week counter was originally 10 bits, which rolled over for the first time in 1999 and again in 2019. Receivers that didn’t handle the rollover started reporting times decades in the past.&lt;/li&gt;
  &lt;li&gt;NTP uses 1 January 1900. Its 32-bit second counter will roll over in 2036, two years before the Unix problem.&lt;/li&gt;
  &lt;li&gt;Excel uses 1 January 1900 as day 1, and famously believes 1900 was a leap year. It wasn’t, 1900 was divisible by 100 but not 400, so the Gregorian rule says no leap day. But Lotus 1-2-3 had the same bug, and Excel chose compatibility over correctness when Microsoft was trying to win the spreadsheet wars in the 1980s. That bug ships in every copy of Excel to this day. Any date arithmetic in Excel that crosses the (nonexistent) February 29, 1900 is silently wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern repeats. Every computer system makes a choice about its epoch, and every choice ages badly. If you store a date as “days since 1 January 1900” in 16 bits, you get to 2079 before you run out. If you store it as a Gregorian “year, month, day” triple, you’re fine for billions of years but you have to do calendar arithmetic every time you want to compute a duration. The choice between those two, a number, or a structured representation, is one of the oldest debates in software, and there is still no clean answer.&lt;/p&gt;

&lt;h3 id=&quot;so-what-day-is-it&quot;&gt;So what day is it?&lt;/h3&gt;

&lt;p&gt;The hour on your phone is a fragile compromise between the sun and politics. The date next to it is a fragile compromise between the moon, the sun, and several thousand years of calendar reformers, deleted weeks, regional epochs, and arbitrary numbering systems chosen by people who are now dead.&lt;/p&gt;

&lt;p&gt;Today’s date depends on:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Which calendar. Gregorian for civil purposes in most of the world, but Hebrew, Islamic, Chinese, Ethiopian, Thai, Juche, and dozens of others operate alongside it for religious or national use.&lt;/li&gt;
  &lt;li&gt;Which side of the date line. And whether the date line in your part of the world has moved recently.&lt;/li&gt;
  &lt;li&gt;Which epoch your computer was built on. And whether that epoch is about to overflow.&lt;/li&gt;
  &lt;li&gt;Whether your timezone has changed recently. Russia has reshuffled its time zones repeatedly. Samoa moved itself across the date line. Kiribati skipped a day. Each of those events makes “what day was it on the 30th of December 1994 in the eastern Line Islands?” a question with an unsatisfying answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ve now covered the human story of the hour and the day. But all of this has been about how we &lt;em&gt;agree&lt;/em&gt; on time. The next post asks what we’re actually &lt;em&gt;measuring&lt;/em&gt; when we count seconds at all. &lt;a href=&quot;/writing/ticks-or-tocks/&quot;&gt;Ticks or Tocks?&lt;/a&gt; is about the physics of the second, from quartz crystals to caesium atoms to optical lattice clocks that won’t lose a tick in the lifetime of the universe.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: Impact Mapping</title>
    <link href="/writing/the-workshop-impact-mapping/"/>
    <updated>2026-04-19T06:00:00+08:00</updated>
    <id>/writing/the-workshop-impact-mapping/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Impact Mapping connects deliverables to actors, behaviour change, and a measurable goal, so when you ship a feature you can tell whether it worked. &lt;a href=&quot;/writing/impact-mapping-connecting-work-to-goals/&quot;&gt;Connecting Work to Goals&lt;/a&gt; is the worked example; this post is the playbook.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;impact-mapping&quot;&gt;Impact Mapping&lt;/h3&gt;

&lt;p&gt;Impact Mapping traces a path from a measurable business goal, through the actors whose behaviour affects that goal, through the behaviour changes you want to cause, to the deliverables that might cause them, so the team can tell the difference between work that moves the number and work that just feels productive. The four columns answer &lt;em&gt;why&lt;/em&gt; (goal), &lt;em&gt;who&lt;/em&gt; (actors), &lt;em&gt;how&lt;/em&gt; (impacts, behaviour changes), &lt;em&gt;what&lt;/em&gt; (deliverables), in that order, left to right. Invented and named by Gojko Adzic in 2012 and sometimes called goal mapping or outcome mapping (though outcome mapping has a separate formal definition in international development that isn’t quite the same thing). Frequently confused with story mapping, story mapping lays out what a user does; Impact Mapping lays out what behaviour you want to change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; a facilitator, a product owner who owns the goal, one or two developers, a designer, and someone with unfiltered exposure to the actors (sales, support, ops). Four to six people, about ninety minutes.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; a four-column map (goal → actors → impacts → deliverables) with a prioritised path picked across it, a target metric and date written on the chosen deliverable so it lands as a bet, and a “not this quarter” list of everything that didn’t make the path.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; the start of a quarter or initiative when you need to decide what to build, or a backlog that’s drifted away from any measurable outcome. Not for sprint planning, and not when nobody can articulate a measurable business goal (fix the goal first, separately).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whats-it-for&quot;&gt;What’s It For&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;“Which of these features, if we built them tomorrow, would move the number we’re supposed to be moving this quarter?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Asked out loud in the right room, that question almost always lands like a flat stone hitting glass. The answer takes longer than anyone expects. Someone defends a feature by explaining why the customer asked for it. Someone else defends another by naming a competitor. A third person points to the roadmap. Nobody says, without hedging, &lt;em&gt;this one will move the number because this specific group of users will do this specific thing differently&lt;/em&gt;. The question doesn’t get answered; it gets re-framed until it can be.&lt;/p&gt;

&lt;p&gt;The backlog has drifted. Each individual feature connects to &lt;em&gt;something&lt;/em&gt; — a request, a rival, a hunch — but nothing connects the features to each other, and nothing connects the set to an outcome anyone is measuring. Reasonable things are infinite. Strategy is the shortlist of reasonable things that share a goal, and the shortlist has gone missing.&lt;/p&gt;

&lt;p&gt;Impact Mapping exists to make that shortlist visible. The wall shows the goal on the left and the work on the right, with the logic connecting them drawn explicitly in between. Work that can’t find a place on the wall can still be reasonable. It just doesn’t belong in this quarter.&lt;/p&gt;

&lt;p&gt;Reach for it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You’re starting a quarter, an initiative, or a new product line and need to decide what to build&lt;/li&gt;
  &lt;li&gt;The team has a list of features but no shared story about how they connect to business outcomes&lt;/li&gt;
  &lt;li&gt;Stakeholders are requesting features and nobody is asking &lt;em&gt;why&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;You need to say no to work and you want a defensible reason&lt;/li&gt;
  &lt;li&gt;The organisation is measuring an outcome and the team doesn’t know how their work connects to it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-its-not-for&quot;&gt;What It’s Not For&lt;/h3&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You already have a clear, shared understanding of the connection between goal and work&lt;/li&gt;
  &lt;li&gt;You’re planning at the sprint level — Impact Mapping is strategic; sprint planning is tactical&lt;/li&gt;
  &lt;li&gt;Nobody in the room can articulate a measurable business goal — solve that first, separately&lt;/li&gt;
  &lt;li&gt;The goal is imposed from above without buy-in — a mapping session with a fake goal is worse than no session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop a session that’s already started if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The goal can’t survive five minutes of scrutiny&lt;/li&gt;
  &lt;li&gt;Every impact the team writes is a feature in disguise&lt;/li&gt;
  &lt;li&gt;Key stakeholders aren’t in the room and the map depends on their actors&lt;/li&gt;
  &lt;li&gt;The disagreement about the goal is political — mapping through a fake goal produces a map nobody will use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stopping and fixing the goal is not failure. Running a session that produces an elegant map of the wrong thing is.&lt;/p&gt;

&lt;h3 id=&quot;inputs&quot;&gt;Inputs&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;One measurable, time-bound business goal, written on a card before the session starts. &lt;em&gt;“Increase weekly active subscribers from 200 to 500 by end of Q3.”&lt;/em&gt; Nothing else. If the goal isn’t concrete enough to fit on a card, the session isn’t ready to run.&lt;/li&gt;
  &lt;li&gt;Sticky notes in four colours: green (goal), yellow (actors), orange (impacts), blue (deliverables). A wall wide enough that all four columns can grow left-to-right without crowding.&lt;/li&gt;
  &lt;li&gt;A 90-minute slot with the right people in the room (see &lt;em&gt;Who’s Needed&lt;/em&gt;) and no interruptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the goal itself is unclear or the business model isn’t coherent, run &lt;a href=&quot;/writing/the-workshop-business-model-canvas/&quot;&gt;Business Model Canvas&lt;/a&gt; first — the Canvas sets the strategy that Impact Mapping then executes against. If the team doesn’t yet know what the system &lt;em&gt;does&lt;/em&gt;, &lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming&lt;/a&gt; comes first to surface that.&lt;/p&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;What lands on the wall at the end:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A four-column map — goal on the left, actors next, impacts next, deliverables on the right — with lines connecting each note to the column behind it.&lt;/li&gt;
  &lt;li&gt;A prioritised path: one actor, one impact, the smallest deliverable that might cause the impact, with a target metric, a target change, and a date written on the deliverable card. The deliverable is now a &lt;em&gt;bet&lt;/em&gt;: a hypothesis that this work will move that number by that much by that date. If the metric doesn’t move, you walk back up the map.&lt;/li&gt;
  &lt;li&gt;A “not this quarter” list: every deliverable that didn’t make the priority path. Just as valuable as the priority list, because it’s the work the team has explicitly agreed not to do yet.&lt;/li&gt;
  &lt;li&gt;A defensible answer to &lt;em&gt;“why are we building this”&lt;/em&gt; for every item on the priority path, traceable back to the goal through an actor and an impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Photograph the wall in panorama (good lighting, readable notes) plus one close-up shot per column. Mark the chosen path on the map — dot stickers, a pen line, a photograph with it circled.&lt;/p&gt;

&lt;p&gt;These outputs feed straight into:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt; — once Impact Mapping has chosen the deliverables, User Story Mapping lays out the user journey through them and slices it into releases.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt; — every impact on the map is an assumption. Assumption Mapping sorts which of those assumptions actually deserve a test run before the team commits to the deliverable.&lt;/li&gt;
  &lt;li&gt;Wardley Mapping — Impact Mapping tells you what to change; Wardley Mapping tells you where each component sits in its evolution and therefore how to change it. They compose well for strategic initiatives.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whos-needed&quot;&gt;Who’s Needed&lt;/h3&gt;

&lt;p&gt;Four to six people, about ninety minutes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Facilitator. Holds the shape of the tree (goal → actors → impacts → deliverables), keeps people from jumping columns, and calls out when someone has smuggled a deliverable into the actors column.&lt;/li&gt;
  &lt;li&gt;Product owner or business stakeholder. Mandatory. They own the goal. They’re the one who will defend it, refine it, or abandon it when the session reveals that the goal itself is the problem.&lt;/li&gt;
  &lt;li&gt;Developers. At least one, ideally two. They know what’s cheap and what’s expensive, which is what turns the deliverables column from wishful thinking into a triaged list.&lt;/li&gt;
  &lt;li&gt;Designers. They think about actor behaviour natively. In the impacts column — the hardest column — a designer’s framing (&lt;em&gt;“they would finish the sign-up flow instead of abandoning it at step three”&lt;/em&gt;) is usually sharper than a developer’s.&lt;/li&gt;
  &lt;li&gt;People who talk to the actors. Sales, support, operations, account managers. Whoever has the least-filtered view of the people whose behaviour you’re trying to change. They will contradict the optimistic assumptions in the room and that is exactly what they’re there to do.&lt;/li&gt;
  &lt;li&gt;SRE / Operations. For infrastructure or reliability initiatives, SRE &lt;em&gt;is&lt;/em&gt; the domain expert on actors and impacts — &lt;em&gt;“on-call engineers stop being paged for the billing cron”&lt;/em&gt; is a valid impact, and &lt;em&gt;“our customers stop opening support tickets about missed deliveries”&lt;/em&gt; is downstream of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Group size is 4–6. Impact Mapping is a thinking-aloud exercise and the room has to stay a conversation. Above six, it becomes a meeting with a whiteboard.&lt;/p&gt;

&lt;p&gt;Who to leave out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Large groups of stakeholders. If seven people need to shape the goal, that’s a pre-session, not this session. Come to Impact Mapping with the goal agreed.&lt;/li&gt;
  &lt;li&gt;People who can’t say no. Someone who will accept every proposed deliverable without challenge makes the prioritisation phase impossible.&lt;/li&gt;
  &lt;li&gt;Pure spectators. Impact Mapping is not a presentation; observers change the dynamic and absorb oxygen without contributing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;how-to-run-it&quot;&gt;How To Run It&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Duration&lt;/th&gt;
      &lt;th&gt;Notes colour&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Orient on the goal&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;Green (one card)&lt;/td&gt;
      &lt;td&gt;“What are we trying to move?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Actors&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;Yellow&lt;/td&gt;
      &lt;td&gt;“Whose behaviour affects the goal?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Impacts&lt;/td&gt;
      &lt;td&gt;25 min&lt;/td&gt;
      &lt;td&gt;Orange&lt;/td&gt;
      &lt;td&gt;“How would their behaviour change?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Deliverables&lt;/td&gt;
      &lt;td&gt;20 min&lt;/td&gt;
      &lt;td&gt;Blue&lt;/td&gt;
      &lt;td&gt;“What could we do to cause that change?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Prioritise&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;Marks on the map&lt;/td&gt;
      &lt;td&gt;“What’s the highest-leverage path?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Wrap-up&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;“Who owns what next?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;~90 minutes&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The map grows left-to-right, one column per phase. Skipping columns is the single most common failure mode. An impact without an actor is a feature. A deliverable without an impact is a hunch. The discipline of the four columns is the technique.&lt;/p&gt;

&lt;p&gt;Impact Mapping alternates between open conversation and quiet placement. The goal is fixed; everything to the right of it is debatable. The key rhythm is work backwards — goal before actor, actor before impact, impact before deliverable. Any deliverable that appears before its impact gets politely moved into a holding area until someone can connect it.&lt;/p&gt;

&lt;h4 id=&quot;phase-1-orient-on-the-goal-10-minutes&quot;&gt;Phase 1. Orient on the goal (10 minutes)&lt;/h4&gt;

&lt;p&gt;Put the goal card on the far left of the wall. Read it aloud:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Our goal for this quarter is to increase weekly active subscribers from 200 to 500 by end of Q3. That’s on the wall. We are not here to debate whether this is the right goal. We are here to map how we could move it.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then make sure everyone understands it the same way:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Before we go any further, what does ‘weekly active’ mean here? What counts as a subscriber? If someone paused mid-July, are they in or out of the 500?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Often a five-minute clarification at this stage reveals that the goal is ambiguous, and the map would have split into three directions based on three different interpretations. Resolve it now. If you can’t, the session isn’t ready.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The goal isn’t measurable. &lt;em&gt;“Grow the business.”&lt;/em&gt; The session cannot proceed. End it and schedule a goal-setting conversation.&lt;/li&gt;
  &lt;li&gt;The goal is actually three goals. &lt;em&gt;“Grow subscribers and reduce churn and increase average box size.”&lt;/em&gt; Pick one for this session. Run the others separately.&lt;/li&gt;
  &lt;li&gt;Silent disagreement. The goal is on the card but one person clearly doesn’t believe it. Surface it: &lt;em&gt;“You look sceptical. Is the goal wrong or is it the number?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-2-actors-15-minutes&quot;&gt;Phase 2. Actors (15 minutes)&lt;/h4&gt;

&lt;p&gt;Ask the room:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Whose behaviour, if it changed, would affect this goal? I want names or roles, not ‘users.’ Specific enough that we could identify them in our database or watch them at their desk.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The team writes actors on yellow notes and places them in the second column. Actors can be external (subscribers, prospects, referrers, suppliers, journalists) or internal (support agents, warehouse staff, operations). They can be automated (the billing cron, the churn-prediction model, the weekly newsletter). Automated actors are first-class here — a scheduled job that sends the wrong email affects the goal exactly as much as a person who does.&lt;/p&gt;

&lt;p&gt;Push for specificity:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“‘Subscribers’ is too broad. Which subscribers? First-month subscribers? Subscribers who’ve paused once and come back? Subscribers whose delivery day has changed in the last sixty days?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Different slices of subscribers have different behaviour, and the map gets useful when the slices are named.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Jumping to deliverables. &lt;em&gt;“We need a referral programme.”&lt;/em&gt; That’s a blue note. Pull it back: &lt;em&gt;“Who would use the referral programme? What behaviour would change? Start from the actor.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Forgetting internal actors. Teams focus on external customers and forget that their own support team, warehouse, or on-call engineer is an actor whose behaviour affects the goal.&lt;/li&gt;
  &lt;li&gt;Forgetting adversarial actors. Churning subscribers are actors. People who try the service and don’t convert are actors. Don’t only list the actors you want to help.&lt;/li&gt;
  &lt;li&gt;Too many actors. Above eight, the map becomes unreadable. Group the similar ones or focus on the actors most central to the goal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-3-impacts-25-minutes&quot;&gt;Phase 3. Impacts (25 minutes)&lt;/h4&gt;

&lt;p&gt;This is the hardest and most valuable phase. For each actor, ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“How could their behaviour change in a way that helps us hit the goal? I want verbs. What would they &lt;em&gt;do&lt;/em&gt; differently?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write impacts on orange notes. Each impact sits in column three, connected to its actor. Good impacts are behaviour changes, not features:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;“New visitors sign up on their first visit instead of leaving to think about it.”&lt;/em&gt; (good)&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“Existing subscribers tell one friend within their first month.”&lt;/em&gt; (good)&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“On-call engineers get paged fewer than twice per week for billing issues.”&lt;/em&gt; (good, SRE flavour)&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“We build a landing page with better copy.”&lt;/em&gt; (not an impact — that’s a deliverable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then flip it:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Now the negative version. How could their behaviour change in a way that &lt;em&gt;hurts&lt;/em&gt; the goal?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Negative impacts are where the defensive work lives and where the risks hide. They are usually where the biggest savings come from: preventing a bad behaviour is often cheaper than causing a good one.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Deliverables disguised as impacts. &lt;em&gt;“Subscribers use the mobile app”&lt;/em&gt; is a deliverable dressed up as a behaviour. The impact is &lt;em&gt;“subscribers manage their subscription on the go”&lt;/em&gt;; the app is one possible deliverable.&lt;/li&gt;
  &lt;li&gt;Vague impacts. &lt;em&gt;“Subscribers are happier.”&lt;/em&gt; Not actionable. Push: &lt;em&gt;“What would a happier subscriber do differently? Stay longer? Refer? Upgrade? Complain less?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;One actor absorbing all the attention. Time-box each actor. You can come back if needed.&lt;/li&gt;
  &lt;li&gt;No negative impacts. Prompt directly: &lt;em&gt;“What could this actor do that would make the goal harder to hit?”&lt;/em&gt; If the answer is nothing, the actor probably doesn’t belong on the map.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-4-deliverables-20-minutes&quot;&gt;Phase 4. Deliverables (20 minutes)&lt;/h4&gt;

&lt;p&gt;For each impact, ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What could we build, do, write, or change to cause this behaviour? I want a list, not a single answer.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write deliverables on blue notes and place them in the fourth column, connected to their impact. A good deliverables column contains multiple options per impact, ordered roughly from cheapest to most ambitious:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Features (a referral programme, a pause flow, a rollback automation)&lt;/li&gt;
  &lt;li&gt;Content (a welcome sequence, a runbook, a one-pager for support)&lt;/li&gt;
  &lt;li&gt;Processes (a proactive call to at-risk subscribers, a handover checklist for on-call)&lt;/li&gt;
  &lt;li&gt;Experiments (a landing page, a prototype, a manual concierge version of the feature)&lt;/li&gt;
  &lt;li&gt;Changes to existing things (copy edits, configuration tweaks, prompt updates)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most valuable column in Impact Mapping is not the widest — it’s the one where cheap experiments live next to expensive builds. If every deliverable is a multi-month project, you’ve filled the column wrong.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Pet features appearing. Someone places a deliverable they’ve wanted to build but can’t connect to an impact. Challenge gently: &lt;em&gt;“Which impact does this serve? If we built it, whose behaviour would change?”&lt;/em&gt; If they can’t answer, park it.&lt;/li&gt;
  &lt;li&gt;Only big deliverables. Push for cheap ones: &lt;em&gt;“What’s the smallest thing we could do this week that would tell us whether the impact is real?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Duplicate deliverables. The same deliverable might serve multiple impacts. That’s a signal: draw lines to both. High-leverage deliverables are the ones that show up in multiple places.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-5-prioritise-10-minutes&quot;&gt;Phase 5. Prioritise (10 minutes)&lt;/h4&gt;

&lt;p&gt;Step back. Look at the whole map. You now have a visual argument from goal to work.&lt;/p&gt;

&lt;p&gt;Use it to pick the first path. Ask four questions in order:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Which actor has the most influence on this goal? Not the most numerous, the most influential.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Which impact is the highest-leverage one for that actor? If we only caused one behaviour change, which one would move the number most?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Which deliverable is the cheapest way to test whether we can actually cause that impact?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What measurable change in this impact would tell us the deliverable worked, and by when?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write the answer to the fourth question on the deliverable card, the target metric, the size of the change, the date. The deliverable is now a &lt;em&gt;bet&lt;/em&gt;: a hypothesis that this work will move that number by that much by that date. If the metric doesn’t move, you walk back up the map, maybe the deliverable was wrong, maybe the impact wasn’t what mattered. The map is a path of bets, not a plan of work.&lt;/p&gt;

&lt;p&gt;Mark the chosen path on the map — dot stickers, a pen line, a photograph with it circled. This is your first commitment. Everything else on the map is the second commitment, the third commitment, or “not this quarter.”&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Prioritising by excitement. The team gravitates toward the interesting technical deliverable rather than the high-leverage one. Redirect to the goal: &lt;em&gt;“Which of these moves the number most?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Trying to do everything. The map has twenty deliverables; the team wants to do all of them. Hold firm: &lt;em&gt;“Pick the top three. If they work, we come back for more.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Ignoring the map after voting. Someone argues for a deliverable that isn’t on the map. Either put it on the map properly (with actor and impact) or park it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;a-worked-example&quot;&gt;A worked example&lt;/h4&gt;

&lt;p&gt;See &lt;a href=&quot;/writing/impact-mapping-connecting-work-to-goals/&quot;&gt;Impact Mapping: Connecting Work to Goals&lt;/a&gt; for the Greenbox team’s first mapping session — including the moment they realise a feature they’ve been planning for six weeks doesn’t connect to any impact on the wall, and the relief of deciding not to build it.&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What Can Go Wrong&lt;/h3&gt;

&lt;p&gt;The goal debate. The team starts arguing about whether the goal is right.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“We can’t map a goal we don’t agree on. Let’s pause the session, fix the goal with leadership in the next forty-eight hours, and reconvene.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The disagreement is political. Mapping through a fake goal produces a map nobody will use.&lt;/p&gt;

&lt;p&gt;The solution-first thinker. Someone keeps proposing deliverables without connecting them to impacts.
  &lt;em&gt;Recovery:&lt;/em&gt; Give them a specific job: &lt;em&gt;“For every deliverable you think of, I need a yellow note and an orange note first. Who does it affect? What behaviour changes?”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; They can’t hold the shape after three prompts. Pair them with a designer for the rest of the session.&lt;/p&gt;

&lt;p&gt;Analysis paralysis. The team is stuck debating whether something is an actor, an impact, or a deliverable.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“The columns exist to structure thinking, not to be perfectly taxonomic. Put it in the best-fit column and move on.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The same argument happens on a third note. The team is avoiding the harder conversation; name it.&lt;/p&gt;

&lt;p&gt;The sceptic. Someone thinks the exercise is pointless because &lt;em&gt;“we already know what we’re building.”&lt;/em&gt;
  &lt;em&gt;Recovery:&lt;/em&gt; Ask them to place their planned work on the map. &lt;em&gt;“Take your top three items. Which actor? Which impact? Place them.”&lt;/em&gt; If they can’t connect the work to the goal through an actor and an impact, the exercise has just earned its keep and they usually become the most engaged participant in the room.
  &lt;em&gt;Stop if:&lt;/em&gt; They refuse to engage. They’re not blocking the session, just their own learning. Carry on without them.&lt;/p&gt;

&lt;p&gt;The everything-is-high-impact problem. Every impact the team writes feels like it moves the goal equally.
  &lt;em&gt;Recovery:&lt;/em&gt; Force a ranking: &lt;em&gt;“If we could only cause one of these impacts, which one? Now pretend I’ve taken that one away — which of the rest?”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The team genuinely can’t distinguish. The goal is probably too abstract to map against; sharpen it before continuing.&lt;/p&gt;

&lt;p&gt;The absent stakeholder. Halfway through, the team realises an actor is owned by someone not in the room.
  &lt;em&gt;Recovery:&lt;/em&gt; Put a pink note on that actor: &lt;em&gt;“Need to talk to [person] before we map this.”&lt;/em&gt; Carry on with the actors you can map.
  &lt;em&gt;Stop if:&lt;/em&gt; More than half the map depends on people who aren’t in the room. You’re storming without the right participants.&lt;/p&gt;

&lt;p&gt;Common failure modes to watch for across the whole session:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The map gets produced, photographed, and then ignored because the backlog keeps being the source of truth&lt;/li&gt;
  &lt;li&gt;The deliverables column is full of big builds and no cheap experiments&lt;/li&gt;
  &lt;li&gt;One actor absorbs the entire conversation and the rest of the map is thin&lt;/li&gt;
  &lt;li&gt;The team confuses “we wrote it down” with “we agreed on it” — absent stakeholders discover the map later and veto half of it&lt;/li&gt;
  &lt;li&gt;Impacts drift into features and nobody catches it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;

&lt;p&gt;The session ends; the work begins.&lt;/p&gt;

&lt;p&gt;Same day, the facilitator:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Takes panoramic photographs of the map. Good lighting, readable notes, one shot per column in close-up.&lt;/li&gt;
  &lt;li&gt;Transcribes the map into a shared document or a digital mind-mapping tool — goal, actors, impacts, deliverables, and the lines between them.&lt;/li&gt;
  &lt;li&gt;Writes a one-page summary message to participants and stakeholders: here’s the goal, here’s the prioritised path, here’s what we’re deferring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This week, the product owner:&lt;/p&gt;

&lt;p&gt;This is where the pattern earns its cost, and the work is mostly the product owner’s.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Turn the priority path into backlog items. Each deliverable on the priority path becomes a backlog item — but with the actor and impact captured in the description. When someone later asks “why are we building this,” the backlog item contains the answer.&lt;/li&gt;
  &lt;li&gt;Park the deferred deliverables explicitly. A “not this quarter” list is as valuable as the priority list. Put it somewhere visible. When new work gets proposed, the first question should be “does this displace something on the not-now list?”&lt;/li&gt;
  &lt;li&gt;Schedule discovery for the experiments. Cheap experiments from the deliverables column — landing pages, manual concierge runs, interview scripts — need to be started within a week of the session. If they sit, the map’s value decays fast.&lt;/li&gt;
  &lt;li&gt;Walk the map to absent stakeholders. Anyone who should have been in the room but wasn’t gets a walk-through. Their challenges either strengthen the map or reveal a problem you need to fix before committing.&lt;/li&gt;
  &lt;li&gt;Use the map to say no. This is the hardest and most important week-after task. The map gives you a defensible reason to refuse work that doesn’t connect. Use it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ongoing, the team:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Reviews the map at the start of each quarter or planning cycle. Has the goal changed? Have you learned which impacts actually work? Update and re-prioritise.&lt;/li&gt;
  &lt;li&gt;When someone proposes new work, asks them to place it on the map. If it doesn’t trace back to the goal through an actor and an impact, it probably isn’t worth doing — or the map needs to grow.&lt;/li&gt;
  &lt;li&gt;Keeps the photographed map visible where the team works. It’s the reference that prevents the slow drift back into feature-list thinking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The benefits compound when this becomes routine: work connected to outcomes instead of to opinions; a defensible answer to “why are we building this” for every item in the quarter; a visible short list, three deliverables picked out of twenty, that the team commits to first; a “not now” list that is just as valuable; a shared mental model of the business strategy that developers, designers, and product all recognise.&lt;/p&gt;

&lt;p&gt;The costs are real too: 6–9 person-hours per session with 4–6 people; a pre-session goal-setting conversation (sometimes the real work); political cost when the map reveals that work people wanted to do doesn’t connect to any goal; quarterly recurrence — the map goes stale as the goal, the actors, and the learnings move.&lt;/p&gt;

&lt;p&gt;Sibling sessions that often follow:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming&lt;/a&gt; — Event Storming describes how things happen now; Impact Mapping describes what you want to change. Run Impact Mapping first when you’re choosing what to build; run Event Storming first when you’re understanding what already exists.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt; — once Impact Mapping has chosen the deliverables, User Story Mapping lays out the user journey through them and slices it into releases.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt; — every impact on the map is an assumption. Assumption Mapping sorts which of those assumptions actually deserve a test run before the team commits to the deliverable.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-business-model-canvas/&quot;&gt;Business Model Canvas&lt;/a&gt; — when the goal itself is unclear or the business model isn’t coherent, the Canvas is the session to run before Impact Mapping. The Canvas sets the strategy; Impact Mapping executes against it.&lt;/li&gt;
  &lt;li&gt;Wardley Mapping — Impact Mapping tells you what to change; Wardley Mapping tells you where each component sits in its evolution and therefore how to change it. They compose well for strategic initiatives.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;variants&quot;&gt;Variants&lt;/h3&gt;

&lt;p&gt;Quarterly strategic mapping (default). One measurable goal, 4–6 people, ninety minutes, four columns built left-to-right with prioritisation at the end. Output: a priority path of bets and a “not this quarter” list. This is what most teams need, and the rest of this post describes it.&lt;/p&gt;

&lt;p&gt;Initiative or product-line mapping. A new product line, a major bet, or a discrete initiative. Same shape, but the goal sits at the initiative level rather than the quarter, and the session may run longer (two to three hours) because the actors and impacts are less familiar. Run it once at kick-off, refresh it every six to eight weeks.&lt;/p&gt;

&lt;p&gt;Infrastructure or reliability mapping. When the goal is operational (“reduce on-call pages by 50% by end of Q3,” “cut mean time to recovery from forty minutes to ten”), SRE takes the product owner’s seat and the actors column fills with on-call engineers, paging systems, support agents, and the customers downstream of incidents. The four-column shape is identical; the vocabulary shifts. Particularly useful when an SRE team needs to defend why reliability investment moves a business number.&lt;/p&gt;

&lt;p&gt;Remote. A Miro or Mural board with the four columns pinned, video call for the conversation. Slightly slower than the in-person rhythm, but the structure transfers cleanly. Use one shared cursor: only the facilitator places notes, prompted by the team, to keep the map legible. Take screenshots at the end of each phase rather than waiting for the close.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>User Story Mapping: Seeing the Whole</title>
    <link href="/writing/user-story-mapping-seeing-the-whole/"/>
    <updated>2026-04-18T06:00:00+08:00</updated>
    <id>/writing/user-story-mapping-seeing-the-whole/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/shipping-what-matters/&quot;&gt;Shipping What Matters&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;The Greenbox team has been busy. Event Storming gave them shared understanding. Example Mapping made their stories concrete. Impact Mapping connected their work to business goals.&lt;/p&gt;

&lt;p&gt;The result? Eighty-three stories in the backlog.&lt;/p&gt;

&lt;p&gt;Eighty-three. And climbing. Every discovery session has surfaced more work, more edge cases, more things that need building. Priya scrolls through the list on her screen and her face goes still. Tom exhales audibly.&lt;/p&gt;

&lt;p&gt;Lee looks at the list. “Eighty-three is a lot. How many are actually ready to build?”&lt;/p&gt;

&lt;p&gt;Tom scrolls. “Maybe… twenty? The rest are ideas, or things that came out of red cards, or stuff Maya mentioned once in a standup.”&lt;/p&gt;

&lt;p&gt;“Right. Part of what we’re going to do today isn’t just organise these. It’s be honest about which ones you actually need.”&lt;/p&gt;

&lt;p&gt;Tom has a sharper question. “Impact Mapping told us what matters. But I’m looking at the referral programme and it’s five stories. The shortfall tool is three. I don’t know which stories need to ship &lt;em&gt;together&lt;/em&gt; to actually work. If I build referral link generation but not referral tracking, have I shipped anything useful?”&lt;/p&gt;

&lt;p&gt;He’s not wrong. A flat backlog tells you what to build. Impact Mapping told them what matters. But neither shows how stories relate to each other, where the gaps are, or what subset adds up to a coherent experience.&lt;/p&gt;

&lt;h3 id=&quot;what-user-story-mapping-is&quot;&gt;What User Story Mapping is&lt;/h3&gt;

&lt;p&gt;User Story Mapping is Jeff Patton’s technique for visualising the user journey and organising stories within it. Instead of a flat list, you arrange stories in a two-dimensional map.&lt;/p&gt;

&lt;p&gt;Three layers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Activities run left to right across the top. The big things a user does, roughly chronological. This row is called the “backbone.”&lt;/li&gt;
  &lt;li&gt;Tasks sit below each activity. The specific things a user does within that activity.&lt;/li&gt;
  &lt;li&gt;Stories sit below each task, prioritised top to bottom. Must-haves at the top, nice-to-haves below.&lt;/li&gt;
&lt;/ul&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0; overflow-x: auto;&quot;&gt;
  &lt;div style=&quot;background: rgba(74,144,217,0.12); padding: var(--space-sm) var(--space-md); font-weight: bold; text-align: center; border-bottom: 1px solid var(--color-rule); color: var(--color-accent);&quot;&gt;Activities (the backbone) &amp;rarr;&lt;/div&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: repeat(6, 1fr); min-width: 600px;&quot;&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-sm); text-align: center;&quot;&gt;
      &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); font-weight: bold; font-size: 0.85rem;&quot;&gt;Activity 1&lt;/div&gt;
      &lt;div style=&quot;background: rgba(123,198,126,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.82rem;&quot;&gt;Task&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(must-have)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(should-have)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(nice-to-have)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-sm); text-align: center;&quot;&gt;
      &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); font-weight: bold; font-size: 0.85rem;&quot;&gt;Activity 2&lt;/div&gt;
      &lt;div style=&quot;background: rgba(123,198,126,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.82rem;&quot;&gt;Task&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(must-have)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(should-have)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(nice-to-have)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-sm); text-align: center;&quot;&gt;
      &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); font-weight: bold; font-size: 0.85rem;&quot;&gt;Activity 3&lt;/div&gt;
      &lt;div style=&quot;background: rgba(123,198,126,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.82rem;&quot;&gt;Task&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(must-have)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(should-have)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-sm); text-align: center;&quot;&gt;
      &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); font-weight: bold; font-size: 0.85rem;&quot;&gt;Activity 4&lt;/div&gt;
      &lt;div style=&quot;background: rgba(123,198,126,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.82rem;&quot;&gt;Task&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(must-have)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(should-have)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-sm); text-align: center;&quot;&gt;
      &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); font-weight: bold; font-size: 0.85rem;&quot;&gt;Activity 5&lt;/div&gt;
      &lt;div style=&quot;background: rgba(123,198,126,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.82rem;&quot;&gt;Task&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(must-have)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;padding: var(--space-sm); text-align: center;&quot;&gt;
      &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); font-weight: bold; font-size: 0.85rem;&quot;&gt;Activity 6&lt;/div&gt;
      &lt;div style=&quot;background: rgba(123,198,126,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.82rem;&quot;&gt;Task&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-top: var(--space-xs); font-size: 0.8rem;&quot;&gt;Story &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;(must-have)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Left to right tells you the user’s journey. Top to bottom tells you priority.&lt;/p&gt;

&lt;h3 id=&quot;building-the-greenbox-story-map&quot;&gt;Building the Greenbox story map&lt;/h3&gt;

&lt;p&gt;Jas suggests running the session. She grabs a fresh wall, a stack of sticky notes, and the whole team. Lee watches her take charge of the room, finding the markers, arranging the space, framing the question, and says nothing until afterwards. Then he tells Maya quietly: “She’s good. She thinks about the whole journey, not just the screen.”&lt;/p&gt;

&lt;p&gt;“Let’s start with the backbone,” Jas says. “What are the big activities a customer goes through, from first hearing about us to becoming a loyal subscriber?”&lt;/p&gt;

&lt;p&gt;After fifteen minutes, six activities:&lt;/p&gt;

&lt;div style=&quot;display: flex; align-items: center; gap: var(--space-xs); margin: var(--space-md) 0; overflow-x: auto; padding: var(--space-sm) 0;&quot;&gt;
  &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm) var(--space-md); font-weight: bold; font-size: 0.88rem; white-space: nowrap;&quot;&gt;Discover Greenbox&lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm) var(--space-md); font-weight: bold; font-size: 0.88rem; white-space: nowrap;&quot;&gt;Browse Boxes&lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm) var(--space-md); font-weight: bold; font-size: 0.88rem; white-space: nowrap;&quot;&gt;Subscribe&lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm) var(--space-md); font-weight: bold; font-size: 0.88rem; white-space: nowrap;&quot;&gt;Receive First Box&lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm) var(--space-md); font-weight: bold; font-size: 0.88rem; white-space: nowrap;&quot;&gt;Manage Subscription&lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(74,144,217,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm) var(--space-md); font-weight: bold; font-size: 0.88rem; white-space: nowrap;&quot;&gt;Refer a Friend&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;That’s the backbone. Not the system’s internals, the person’s experience.&lt;/p&gt;

&lt;h3 id=&quot;adding-tasks-and-stories&quot;&gt;Adding tasks and stories&lt;/h3&gt;

&lt;p&gt;The team fills in the tasks under each activity, then takes their eighty-three stories and places them. Some map neatly. Others don’t fit anywhere, which is revealing in itself.&lt;/p&gt;

&lt;p&gt;Sam sticks a note under “Discover Greenbox” and pauses. “I’ve got three marketing stories here, social media, SEO, press outreach. But none of them connect to anything Tom or Priya are building. If I run a press campaign and someone signs up, is the onboarding experience actually ready for them?”&lt;/p&gt;

&lt;p&gt;Everyone looks at the map. There’s a gap. The “Discover” column has marketing work, but “Browse” and “Subscribe” are sparse.&lt;/p&gt;

&lt;p&gt;“That’s exactly why we’re doing this,” Lee says. “A flat backlog would never have shown you that gap.”&lt;/p&gt;

&lt;p&gt;Sam points at the gap between “Subscribe” and “Receive First Box.” “After signup, the customer’s next touchpoint is the box arriving. If anything goes wrong, payment fails, delivery delayed, substitution they hate, the only way they can tell us is email. We have no status page, no tracking, no FAQ.” She pulls up her spreadsheet. “Sixty percent of my inbox is people asking things they should be able to find themselves.”&lt;/p&gt;

&lt;p&gt;They add new stories where the map shows gaps. Under “Check delivery area,” there were no stories at all. Under “Manage Subscription,” there were fourteen, far more than any other activity. One card, almost hidden in the supply-side column, reads “farm reliability scoring.” It goes up without much discussion.&lt;/p&gt;

&lt;p&gt;Three things jump out:&lt;/p&gt;

&lt;p&gt;Gaps. The “Discover” column is thin. Sam flags it: “We can build the best subscription experience in the world, but if nobody knows we exist, it doesn’t matter.”&lt;/p&gt;

&lt;p&gt;Over-investment. Fourteen stories about pausing, changing, upgrading, downgrading, cancelling. Is that where the team should spend its energy before they have more subscribers?&lt;/p&gt;

&lt;p&gt;Missing connections. Nothing between “Receive First Box” and “Manage Subscription.” What happens after someone gets their first box? How do they become a regular?&lt;/p&gt;

&lt;p&gt;None of this was visible in the flat backlog.&lt;/p&gt;

&lt;h3 id=&quot;release-slicing&quot;&gt;Release slicing&lt;/h3&gt;

&lt;p&gt;This is where User Story Mapping earns its keep. Instead of arguing about which stories to do first, the team draws horizontal lines across the map. Each line defines a release. Everything above ships in that release. Everything below waits.&lt;/p&gt;

&lt;p&gt;The rule: each release must tell a complete story. You can’t ship “Subscribe” without “Receive.” Each horizontal slice must be a usable product, even if it’s thin.&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0; overflow-x: auto;&quot;&gt;
  &lt;!-- Activity backbone --&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: repeat(6, 1fr); min-width: 700px; border-bottom: 1px solid var(--color-rule);&quot;&gt;
    &lt;div style=&quot;background: rgba(74,144,217,0.12); padding: var(--space-xs) var(--space-sm); font-weight: bold; text-align: center; font-size: 0.85rem; border-right: 1px solid var(--color-rule);&quot;&gt;Discover&lt;/div&gt;
    &lt;div style=&quot;background: rgba(74,144,217,0.12); padding: var(--space-xs) var(--space-sm); font-weight: bold; text-align: center; font-size: 0.85rem; border-right: 1px solid var(--color-rule);&quot;&gt;Browse&lt;/div&gt;
    &lt;div style=&quot;background: rgba(74,144,217,0.12); padding: var(--space-xs) var(--space-sm); font-weight: bold; text-align: center; font-size: 0.85rem; border-right: 1px solid var(--color-rule);&quot;&gt;Subscribe&lt;/div&gt;
    &lt;div style=&quot;background: rgba(74,144,217,0.12); padding: var(--space-xs) var(--space-sm); font-weight: bold; text-align: center; font-size: 0.85rem; border-right: 1px solid var(--color-rule);&quot;&gt;Receive&lt;/div&gt;
    &lt;div style=&quot;background: rgba(74,144,217,0.12); padding: var(--space-xs) var(--space-sm); font-weight: bold; text-align: center; font-size: 0.85rem; border-right: 1px solid var(--color-rule);&quot;&gt;Manage&lt;/div&gt;
    &lt;div style=&quot;background: rgba(74,144,217,0.12); padding: var(--space-xs) var(--space-sm); font-weight: bold; text-align: center; font-size: 0.85rem;&quot;&gt;Refer&lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- Release 1 --&gt;
  &lt;div style=&quot;background: rgba(46,204,113,0.10); padding: var(--space-xs) var(--space-sm); font-weight: bold; font-size: 0.85rem; color: var(--color-accent); border-bottom: 1px solid var(--color-rule); min-width: 700px;&quot;&gt;Release 1 &amp;mdash; MVP&lt;/div&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: repeat(6, 1fr); min-width: 700px; border-bottom: 2px solid var(--color-rule);&quot;&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-bottom: var(--space-xs);&quot;&gt;Landing page with value prop&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-bottom: var(--space-xs);&quot;&gt;Show two box sizes&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Show sample contents&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-bottom: var(--space-xs);&quot;&gt;Stripe checkout&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Collect address&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Email: box is on its way&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem; color: var(--color-ink-tertiary); text-align: center;&quot;&gt;&amp;mdash;&lt;/div&gt;
    &lt;div style=&quot;padding: var(--space-xs); font-size: 0.8rem; color: var(--color-ink-tertiary); text-align: center;&quot;&gt;&amp;mdash;&lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- Release 2 --&gt;
  &lt;div style=&quot;background: rgba(243,156,18,0.10); padding: var(--space-xs) var(--space-sm); font-weight: bold; font-size: 0.85rem; color: var(--color-accent); border-bottom: 1px solid var(--color-rule); min-width: 700px;&quot;&gt;Release 2 &amp;mdash; Operational&lt;/div&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: repeat(6, 1fr); min-width: 700px; border-bottom: 2px solid var(--color-rule);&quot;&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;SEO basics&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Delivery area checker&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem; color: var(--color-ink-tertiary); text-align: center;&quot;&gt;&amp;mdash;&lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-bottom: var(--space-xs);&quot;&gt;Delivery tracking link&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Rate this box&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-bottom: var(--space-xs);&quot;&gt;Pause for one week&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Change box size&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;padding: var(--space-xs); font-size: 0.8rem; color: var(--color-ink-tertiary); text-align: center;&quot;&gt;&amp;mdash;&lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- Release 3 --&gt;
  &lt;div style=&quot;background: rgba(231,76,60,0.08); padding: var(--space-xs) var(--space-sm); font-weight: bold; font-size: 0.85rem; color: var(--color-accent); border-bottom: 1px solid var(--color-rule); min-width: 700px;&quot;&gt;Release 3 &amp;mdash; Growth&lt;/div&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: repeat(6, 1fr); min-width: 700px;&quot;&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Instagram integration&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Seasonal calendar&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Gift subscription&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Photo of your farmer&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;border-right: 1px solid var(--color-rule); padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-bottom: var(--space-xs);&quot;&gt;Update payment card&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Cancel with feedback form&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;padding: var(--space-xs); font-size: 0.8rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs); margin-bottom: var(--space-xs);&quot;&gt;Generate referral link&lt;/div&gt;
      &lt;div style=&quot;background: rgba(245,215,110,0.15); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs);&quot;&gt;Friend gets 10% off&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Release 1 (MVP): Find Greenbox, see what’s on offer, subscribe, pay, receive a box with a notification. The bare minimum to prove someone will pay.&lt;/p&gt;

&lt;p&gt;Release 2 (Operational): Delivery tracking, pause and resize, delivery area checker, basic feedback. The essentials for keeping people subscribed.&lt;/p&gt;

&lt;p&gt;Release 3 (Growth): Referral programme, gift subscriptions, seasonal calendar, Instagram integration. Growth features that only matter once the product works.&lt;/p&gt;

&lt;p&gt;The team didn’t argue about whether referrals or delivery tracking should come first. The map made the answer obvious. Referrals are meaningless without a product worth referring.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-user-story-mapping&quot;&gt;When to use User Story Mapping&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;You’re planning releases and need to figure out what subset makes a coherent, shippable product.&lt;/li&gt;
  &lt;li&gt;You’ve lost the plot. “We have 80 stories and no idea what to ship first” is the classic symptom.&lt;/li&gt;
  &lt;li&gt;Product and engineering are misaligned. The story map bridges user journeys and engineering components on the same wall.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;when-not-to-use-it&quot;&gt;When not to use it&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;You need to refine individual stories. That’s &lt;a href=&quot;/writing/example-mapping-making-stories-concrete/&quot;&gt;Example Mapping&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;You need to understand the domain. That’s &lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Storming&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;You have a small, well-understood scope. Three stories don’t need a map. They need a whiteboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-happens-next&quot;&gt;What happens next&lt;/h3&gt;

&lt;p&gt;Looking at the wall, the team feels like they’ve cracked it. Four workshops, each building on the last, and they have a clear path from where they are to where they need to be. Event Storming gave them the domain. Example Mapping gave them concrete stories. Impact Mapping connected the work to goals. And now User Story Mapping shows them the whole journey, with release slices that make planning obvious instead of political.&lt;/p&gt;

&lt;p&gt;“I wish we’d done this four weeks ago,” Tom says.&lt;/p&gt;

&lt;p&gt;Jas smiles. “We didn’t know enough four weeks ago. We needed Event Storming to understand the domain, and Example Mapping to make the stories real. This built on top of all that.”&lt;/p&gt;

&lt;p&gt;The team knows what to build. They know the order. They know what a coherent release looks like. For the first time, the whole product is visible on a single wall.&lt;/p&gt;

&lt;p&gt;But a harder question is coming. The story map is beautiful. The release slices are clean. The team is confident they’re building the correct thing.&lt;/p&gt;

&lt;p&gt;They haven’t asked the customers yet.&lt;/p&gt;

&lt;p&gt;Forty percent monthly churn will force that question in ways the team doesn’t expect. Because it turns out the answer to “what’s for dinner?” isn’t just a box of vegetables, it’s a &lt;a href=&quot;/writing/jobs-to-be-done-why-subscribers-actually-stay/&quot;&gt;job to be done&lt;/a&gt;.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: Sprint Planning</title>
    <link href="/writing/the-workshop-sprint-planning/"/>
    <updated>2026-04-17T06:00:00+08:00</updated>
    <id>/writing/the-workshop-sprint-planning/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Two weeks, one goal, a plan everyone in the room committed to. Sprint Planning is the workshop where a refined backlog becomes a sprint: a contract about what you’ll learn by the end, not a to-do list with a deadline. For the worked example, see &lt;a href=&quot;/writing/sprint-planning-turning-sticky-notes-into-delivery/&quot;&gt;Turning Sticky Notes into Delivery&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;sprint-planning&quot;&gt;Sprint Planning&lt;/h3&gt;

&lt;p&gt;Sprint Planning turns a refined, prioritised backlog into a sprint the team can commit to: a goal, a set of stories that fit capacity, a task breakdown, and an explicit commitment from everyone in the room. Often called planning or iteration planning. Frequently confused with roadmap planning (which spans months) and with the story-preparation work that belongs &lt;em&gt;before&lt;/em&gt; planning, not inside it. The ritual is Scrum’s, but the pattern, “here is what we’ll do and here is why we believe it”, predates Scrum by decades.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; the whole team (4-9 people: facilitator, product owner, developers, tester, ops where relevant), one hour per sprint week.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; a sprint goal the team can state from memory, a set of stories that fit capacity, a task breakdown concrete enough for daily standup, and an explicit commitment from every person in the room.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; a refined backlog needs to become a sprint the team genuinely believes in. Not for unrefined stories (run &lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt; first), roadmap-scale planning, continuous-flow teams with no sprint boundary, or any session the product owner can’t attend.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whats-it-for&quot;&gt;What’s It For&lt;/h3&gt;

&lt;p&gt;A team finishes a sprint having delivered six of the eight stories they pulled in. The two unfinished stories get carried over. The next sprint they pull in ten stories to “catch up” and finish five. The sprint after that they pull in twelve. Velocity (the team’s average completed points per sprint, smoothed over the last few sprints) is now invisible, commitment has become theatre, and the team quietly stops believing the numbers.&lt;/p&gt;

&lt;p&gt;What went wrong wasn’t the work; it was the planning. Nobody said out loud what the sprint was actually &lt;em&gt;for&lt;/em&gt;. Stories got pulled in because they were next in the list, not because they served a goal. When a fire came up mid-sprint, the team had no way to decide whether to fight it or park it, because there was no goal to measure the fire against. Every sprint became a slightly different version of “do as much as possible,” which is indistinguishable from “do whatever’s loudest.”&lt;/p&gt;

&lt;p&gt;Sprint Planning exists to break that pattern. The sprint goal is the contract. The stories are the plan. When the plan has to change (and it always does) the goal is the thing you steer by. Without a goal, a sprint is a to-do list. With one, it’s a commitment.&lt;/p&gt;

&lt;p&gt;Reach for it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You work in sprints of one or two weeks&lt;/li&gt;
  &lt;li&gt;The top of the backlog has been refined: stories have acceptance criteria, have been through Example Mapping or equivalent, and are sized&lt;/li&gt;
  &lt;li&gt;The whole team can attend&lt;/li&gt;
  &lt;li&gt;Someone can articulate what the sprint should &lt;em&gt;achieve&lt;/em&gt;, not just what should be done&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-its-not-for&quot;&gt;What It’s Not For&lt;/h3&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The top of the backlog is a mess. Prepare the stories first, in a separate session; planning is not story preparation with an audience. &lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt; is the usual gate before planning.&lt;/li&gt;
  &lt;li&gt;The team operates in continuous flow and has no sprint boundary.&lt;/li&gt;
  &lt;li&gt;You’re planning more than one sprint ahead. That’s roadmap work, a different session.&lt;/li&gt;
  &lt;li&gt;The product owner can’t attend. Reschedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop a session that’s already started if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Three stories in a row need preparation work mid-planning. The top of the backlog isn’t ready; end the session, schedule the preparation, and come back&lt;/li&gt;
  &lt;li&gt;The product owner won’t accept capacity as a constraint. That’s a systemic problem, not a session problem&lt;/li&gt;
  &lt;li&gt;Two sprints in a row the committed plan wasn’t achievable. The sprint length, the preparation process, or the estimation practice is broken, not the planning session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ending planning and scheduling something else is not failure. Committing to a sprint nobody believes is.&lt;/p&gt;

&lt;h3 id=&quot;definitions--background&quot;&gt;Definitions &amp;amp; Background&lt;/h3&gt;

&lt;p&gt;The sprint goal is one sentence describing what the sprint must achieve. Not a list. A capability the team delivers, a metric they move, a problem they solve. &lt;em&gt;“Subscribers can pause and resume their subscriptions through the web app”&lt;/em&gt; is a goal. &lt;em&gt;“Make progress on the subscription area”&lt;/em&gt; is not.&lt;/p&gt;

&lt;p&gt;Capacity is not velocity. Velocity is a historical average: what the team has delivered over recent sprints. Capacity is &lt;em&gt;this&lt;/em&gt; sprint’s actually-available time: velocity, minus on-call rotations, minus holidays and leave, minus scheduled meetings outside the sprint cadence, minus carry-over (stories committed last sprint that didn’t ship) still in flight. Teams that plan to last sprint’s velocity in a sprint with two people on holiday over-commit by definition.&lt;/p&gt;

&lt;p&gt;Points are abstract sizing units, usually a Fibonacci-like 1/2/3/5/8/13 scale. T-shirt sizes or hours work too; the unit matters less than using the same one consistently.&lt;/p&gt;

&lt;p&gt;The four phases, set, select, break down, commit, each have a different shape:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Goal-setting is a conversation between the product owner and the team, moderated by the facilitator. The product owner proposes, the team pressure-tests, the facilitator writes the agreed goal somewhere everyone can see it for the rest of the session.&lt;/li&gt;
  &lt;li&gt;Story selection is a negotiation. The product owner defends priority; the team asserts capacity; the facilitator holds the capacity number honest.&lt;/li&gt;
  &lt;li&gt;Task breakdown is team work. The product owner is available for questions but not driving. Developers, testers, and ops decompose each story into tasks that fit inside a day.&lt;/li&gt;
  &lt;li&gt;Commitment check is a round-the-room moment. Each person says, in their own words, whether they believe the plan is achievable. This is the contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of the four collapse into the others, the ritual stops working.&lt;/p&gt;

&lt;h3 id=&quot;inputs&quot;&gt;Inputs&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;A refined backlog. Stories at the top with acceptance criteria, sized, and through &lt;a href=&quot;/writing/the-workshop-example-mapping/&quot;&gt;Example Mapping&lt;/a&gt; or equivalent. A story that hasn’t been through Example Mapping is not ready to plan; plan it anyway and you’ll be mid-sprint when you find out why.&lt;/li&gt;
  &lt;li&gt;The team’s actual capacity for this sprint: velocity, minus on-call rotations, minus holidays and leave, minus carry-over still in flight. Calculate this &lt;em&gt;before&lt;/em&gt; the session starts and write it on the board.&lt;/li&gt;
  &lt;li&gt;Recent velocity: the average of the last three delivered totals, not committed totals.&lt;/li&gt;
  &lt;li&gt;The whole team available for the duration of the session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing else needs to be prepared in-session. If it does, the preparation didn’t happen. If the backlog is chaotic, &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt; is where that gets fixed, not in planning. If the sprint goal can’t be traced back to a meaningful outcome, &lt;a href=&quot;/writing/the-workshop-impact-mapping/&quot;&gt;Impact Mapping&lt;/a&gt; is the upstream conversation.&lt;/p&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;What lands at the end of the session:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A sprint goal the whole team can state from memory: the one line that makes mid-sprint trade-offs decidable.&lt;/li&gt;
  &lt;li&gt;A selected set of stories that fits inside capacity, with priority order intact.&lt;/li&gt;
  &lt;li&gt;A task breakdown for each story, concrete enough that the daily standup has something to work against.&lt;/li&gt;
  &lt;li&gt;An explicit commitment from everyone in the room, not silent acquiescence.&lt;/li&gt;
  &lt;li&gt;Visible dependencies, on-call load, and carry-over so the sprint doesn’t break on something the plan didn’t acknowledge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Photograph the whiteboard if the task breakdown happened on physical sticky notes. The sprint goal goes where everyone can see it: team board, wiki, Slack topic, the top of the sprint in the tracker.&lt;/p&gt;

&lt;p&gt;These outputs feed straight into:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Retrospectives: the retrospective is where you notice that planning sessions have become theatre and fix the ritual before it fully collapses.&lt;/li&gt;
  &lt;li&gt;Ensemble Programming: when the task breakdown keeps flagging one developer as the sole person who can touch a system, ensemble work is the pattern that breaks the bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whos-needed&quot;&gt;Who’s Needed&lt;/h3&gt;

&lt;p&gt;The whole team, typically 4-9 people, for one hour per sprint week:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Facilitator. Runs the session, holds the clock, keeps the team off preparation detours. Often the Scrum Master (the role that protects the team from disruption and removes blockers), team lead, or a rotating role.&lt;/li&gt;
  &lt;li&gt;Product owner. Mandatory. They set the sprint goal, explain the stories, and make the trade-off calls when capacity doesn’t match ambition. Without them in the room, you are planning to build the wrong thing.&lt;/li&gt;
  &lt;li&gt;Developers. The whole development team. Sprint Planning is the one meeting where partial attendance breaks the ritual. If a developer isn’t in the room, they haven’t committed, and the commitment is what the meeting is for.&lt;/li&gt;
  &lt;li&gt;Tester / QA. If they sit with the team, they’re part of the team, and they plan with the team. Testing capacity is capacity. Treat it that way.&lt;/li&gt;
  &lt;li&gt;Operations / SRE. For any team whose sprint includes deployment work, on-call rotations, or infrastructure change, ops is a first-class planning participant. On-call load is a real capacity drain and if it isn’t in the plan it will consume the plan anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sprint Planning is one of the few patterns that scales with team size; larger teams need longer sessions, not different ones.&lt;/p&gt;

&lt;p&gt;Who to leave out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Stakeholders. They shape the backlog before planning and see the output afterwards. They don’t attend planning itself. Observers warp the commitment conversation because people self-censor in front of the people they serve.&lt;/li&gt;
  &lt;li&gt;Other teams. Dependencies on other teams belong in the task breakdown as risks, not in the room as people.&lt;/li&gt;
  &lt;li&gt;Senior leaders with “just a quick ask.” The just-a-quick-ask is the thing that destroys the sprint goal you’re supposed to be setting. Leadership input happens in the story-preparation work upstream of planning, not in planning itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;how-to-run-it&quot;&gt;How To Run It&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Duration (1-week sprint)&lt;/th&gt;
      &lt;th&gt;Duration (2-week sprint)&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Sprint goal&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;“What must this sprint achieve?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Story selection&lt;/td&gt;
      &lt;td&gt;20 min&lt;/td&gt;
      &lt;td&gt;40 min&lt;/td&gt;
      &lt;td&gt;“What fits, against real capacity?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Task breakdown&lt;/td&gt;
      &lt;td&gt;25 min&lt;/td&gt;
      &lt;td&gt;50 min&lt;/td&gt;
      &lt;td&gt;“What are the concrete steps?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Commitment check&lt;/td&gt;
      &lt;td&gt;5 min&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;“Does everyone believe we can do this?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;1 hour&lt;/td&gt;
      &lt;td&gt;~2 hours&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The rule of thumb is one hour per sprint week. Most teams beat that once they’ve run the pattern a few times and once upstream story preparation is reliable. If you’re consistently running longer, the problem is upstream: stories are arriving at planning unready.&lt;/p&gt;

&lt;h4 id=&quot;phase-1-sprint-goal-10-15-minutes&quot;&gt;Phase 1. Sprint goal (10-15 minutes)&lt;/h4&gt;

&lt;p&gt;Before any story is discussed, the product owner proposes a sprint goal. Not a list. A sentence.&lt;/p&gt;

&lt;p&gt;Open with:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Before we look at stories, let’s agree what this sprint is for. Product owner, if we could only ship one thing this sprint, one capability, one metric we move, one problem we solve, what is it?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write whatever they say on the whiteboard. Then pressure-test it with the team:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Is this achievable in one sprint? Is this worth a sprint? Does everyone in this room understand why this matters?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal should be specific, achievable in one sprint, and measurable or demonstrable. A good goal sounds like: &lt;em&gt;“Subscribers can pause and resume their subscriptions through the web app.”&lt;/em&gt; A bad goal sounds like: &lt;em&gt;“Make progress on the subscription area.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once the team accepts the goal, write it in large letters at the top of the whiteboard. Everything that follows is in service of this goal.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;No goal, just a list. The product owner says &lt;em&gt;“let’s just pull in as much as we can.”&lt;/em&gt; Push back: &lt;em&gt;“If we could only ship one thing this sprint, what would it be?”&lt;/em&gt; Refuse to move to story selection until a goal is on the board.&lt;/li&gt;
  &lt;li&gt;Vague goal. &lt;em&gt;“Make progress on subscriptions”&lt;/em&gt; is not a goal. Push: &lt;em&gt;“What would a subscriber be able to do at the end of this sprint that they can’t do now?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Three goals disguised as one. &lt;em&gt;“Pause, resume, and billing integration”&lt;/em&gt; is a roadmap item, not a sprint goal. Help the product owner pick the most important one; the others come next sprint.&lt;/li&gt;
  &lt;li&gt;SRE sprint goal looks different. For an ops-heavy sprint, the goal might be &lt;em&gt;“Reduce deployment rollback rate from 1 in 5 to 1 in 20”&lt;/em&gt; or &lt;em&gt;“Move the billing cron to the new scheduler with zero missed runs.”&lt;/em&gt; Same shape, same test: specific, achievable, demonstrable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-2-story-selection-20-40-minutes&quot;&gt;Phase 2. Story selection (20-40 minutes)&lt;/h4&gt;

&lt;p&gt;Start from the top of the backlog. For each story, the product owner gives a thirty-second explanation of what it is and why it serves the sprint goal. The team confirms they understand it (a quick nod around the room is enough; if it’s more than a nod, the story wasn’t refined). Then the team decides whether it fits.&lt;/p&gt;

&lt;p&gt;Keep a running tally of points (or t-shirt sizes, or hours, or whatever you size in) against capacity, visible at the edge of the whiteboard. When the tally reaches capacity, stop pulling.&lt;/p&gt;

&lt;p&gt;Capacity, not velocity. Velocity is the historical average; capacity is this sprint’s actually-available time. Plan to capacity, not velocity.&lt;/p&gt;

&lt;p&gt;Say it explicitly when you get there:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“We’re at 24 points. That’s our capacity. Any story we pull in now has to push another one out. Product owner, is there a trade you want to make?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Over-commitment. The team pulls in more than their average velocity because &lt;em&gt;“this sprint feels different.”&lt;/em&gt; It never is. Use the velocity. Optimism is not a planning strategy, and a team that over-commits twice loses the ability to trust itself.&lt;/li&gt;
  &lt;li&gt;Under-commitment. The team sandbagging because they got burned. A sprint or two of under-commitment to rebuild confidence is fine; persistent under-commitment means the stories are bigger than estimated or there’s hidden work the estimates don’t cover.&lt;/li&gt;
  &lt;li&gt;Skipping the priority order. &lt;em&gt;“Let’s skip story 3 and do story 7 instead.”&lt;/em&gt; Only the product owner can approve a priority swap, and they should say why out loud. Otherwise the backlog order stops meaning anything.&lt;/li&gt;
  &lt;li&gt;Gold-plating during selection. The team starts redesigning a story. Redirect: &lt;em&gt;“We’re deciding what’s in, not how to build it. Task breakdown is next.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Carry-over invisible. Last sprint’s unfinished work is coming in. Make it visible in the tally: &lt;em&gt;“We have 8 points of carry-over. That leaves 16 for new work.”&lt;/em&gt; Carry-over that isn’t counted is how velocity quietly disappears.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-3-task-breakdown-25-50-minutes&quot;&gt;Phase 3. Task breakdown (25-50 minutes)&lt;/h4&gt;

&lt;p&gt;For each selected story, the team breaks it into tasks. A task is concrete enough that one person can do it in a day or less.&lt;/p&gt;

&lt;p&gt;The facilitator’s job in this phase is mostly to keep moving. Give each story a time budget (&lt;em&gt;“five minutes per story”&lt;/em&gt;) and hold it. If a story needs more than five minutes of task breakdown, it wasn’t ready for planning; pull it and prepare it separately.&lt;/p&gt;

&lt;p&gt;For each story, the team identifies:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;What needs to happen: the concrete tasks, written on sticky notes or into the tracker&lt;/li&gt;
  &lt;li&gt;Who’s likely to do what (not assignments, but a flag for tasks that need specific expertise)&lt;/li&gt;
  &lt;li&gt;Dependencies: between tasks, between stories, between teams&lt;/li&gt;
  &lt;li&gt;The not-obvious work: testing, deployment, migration, documentation, feature flag cleanup, on-call handover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Tasks that are too big. &lt;em&gt;“Build the UI for pausing”&lt;/em&gt; is probably three tasks: form, validation, API wiring. If a task is longer than a day, split it.&lt;/li&gt;
  &lt;li&gt;Missing the unglamorous work. Teams forget testing, migrations, deployment, feature flag management, observability wiring, documentation updates, on-call runbook edits. Prompt: &lt;em&gt;“What else has to happen before we can call this done?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;One-person bottlenecks. If every story has the same developer flagged as essential, that’s a risk. Don’t solve it now; flag it and discuss pairing or knowledge-sharing in the retrospective.&lt;/li&gt;
  &lt;li&gt;External team dependencies. If a task needs another team’s API, approval, or review, name it and name the person who’ll chase it. Better still: can you start with a mock or stub so the dependency isn’t blocking?&lt;/li&gt;
  &lt;li&gt;On-call capacity. If a developer is carrying the pager this sprint, their capacity is not the same as a developer who isn’t. Build that in during task assignment, not after the fact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-4-commitment-check-5-10-minutes&quot;&gt;Phase 4. Commitment check (5-10 minutes)&lt;/h4&gt;

&lt;p&gt;Read the sprint goal aloud. Read the list of selected stories. Then go round the room and ask each person the same question:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Do you believe we can deliver this sprint as planned?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not a vote. It’s a check. You are looking for the person whose body language doesn’t match their words. You are giving the quiet team member a direct invitation to raise a concern that would otherwise stay silent until the retrospective.&lt;/p&gt;

&lt;p&gt;Run a silent confidence check before any verbal round-the-room: &lt;em&gt;“On a count of three, hold up fingers from one to five. One means you don’t believe we can deliver this sprint. Five means you’re confident. Three is the middle. No talking yet.”&lt;/em&gt; Anything below a four gets a follow-up question: &lt;em&gt;“You held up a two. Which story is the worry, and what would lift it?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The silent signal goes first because round-the-room polling, with senior people answering early, pressures introverts and juniors toward conformity. By the time the second person speaks, the room has anchored. Silent first surfaces the dissent that the verbal round would flatten.&lt;/p&gt;

&lt;p&gt;If someone says &lt;em&gt;“we’ll try,”&lt;/em&gt; it’s a signal. Ask one more question: &lt;em&gt;“What would turn ‘we’ll try’ into ‘I think we can’?”&lt;/em&gt; Sometimes the answer is “nothing, that’s my honest level of confidence and I’d ship anyway.” Sometimes it’s “remove this one story.” Either is a real answer; the silent vote is the trigger to find out which.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Silent discomfort. Someone doesn’t object but their face says the plan is too much. The confidence check should have caught this; if it didn’t, ask directly, by name: &lt;em&gt;“You’ve gone quiet. Which story are you worried about, and what would it take to remove the worry?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;“We’ll try.” Not automatically a no; a signal to ask one more question. &lt;em&gt;“What would turn ‘we’ll try’ into ‘I think we can’?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Scope-cutting inside stories. &lt;em&gt;“We can do it if we skip the error handling.”&lt;/em&gt; No. Error handling is in the acceptance criteria or it isn’t. Cut scope by removing stories, never by cutting corners inside them.&lt;/li&gt;
  &lt;li&gt;The product owner negotiating down. The product owner offers to cut a story to reassure the team. Let them. This is the ritual working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When everyone has said yes, genuinely said yes, not politely nodded, the sprint is committed. Write the commitment down with a date. The sprint starts.&lt;/p&gt;

&lt;p&gt;See &lt;a href=&quot;/writing/sprint-planning-turning-sticky-notes-into-delivery/&quot;&gt;Sprint Planning: Turning Sticky Notes into Delivery&lt;/a&gt; for the Greenbox team running their first planning session, including the moment they discover that a sprint goal changes every single decision that follows it, and the moment one of the developers realises the commitment check exists precisely for people who were about to nod along with a plan they didn’t believe in.&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What Can Go Wrong&lt;/h3&gt;

&lt;p&gt;The preparation session in disguise. Fifteen minutes debating what a story means, mid-planning.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“This story isn’t ready. Let’s pull it out of the sprint and work through it properly this week, maybe with Example Mapping, before it comes back to planning.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; Three stories in a row need that treatment. The top of the backlog isn’t ready for planning. End the session, schedule the preparation work, and come back.&lt;/p&gt;

&lt;p&gt;The wish list. The product owner keeps adding &lt;em&gt;“just one more.”&lt;/em&gt;
  &lt;em&gt;Recovery:&lt;/em&gt; Hold the capacity number honest: &lt;em&gt;“We’re at 24. This story is 5. Which 5-point story do you want to remove to make room?”&lt;/em&gt; Force the trade, every time.
  &lt;em&gt;Stop if:&lt;/em&gt; The product owner won’t accept capacity as a constraint. That’s a systemic problem, not a session problem. Flag it to leadership outside the room.&lt;/p&gt;

&lt;p&gt;The architecture debate. Developers start debating framework choices during task breakdown.
  &lt;em&gt;Recovery:&lt;/em&gt; Park it: &lt;em&gt;“Capture that as a design question. We need to know can we do it this sprint, not how.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The debate is blocking task breakdown entirely. The story needs a time-boxed design investigation before it’s plannable; pull it.&lt;/p&gt;

&lt;p&gt;The absent product owner. Product owner cancels the morning of.
  &lt;em&gt;Recovery:&lt;/em&gt; Reschedule, same day if possible, next day if not.
  &lt;em&gt;Stop if:&lt;/em&gt; This keeps happening. The ritual is broken; escalate the pattern, not the individual session.&lt;/p&gt;

&lt;p&gt;The permanent over-commitment. Every sprint the team commits to 30 points and delivers 20, and nobody adjusts.
  &lt;em&gt;Recovery:&lt;/em&gt; In the next planning session, write the last three sprints’ &lt;em&gt;delivered&lt;/em&gt; totals on the board before story selection. Plan to the delivered number, not the committed one. Watch what happens.
  &lt;em&gt;Stop if:&lt;/em&gt; The product owner insists on the committed number anyway. That’s a systemic trust problem; a planning session won’t solve it.&lt;/p&gt;

&lt;p&gt;The silent veto. Commitment check passes, but one person clearly doesn’t believe it. They’ve said yes because saying no feels rude.
  &lt;em&gt;Recovery:&lt;/em&gt; Take a break. Talk to them privately. Bring the concern back into the room as &lt;em&gt;“There’s a worry about the migration story that I don’t think we surfaced properly. Can we talk through it before committing?”&lt;/em&gt; so the objection is legitimised by the facilitator, not left to the quiet person to defend alone.
  &lt;em&gt;Stop if:&lt;/em&gt; They still won’t speak. The team has a safety problem, not a planning problem.&lt;/p&gt;

&lt;p&gt;The ritual collapsing into theatre. The sprint goal gets skipped and the sprint becomes a to-do list. Or carry-over isn’t counted against capacity and velocity quietly collapses. Or the commitment check becomes theatre and nobody actually believes the plan. Or task breakdown is skipped because “we know what to do,” and the team discovers mid-sprint that they didn’t. Or the session runs so long the team arrives at their first ticket exhausted.
  &lt;em&gt;Recovery:&lt;/em&gt; Name the failure mode in the next retrospective. Pick one to fix. Don’t try to fix all five at once.
  &lt;em&gt;Stop if:&lt;/em&gt; The retrospective itself can’t surface the problem. The ritual has fully collapsed and a planning session won’t rebuild it. Step back to first principles: what is this sprint &lt;em&gt;for&lt;/em&gt;?&lt;/p&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;

&lt;p&gt;The session ends; the sprint begins.&lt;/p&gt;

&lt;p&gt;Same day, the facilitator:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sprint goal written where everyone can see it: team board, wiki, Slack topic, the top of the sprint in the tracker.&lt;/li&gt;
  &lt;li&gt;All selected stories moved into the sprint in the tracker, with tasks attached.&lt;/li&gt;
  &lt;li&gt;External dependencies communicated to the teams they touch, today, before the sprint starts.&lt;/li&gt;
  &lt;li&gt;Photograph the whiteboard if the task breakdown happened on physical sticky notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This sprint, the product owner:&lt;/p&gt;

&lt;p&gt;This is where the pattern earns its cost, and the work is mostly the product owner’s.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Protect the sprint goal. Every “just one more thing” request that arrives this sprint gets measured against the goal. If it doesn’t serve the goal, it goes into the backlog for next sprint. If it does, it replaces something that doesn’t. The product owner is the only person who can make that trade.&lt;/li&gt;
  &lt;li&gt;Watch the burndown (the sprint’s day-by-day chart of remaining work) at the midpoint. For a two-week sprint, Thursday of week one. For a one-week sprint, the morning of day three. If you’re behind, have the conversation about what to cut &lt;em&gt;now&lt;/em&gt;, not on the last day. The goal survives a scope cut; it doesn’t survive a last-day scramble.&lt;/li&gt;
  &lt;li&gt;Prepare the top of the backlog for the next planning session. This is the unglamorous work that makes next sprint’s planning an hour instead of three. Run Example Mapping on the candidate stories. Answer the red cards. Size the stories. Arrive at the next planning session with a backlog that is actually plannable.&lt;/li&gt;
  &lt;li&gt;Walk the goal to anyone who matters. Stakeholders, leadership, dependent teams. “Here’s what we’re doing this sprint and here’s why” said once at the start prevents five “what are you working on” interruptions mid-sprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ongoing, the team:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Track velocity honestly. It’s the single most useful planning input and it only works if you measure what actually shipped, not what was committed.&lt;/li&gt;
  &lt;li&gt;If planning sessions consistently run long, the preparation happening before them needs work. Stories should arrive at planning ready to plan.&lt;/li&gt;
  &lt;li&gt;Retrospect on the planning session itself periodically. Is the goal still the contract? Is commitment still meaningful? Or has the ritual become theatre?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern earns its cost across sprints, not within one. One hour per sprint week, every sprint, forever: that’s the price. A product owner who has to be available and prepared, a story-preparation process upstream that reliably produces ready stories, and the discipline to say no to “just one more” every single time: that’s what holds it up.&lt;/p&gt;

&lt;h3 id=&quot;variants&quot;&gt;Variants&lt;/h3&gt;

&lt;p&gt;One-week sprint (default short cadence). One hour total, the four phases compressed. Task breakdown is tighter because the stories are smaller. Use when the team needs faster feedback loops or when scope volatility is high enough that two-week commitments break too often.&lt;/p&gt;

&lt;p&gt;Two-week sprint (default long cadence). Two hours total, more room for goal pressure-testing and richer task breakdown. The default for most teams; one-week cadence costs more facilitation overhead per unit of delivery.&lt;/p&gt;

&lt;p&gt;Distributed / remote. Same four phases on a Miro or Mural board. The silent confidence check transfers especially well to remote: everyone holds up fingers to camera at the same moment, no anchoring effect. Run the goal-setting conversation on video with the goal pinned at the top of the board for the rest of the session.&lt;/p&gt;

&lt;p&gt;SRE / ops-heavy sprint. The goal looks like &lt;em&gt;“Reduce deployment rollback rate from 1 in 5 to 1 in 20”&lt;/em&gt; or &lt;em&gt;“Move the billing cron to the new scheduler with zero missed runs”&lt;/em&gt; rather than a user-facing capability. Capacity calculations include on-call load explicitly. Task breakdown often surfaces runbook updates and observability wiring as first-class tasks rather than afterthoughts.&lt;/p&gt;

&lt;p&gt;First-sprint-with-this-team. Velocity is unknown, so capacity is a guess. Plan conservatively (commit to less than feels right), agree explicitly that the first two sprints are calibration, and use them to discover real velocity. Don’t pretend the guess is data.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>What Time Is It?</title>
    <link href="/writing/what-time-is-it/"/>
    <updated>2026-04-16T06:00:00+08:00</updated>
    <id>/writing/what-time-is-it/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/time/&quot;&gt;the Time series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;You glance at your phone, read the digits, and get on with your day. Behind those digits is a tower of compromises, conventions, and politics that has taken humanity thousands of years to build. The time it shows you is wobblier than you’d think.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;sticks-in-the-ground&quot;&gt;Sticks in the ground&lt;/h3&gt;

&lt;p&gt;Timekeeping started the way most useful things start: someone needed to solve a practical problem.&lt;/p&gt;

&lt;p&gt;If you’re growing crops, you need to know when to plant. If you’re a priest, you need to know when to perform a ritual. If you’re a trader, you need to know when the market opens. The sun moves across the sky in a predictable arc, so you stick a pole in the ground and watch its shadow. Congratulations: you’ve invented the sundial, and you’re roughly in agreement with everyone else in your village about when noon is.&lt;/p&gt;

&lt;p&gt;The Egyptians were dividing daylight into twelve parts around 1500 BCE. The Babylonians gave us base-60 counting, which is why we’re stuck with 60 minutes in an hour and 60 seconds in a minute: a convention so old that nobody alive chose it, yet nobody can change it.&lt;/p&gt;

&lt;p&gt;But sundials only work when the sun shines. So people got creative.&lt;/p&gt;

&lt;p&gt;Water clocks, clepsydrae, from the Greek &lt;em&gt;kleptein&lt;/em&gt; (to steal) and &lt;em&gt;hydor&lt;/em&gt; (water), measured time by the regulated flow of water from one vessel to another. The ancient Egyptians used them by at least 1500 BCE, and versions appeared independently in China, Greece, and Rome. Some were remarkably sophisticated. The Chinese polymath Su Song built a water-powered astronomical clock tower in 1088 that stood ten metres tall and featured an escapement mechanism (a device for converting continuous water flow into regular, counted intervals) centuries before European clockmakers would independently develop the same idea (Joseph Needham, &lt;em&gt;Science and Civilisation in China&lt;/em&gt;, Vol. 4, Part 2).&lt;/p&gt;

&lt;p&gt;Candles as clocks were simpler but clever. You’d mark a candle at regular intervals and burn it down, reading the time from how much remained. King Alfred the Great is traditionally credited with using graduated candles to regulate his daily routine in the 9th century, though the story is likely embroidered. The ingenious trick was the candle alarm clock: push tacks or small nails into the wax at a specific height. When the candle burns down to that point, the tacks fall onto a metal plate below with a clatter. You’ve just been woken up by a piece of wax and some hardware.&lt;/p&gt;

&lt;p&gt;Hourglasses, or sandglasses really, became widespread from the 14th century. Cheap, portable, and indifferent to weather. Ships used them to mark watches. Churches used them to time sermons (some congregations reportedly installed them facing the preacher, as a gentle hint). Kitchens used them, and still do. An hourglass doesn’t tell you &lt;em&gt;what&lt;/em&gt; time it is; it tells you how much time has passed, which is often what you actually need.&lt;/p&gt;

&lt;p&gt;Village clocks mattered more than any personal timepiece for most of human history. Before wristwatches, before pocket watches, the church or town clock &lt;em&gt;was&lt;/em&gt; the time. Its bell rang the hours, and entire communities synchronised their days to one mechanism in one tower. If that clock drifted, everyone drifted with it, and nobody noticed because there was nothing else to compare it to. Time was communal, and it was local.&lt;/p&gt;

&lt;p&gt;For most of human history, this was fine. Noon was when the sun was highest where &lt;em&gt;you&lt;/em&gt; stood, and what noon meant three towns over was someone else’s problem.&lt;/p&gt;

&lt;h3 id=&quot;clockwork&quot;&gt;Clockwork&lt;/h3&gt;

&lt;p&gt;The mechanical clock changed everything slowly, then all at once.&lt;/p&gt;

&lt;p&gt;Weight-driven clocks with verge escapements (an early mechanism that converted the steady pull of a hanging weight into a regular tick-tock) appeared in European church towers in the late 13th and early 14th centuries. They were large, expensive, and not particularly accurate, drifting by perhaps fifteen minutes per day. But they worked at night, they worked in rain, and they kept the whole town on the same schedule.&lt;/p&gt;

&lt;p&gt;Then Galileo noticed something. In 1583, as the story goes, he watched a lamp swinging in the Cathedral of Pisa and timed it against his pulse. Every swing took the same amount of time, regardless of how far the lamp swung. He’d stumbled on what physicists call isochronism: a pendulum’s swings take the same time regardless of how wide they are. He never built a pendulum clock himself.&lt;/p&gt;

&lt;p&gt;Christiaan Huygens did. In 1656, the Dutch mathematician and physicist built the first working pendulum clock, and the leap in accuracy was extraordinary: from roughly fifteen minutes of drift per day to about fifteen &lt;em&gt;seconds&lt;/em&gt; per day. That’s an improvement of roughly sixty-fold. Huygens patented the design the following year and published the theory in &lt;em&gt;Horologium Oscillatorium&lt;/em&gt; (1673), one of the great works of 17th-century physics.&lt;/p&gt;

&lt;p&gt;From there, the history of timekeeping is a history of progressive miniaturisation. Tower clocks became mantel clocks. Mantel clocks became pocket watches as mainsprings replaced hanging weights and balance wheels replaced pendulums (a pendulum, after all, needs gravity and a stable surface; useless in a trouser pocket). Pocket watches became wristwatches. Each step required new engineering: smaller parts, better lubricants, more precise machining. The craft of watchmaking drove precision manufacturing for centuries before the Industrial Revolution made it commonplace.&lt;/p&gt;

&lt;h3 id=&quot;the-problem-that-made-time-matter&quot;&gt;The problem that made time matter&lt;/h3&gt;

&lt;p&gt;In the 18th century, ships were sinking because sailors couldn’t figure out where they were. Not north-south; that part was easy. You measure the angle of the sun or the North Star above the horizon and you’ve got your latitude, your position north or south of the equator. Sailors had been doing this reliably for centuries.&lt;/p&gt;

&lt;p&gt;The deadly question was east-west. Longitude, your position east or west of a reference point, was a different beast entirely, because longitude is fundamentally a time problem. The Earth rotates 360 degrees in 24 hours, which is 15 degrees per hour. If you know it’s noon where you are and you also know it’s currently 3:00 PM back at your reference point, you’re three hours west, 45 degrees of longitude. Simple arithmetic. And once you know your longitude, you combine it with your latitude (which you already have from the stars) and plot your position on a chart. From your position on a chart, you can see where the land is, where the rocks are, and whether you need to change course. Longitude turns “somewhere in the Atlantic” into a dot on a map.&lt;/p&gt;

&lt;p&gt;The catch: you need to know what time it is &lt;em&gt;somewhere else&lt;/em&gt;. And in the 18th century, no clock could survive months at sea. Pendulum clocks were hopeless on a rocking ship. Without a reliable way to carry a reference time, sailors relied on dead reckoning: estimating their position by tracking how far they’d travelled from a known starting point. Speed was measured with beautiful simplicity: throw a rope with knots tied at regular intervals off the stern, let it run through your hands, and count how many knots pay out in a set time. That’s why we still measure nautical speed in knots. Note your speed, note your compass heading, note how long you’ve been on that heading, and do the arithmetic. If you left Lisbon heading west at five knots for six hours, you’re roughly thirty nautical miles west of Lisbon.&lt;/p&gt;

&lt;p&gt;The problem is that dead reckoning accumulates errors. Every estimate is slightly off: the current pushed you north, the wind shifted and nobody noticed for an hour, the speed measurement was wrong because the sea was rough. Each small error compounds on the last. After weeks at sea, a dead reckoning position could be off by hundreds of miles. And there was no way to check it, because checking required knowing your longitude, which required a clock.&lt;/p&gt;

&lt;p&gt;On 22 October 1707, a fleet of Royal Navy warships under Admiral Sir Cloudesley Shovell was returning to England through the Western Approaches. Fog. No sun for days. The navigators’ dead reckoning, weeks of accumulated estimates, each one slightly off, told them they were safely west of the Isles of Scilly. They were further east than they thought. Four ships struck the rocks. The &lt;em&gt;Association&lt;/em&gt;, Shovell’s flagship, went down in minutes. Nearly two thousand sailors drowned. It was one of the worst maritime disasters in British history, and the root cause was that nobody on board could answer “what time is it in Greenwich right now?”&lt;/p&gt;

&lt;p&gt;A Greenwich clock would have saved them. A navigator with a sextant can fix local noon to within a minute or two even through overcast. If local noon fell at 12:40 PM Greenwich time, that’s a 40-minute difference: 10 degrees west. Scilly sits at 6.3 degrees west. A clock, a sextant, and some arithmetic would have shown them they were closer to the rocks than they thought. They didn’t have the clock. They hit the rocks.&lt;/p&gt;

&lt;p&gt;The disaster was so shocking that Parliament offered a prize of £20,000 (millions in today’s money) for a practical solution. The &lt;a href=&quot;https://www.rmg.co.uk/stories/topics/longitude-act&quot;&gt;Longitude Act of 1714&lt;/a&gt; established the Board of Longitude, and the race was on.&lt;/p&gt;

&lt;p&gt;John Harrison, a self-taught carpenter and clockmaker from Yorkshire, spent decades building a series of marine chronometers, each one a masterwork of engineering. His H4, completed in 1761, was a pocket-watch-sized device that lost only five seconds on an 81-day voyage to Jamaica. The Board of Longitude, staffed largely by astronomers who preferred a celestial solution, dragged their feet on paying him. Harrison eventually got his money, but he was 80 years old by the time the matter was fully settled. Dava Sobel’s &lt;em&gt;Longitude&lt;/em&gt; (1995) tells the story beautifully.&lt;/p&gt;

&lt;p&gt;It’s hard to overstate the impact. Accurate portable clocks didn’t just solve navigation; they made the modern world possible. Once you can coordinate time across distance, you can coordinate &lt;em&gt;anything&lt;/em&gt; across distance.&lt;/p&gt;

&lt;h3 id=&quot;railways-ruin-everything-in-a-good-way&quot;&gt;Railways ruin everything (in a good way)&lt;/h3&gt;

&lt;p&gt;For a long time after Harrison, local time persisted on land. Bristol is about 2.5 degrees west of London, so noon in Bristol is roughly 10 minutes after noon in London. Nobody cared, because the fastest you could travel between them was by horse, and ten minutes didn’t matter.&lt;/p&gt;

&lt;p&gt;Then came the railways.&lt;/p&gt;

&lt;p&gt;If a train departs London at 8:00 AM London time and is due in Bristol at 10:00 AM, is that 10:00 AM London time or Bristol time? Now multiply this confusion by every station on every line. Timetables became dangerous nonsense, and not just an inconvenience. On single-track lines, the entire safety model depended on timetables keeping trains from meeting head-on. If the station master in Bristol and the station master in Bath were working to clocks that disagreed by several minutes, trains could occupy the same stretch of track at the same time. And they did. Accidents were attributed to time discrepancies between stations.&lt;/p&gt;

&lt;p&gt;Passengers missed trains because timetables were printed in London time but station clocks showed local time. Goods shipments went astray. Mail coaches connecting to trains arrived at the wrong moment. Some station clocks tried to have it both ways, sporting two minute hands, one showing local time, one showing railway time, a wonderfully British solution to a problem that shouldn’t have existed.&lt;/p&gt;

&lt;p&gt;The confusion reached the courts. In the 1858 case &lt;em&gt;Curtis v. March&lt;/em&gt;, the verdict hinged on whether “10:00” meant local time or Greenwich time. The law itself couldn’t answer the question “what time is it?” with a single answer.&lt;/p&gt;

&lt;p&gt;The Great Western Railway had already forced the issue. It adopted Greenwich Mean Time across its network in 1840, and other railways followed. The practice became known as Railway Time: GMT imposed not by government decree but by operational necessity. The trains couldn’t run safely without it, so the trains won. The legal standardisation didn’t come until the Definition of Time Act 1880, four decades after the railways had already settled the matter in practice.&lt;/p&gt;

&lt;p&gt;Once Britain had a single time, the same problem surfaced at the international scale. Telegraph networks and shipping lanes crossed borders, and every country still kept its own reference. The International Meridian Conference in Washington DC in 1884 was convened to fix this. It didn’t impose a grid of time zones the way people often assume.&lt;/p&gt;

&lt;p&gt;What the conference actually decided was narrower: Greenwich would be the prime meridian (longitude zero), and a universal day would start at Greenwich midnight. The vote was 22 to 1, with San Domingo against and France and Brazil abstaining. France was the holdout: Paris had been a rival prime meridian for centuries, and French pride didn’t yield easily. France didn’t officially adopt Greenwich-based time until 1911, and even then called it &lt;em&gt;“Paris Mean Time retarded by 9 minutes 21 seconds”&lt;/em&gt; to avoid saying “GMT.” (The grudge was real.)&lt;/p&gt;

&lt;p&gt;The conference said nothing about how countries should organise their civil clocks. Time zones emerged organically over the following decades as each nation decided how to align its local time to the Greenwich reference. Some adopted clean hour offsets. Others didn’t. The result is the glorious, maddening patchwork we have today.&lt;/p&gt;

&lt;h3 id=&quot;time-zones-and-their-discontents&quot;&gt;Time zones and their discontents&lt;/h3&gt;

&lt;p&gt;Time zones are a hack. They pretend that everyone within a wide strip of the Earth shares the same local time, which is obviously not true. And they’re political as much as they are geographical.&lt;/p&gt;

&lt;p&gt;The zones are not neat strips. They follow national and regional borders, creating wild zigzags on any map. Spain is geographically in line with the UK and Portugal but uses Central European Time because Francisco Franco aligned Spain’s clocks with Nazi Germany in 1940, and nobody ever changed them back; the sun sets absurdly late in Madrid in summer. Western China is officially UTC+8 (Beijing Time) but the sun doesn’t rise until 10 AM in winter in Kashgar; the whole country uses a single time zone because Beijing says so. France uses UTC+1 despite Brest being west of Greenwich. Western Argentina runs on UTC-3 though its geography suggests UTC-5.&lt;/p&gt;

&lt;p&gt;Some countries use odd offsets. India uses UTC+5:30, a compromise between Mumbai in the west and Kolkata in the east. Iran, Afghanistan, and Myanmar sit on their own half-hour offsets, as do Newfoundland and the Marquesas. Nepal is UTC+5:45, the only country on a 45-minute offset. Sri Lanka briefly switched from UTC+5:30 to UTC+6 in 1996, then switched back six months later.&lt;/p&gt;

&lt;p&gt;And then there’s Eucla. On the Eyre Highway near the Western Australia-South Australia border, a handful of roadhouses and a telegraph station use UTC+8:45, an unofficial timezone that splits the difference between Western Australia’s UTC+8 and South Australia’s UTC+9:30. It’s not recognised by any government. It’s not in any legislation. The locals just decided that neither neighbouring timezone made sense for them, so they invented their own. The IANA database doesn’t even have an entry for it; it falls under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Australia/Eucla&lt;/code&gt; with a +8:45 offset, one of the most obscure timezone entries in the world. If you’re driving from Perth to Adelaide and you stop for petrol at Border Village, you’re in a timezone that officially doesn’t exist. Your phone will probably show the wrong time. Welcome to Australia.&lt;/p&gt;

&lt;h3 id=&quot;the-shifting-ground&quot;&gt;The shifting ground&lt;/h3&gt;

&lt;p&gt;Even what’s been agreed keeps moving.&lt;/p&gt;

&lt;p&gt;Standard offsets shift, not just because of daylight saving. Take Perth. We’re on UTC+8 now, but before 1895, Western Australia used local mean time, roughly UTC+7:43. During both World Wars and again from 2006 to 2009, Perth observed daylight saving time and temporarily became UTC+9. During those DST periods, a timestamp from Perth at 2:00 AM on a transition day is &lt;em&gt;ambiguous&lt;/em&gt;: did it happen before the clocks went back, or after? The same wall-clock time occurred twice. And when clocks spring forward, an hour simply doesn’t exist; 2:00 AM to 2:59 AM never happened. Anyone born in that hour, any event scheduled in that hour, any log entry timestamped in that hour: none of it is real.&lt;/p&gt;

&lt;p&gt;This isn’t unique to Perth. Virtually every inhabited place on Earth has changed its UTC offset at least once. Russia has reshuffled its eleven time zones repeatedly. Turkey moved from UTC+2 to UTC+3 permanently in 2016. North Korea created UTC+8:30 in 2015, then switched back to UTC+9 in 2018 as a diplomatic gesture. Morocco observes DST year-round &lt;em&gt;except&lt;/em&gt; during Ramadan, when they suspend it, meaning the offset changes on religious dates that shift by roughly eleven days each year against the Gregorian calendar.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://www.iana.org/time-zones&quot;&gt;IANA timezone database&lt;/a&gt;, the file your phone and your servers use to figure out what time it is, tracks all of this. Every historical offset change, every DST transition, every political decision that moved a clock. It’s updated several times a year because governments keep changing the rules. If you’re writing software that handles time, this database is your source of truth, and the fact that it needs regular updates tells you everything about how stable time zones actually are.&lt;/p&gt;

&lt;p&gt;The consequence for software is brutal. You cannot store a local time and assume you know the UTC equivalent without also knowing &lt;em&gt;which version of the timezone rules were in effect&lt;/em&gt;. A timestamp of “2:30 AM, 25 March 2007, Perth” is meaningless unless you know whether DST was active, and the answer depends on whether you’re using the pre-trial rules or the trial rules. The time itself depends on &lt;em&gt;when you ask the question&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;daylight-saving-time&quot;&gt;Daylight saving time&lt;/h3&gt;

&lt;p&gt;And then there’s daylight saving time, which deserves its own category of complaint.&lt;/p&gt;

&lt;p&gt;The idea is attributed to George Vernon Hudson, a New Zealand entomologist who proposed it in 1895 because he wanted more daylight hours after work for collecting insects. (The things that change the world.) Germany was the first country to actually adopt it, in 1916, to save coal during World War I. Britain and the United States followed.&lt;/p&gt;

&lt;p&gt;When it happens varies wildly. The EU switches on the last Sunday of March and October. The US switches on the second Sunday of March and the first Sunday of November, a change made by the Energy Policy Act of 2005, which took effect in 2007 and broke a surprising amount of software. Australia varies by state. And because Australia is in the southern hemisphere, the transitions go the opposite way: clocks spring forward in October and fall back in April, which confuses anyone used to the northern pattern.&lt;/p&gt;

&lt;p&gt;Where it doesn’t happen is a longer and more entertaining list. Most of Africa. Most of Asia. Iceland. Hawaii. Most of the tropics. Queensland, Australia, though New South Wales, Victoria, and South Australia, which share the same longitude, do observe it, leading to the odd situation where crossing a state border changes your clock. Here in Western Australia, we’ve voted against DST in four separate referendums, most recently in 2009, after a three-year trial, and the answer is always no.&lt;/p&gt;

&lt;p&gt;And then there’s Arizona. Arizona doesn’t observe DST. But the Navajo Nation, which sits inside Arizona, does. And the Hopi reservation, which sits inside the Navajo Nation, doesn’t. Drive across those borders and your clock changes, doesn’t change, changes, and doesn’t change again. It’s a time zone nesting doll. Meanwhile, Lord Howe Island, a small Australian territory in the Tasman Sea, shifts by only 30 minutes for DST, because, apparently, why not.&lt;/p&gt;

&lt;p&gt;The costs are real. A 2008 study by Janszky and Ljung in the &lt;em&gt;New England Journal of Medicine&lt;/em&gt; found that heart attacks increase by about 5% in the week after the spring-forward transition, likely due to sleep disruption. Car accidents increase. Productivity drops. Software bugs bloom. The EU Parliament voted in 2019 to abolish DST entirely, but member states couldn’t agree on whether to keep permanent summer time or permanent winter time, and the proposal stalled.&lt;/p&gt;

&lt;p&gt;If you’re writing code that handles time zones, the &lt;a href=&quot;https://www.iana.org/time-zones&quot;&gt;IANA tz database&lt;/a&gt;, sometimes called the Olson database, after its creator Arthur David Olson, is your scripture. It’s updated several times a year because governments keep changing the rules. I’ve written about the kind of compound complexity this creates in &lt;a href=&quot;/writing/the-value-is-in-ideas-not-code/&quot;&gt;The Value Is in Ideas, Not Code&lt;/a&gt;; your library of knowledge about edge cases like these is exactly the sort of thing that separates useful software from software that crashes on a Sunday in Samoa.&lt;/p&gt;

&lt;h3 id=&quot;beautiful-ideas-nobody-uses&quot;&gt;Beautiful ideas nobody uses&lt;/h3&gt;

&lt;p&gt;Every now and then someone looks at the mess of time zones and leap seconds and local conventions and says: surely we can do better.&lt;/p&gt;

&lt;p&gt;TAI64 is one such attempt. Proposed by Daniel J. Bernstein (the same person behind qmail and djbdns, and the plaintiff in &lt;em&gt;Bernstein v. United States&lt;/em&gt;, the case that established code as protected speech under the First Amendment), TAI64 is a 64-bit representation of TAI: a simple count of seconds from a fixed epoch, with no leap seconds, no time zones, no daylight saving. It’s monotonically increasing, which means it’s ideal for log timestamps and event ordering. Every TAI64 label refers to exactly one second of real time, and the labels never go backwards or repeat. The extended form, TAI64N, adds nanosecond precision.&lt;/p&gt;

&lt;p&gt;It’s elegant. It solves almost every practical problem with timestamps in one clean design. Almost nobody uses it.&lt;/p&gt;

&lt;p&gt;Swatch Internet Time took a completely different approach. In 1998, the Swatch watch company proposed dividing the day into 1,000 “.beats”, with no time zones at all. The whole world would share a single time: @500 would mean the same moment for someone in Tokyo as in Toronto. The meridian was set at Biel, Switzerland (Swatch’s headquarters, naturally). One .beat is 86.4 seconds.&lt;/p&gt;

&lt;p&gt;It was a lovely idea. Time zones exist because of the sun, but in an increasingly connected digital world, coordinating across zones creates constant friction. A universal internet time would eliminate “my 3 PM or your 3 PM?” forever. The notation was fun, the concept was sound, and it was backed by a major brand.&lt;/p&gt;

&lt;p&gt;Nobody used it. The sun is still there. People still wake when it rises and sleep when it sets, more or less, and local time still reflects that biological reality. Swatch Internet Time lives on as a curious footnote and the occasional novelty watch face.&lt;/p&gt;

&lt;p&gt;Both TAI64 and Swatch Internet Time failed for the same fundamental reason: they solved a technical problem while ignoring the human one. We don’t just use time to coordinate machines. We use it to coordinate lives, and lives are lived in places where the sun rises and sets at particular local times. Any scheme that ignores this is swimming against a very strong current.&lt;/p&gt;

&lt;p&gt;It’s the same pattern: the technically “correct” solution (a universal encoding, a universal timescale) only wins when it also solves the human problem. UTF-8 succeeded where other encodings failed because it was backwards-compatible with ASCII. A universal time system would need to be backwards-compatible with the sun.&lt;/p&gt;

&lt;h3 id=&quot;even-the-source-of-truth-gets-it-wrong&quot;&gt;Even the source of truth gets it wrong&lt;/h3&gt;

&lt;p&gt;The tz database is the closest thing we have to a global authority on time. Every phone, every server, every programming language runtime uses it. And it has been wrong.&lt;/p&gt;

&lt;p&gt;Governments don’t give notice. Egypt has announced DST changes with literally days of warning: not enough time for the database to ship an update, propagate through OS vendors, and reach the devices that need it. In 2014, Egypt &lt;a href=&quot;https://www.timeanddate.com/news/time/egypt-cancels-dst-2014.html&quot;&gt;cancelled DST with ten days’ notice&lt;/a&gt;, then reinstated it two years later, then cancelled it again. Each flip left a window where every computer in Egypt was showing the wrong time. Morocco’s Ramadan DST suspensions are worse; they shift against the Gregorian calendar by roughly eleven days each year, so the database has to predict Islamic calendar dates in advance. Sometimes the prediction is wrong and a correction has to be issued after the fact.&lt;/p&gt;

&lt;p&gt;Turkey in 2016 was a sharp example. The government announced permanent UTC+3 with almost no lead time. Software using cached or bundled tz data (which is most software) was simply wrong until updates shipped. Java, Python, every major OS: all had a window where timezone calculations for Istanbul were incorrect. If you’d scheduled a meeting in Turkey during that window, your calendar was lying to you.&lt;/p&gt;

&lt;p&gt;A lawsuit nearly killed it. In 2011, a company called &lt;a href=&quot;https://www.eff.org/cases/astrolabe-v-olson&quot;&gt;Astrolabe Inc. sued Arthur David Olson&lt;/a&gt; for copyright infringement, claiming the database incorporated data from their copyrighted timezone atlas. The database was briefly taken offline; the world’s timezone source of truth, gone. ICANN stepped in, took over maintenance under the Internet Engineering Task Force, and the lawsuit was eventually dismissed. But for a period, the infrastructure that every computer on Earth depends on for knowing what time it is was legally threatened by a copyright claim.&lt;/p&gt;

&lt;p&gt;The past keeps changing. The database relies on historical records (newspaper clippings, government gazettes, personal recollections) that are sometimes incomplete or contradictory, and corrections to decades-old entries ship regularly. Sometimes a zone didn’t exist yet: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Asia/Tomsk&lt;/code&gt; wasn’t added until tzdata 2016j, so Tomsk events stored before then used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Asia/Novosibirsk&lt;/code&gt; rules, which had different offsets in some periods. The timestamp didn’t change, the interpretation did. Sometimes the history gets rewritten: in tzdata 2018i, the pre-independence data for several West African countries was substantially revised based on new archival research. Sometimes it gets erased: in tzdata 2022b, zones with identical post-1970 data were merged and their distinct pre-1970 histories moved to a separate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backzone&lt;/code&gt; file that most operating systems don’t ship, so pre-1970 lookups silently started resolving to different UTC instants. The answer to “what time was it?” depends on when you ask.&lt;/p&gt;

&lt;p&gt;And then there’s Antarctica. The South Pole doesn’t have a natural timezone; every line of longitude converges there, so the concept is meaningless. The Amundsen-Scott South Pole Station uses New Zealand time (UTC+12/+13) because its supply flights come from Christchurch. But other Antarctic stations use the timezone of their home country, or the timezone of their supply base, or whatever the station commander decided that year. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Antarctica/Vostok&lt;/code&gt; entry in the tz database has been &lt;a href=&quot;https://mm.icann.org/pipermail/tz/2010-September/041427.html&quot;&gt;corrected multiple times&lt;/a&gt;; at one point it referenced the South Magnetic Pole, which had drifted hundreds of kilometres away from the station. Vostok officially uses UTC+5 (matching its Russian supply base in Novosibirsk), but in practice the station has used UTC+6 and UTC+7 at various points depending on who was running the base that year. When the tz database maintainers tried to nail down the correct offset, the answer was: it depends on who you ask and when you asked them. In 2023, the actual chief of Vostok station &lt;a href=&quot;https://mm.icann.org/pipermail/tz/2023-December/058376.html&quot;&gt;wrote to the tz mailing list&lt;/a&gt; to announce yet another offset change. When other list members questioned the short notice and process, his reply was disarming: “Well, sorry, but I am not too experienced with timezone changing.” The man responsible for the time at one of the most remote places on Earth was doing it for the first time, explaining his reasoning to a mailing list of strangers, and offering to send documentation in Russian.&lt;/p&gt;

&lt;p&gt;The tz database is maintained by volunteers. It’s one of the most critical pieces of infrastructure on the internet, right up there with DNS root servers and the BGP routing tables, and it runs on the goodwill of people who care about getting the time right. Every time your phone silently adjusts for a timezone change you didn’t know about, that’s someone on the &lt;a href=&quot;https://mm.icann.org/pipermail/tz/&quot;&gt;tz mailing list&lt;/a&gt; who noticed, researched it, wrote a patch, and got it merged. The system works. It just works by the thinnest of margins.&lt;/p&gt;

&lt;h3 id=&quot;so-what-time-is-it&quot;&gt;So what time is it?&lt;/h3&gt;

&lt;p&gt;That’s the human story of the hour: thousands of years of sticks in the ground, springs and pendulums, political compromises, and a volunteer-maintained database that quietly keeps the world’s clocks from lying to us.&lt;/p&gt;

&lt;p&gt;The hour on your phone is a fragile compromise between the sun and politics. The &lt;em&gt;date&lt;/em&gt; next to it is a fragile compromise too, built from a different history, a different cast, and its own pile of arguments. That’s what &lt;a href=&quot;/writing/what-day-is-it/&quot;&gt;What Day Is It?&lt;/a&gt; is about: Gregorian switchovers, lunar and lunisolar calendars, the International Date Line, and the year numbers that don’t agree. It’s coming shortly.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: Example Mapping</title>
    <link href="/writing/the-workshop-example-mapping/"/>
    <updated>2026-04-15T06:00:00+08:00</updated>
    <id>/writing/the-workshop-example-mapping/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Twenty-five minutes, four colours of card, and a vague user story turns into something developers can actually build. &lt;a href=&quot;/writing/example-mapping-making-stories-concrete/&quot;&gt;Making Stories Concrete&lt;/a&gt; shows one team’s first run; this post is the reference you keep open the morning of yours.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;example-mapping&quot;&gt;Example Mapping&lt;/h3&gt;

&lt;p&gt;Example Mapping breaks a single user story into rules and concrete examples in twenty-five minutes, so the team knows whether the story is ready to build and what “done” actually means. Invented by Matt Wynne in 2015. Frequently confused with BDD scenario writing; Example Mapping produces the material BDD scenarios are then written from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; a facilitator who doesn’t put cards down, a product owner, one or two developers, and a tester if you have one, with an ops or domain expert pulled in when the story touches their patch. Three to five people, twenty-five minutes.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; a verdict said out loud (ready, ready with named assumptions, needs splitting, or blocked), blue rules ready to paste into the tracker as acceptance criteria, green examples that are almost BDD scenarios already, and a counted pile of red question cards.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; a story about to enter a sprint where you want to know if it’s actually ready, or where you suspect the product owner and developers disagree about “done” but haven’t surfaced it. Not for genuinely trivial stories (just build them), not for stories whose shape you don’t yet know (run &lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming&lt;/a&gt; or &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt; first), and not without the product owner in the room.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whats-it-for&quot;&gt;What’s It For&lt;/h3&gt;

&lt;p&gt;A developer picks up a story called “Subscriber can pause their box.” She reads the acceptance criteria, they look fine, she starts building. Three days in she hits a question: what happens to a box that’s already been packed when the pause takes effect? She asks the product owner. The product owner doesn’t know. The product owner asks the warehouse lead. The warehouse lead says “obviously the packed box goes out, we can’t unpack it,” and the developer’s first two days of work are now wrong.&lt;/p&gt;

&lt;p&gt;This happens because the story looked simple. It wasn’t. There were three rules hiding inside it and at least one of them depended on operational knowledge nobody had written down. The conversation that would have caught it, a twenty-minute chat between the product owner, a developer, and someone from operations, would have happened before the sprint started if anyone had thought it was worth the time.&lt;/p&gt;

&lt;p&gt;Example Mapping exists to make that conversation cheap enough that you always have it. The cards are the forcing function: you can’t hand-wave acceptance criteria when someone asks for a concrete example and you have to write it on a green card.&lt;/p&gt;

&lt;p&gt;Reach for it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A story is about to enter a sprint and you want to know whether it’s actually ready&lt;/li&gt;
  &lt;li&gt;Developers and the product owner suspect they disagree about “done” but haven’t surfaced it&lt;/li&gt;
  &lt;li&gt;The story feels simple and you don’t trust the feeling&lt;/li&gt;
  &lt;li&gt;You need to decide whether a story should be split, built, or deferred for more discovery&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-its-not-for&quot;&gt;What It’s Not For&lt;/h3&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The story is genuinely trivial. A copy change, a feature flag flip, a config tweak. Just build it.&lt;/li&gt;
  &lt;li&gt;You don’t yet know &lt;em&gt;what&lt;/em&gt; you’re building. Example Mapping assumes you have a story and drills into what it means; run &lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming&lt;/a&gt; or &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt; first to get the story in the first place.&lt;/li&gt;
  &lt;li&gt;The people who know the answers aren’t in the room. Without the product owner or the domain expert, the rules will get written but nobody will have the authority to say they’re right. Reschedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop a session that’s already started if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Five minutes in and you can’t even seed a green card, the story is too vague to map; it needs discovery, not Example Mapping&lt;/li&gt;
  &lt;li&gt;The product owner is absent or checking their phone&lt;/li&gt;
  &lt;li&gt;Every rule is producing a red card, you’re not mapping, you’re discovering, and that’s a different session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stopping early when the signal is clear is not failure. Forcing a doomed session to the 25-minute bell is.&lt;/p&gt;

&lt;h3 id=&quot;definitions--background&quot;&gt;Definitions &amp;amp; Background&lt;/h3&gt;

&lt;p&gt;Four card colours, each one belonging to a role in the conversation:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Yellow, the story. One card, at the top of the table, present for the whole session.&lt;/li&gt;
  &lt;li&gt;Blue, rules. Acceptance criteria phrased as business rules. &lt;em&gt;“A subscriber can pause for up to eight weeks.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Green, examples. Concrete scenarios that illustrate a rule. &lt;em&gt;“A subscriber pauses on a Monday for two weeks. Her Wednesday box skips. Her next box is the following Wednesday.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Red, questions. Things nobody in the room can answer. &lt;em&gt;“What happens to a box that’s already been packed when the pause takes effect?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples are primary; rules fall out of them. Wynne’s framing all the way through: someone offers a concrete example the product owner has in mind, the room abstracts a rule from it, then someone offers another example to test that rule. The next example either confirms the rule, refines it, or breaks it, and breaking it is fine. A broken rule is replaced with one or two sharper rules, or a red card if nobody knows. Teams who try to lead with rules produce neat-looking maps that miss the cases the business actually cares about.&lt;/p&gt;

&lt;p&gt;The cards arrange themselves around the story: blue rules stretch left-to-right under the yellow story; green examples drop in columns under the rule they illustrate; red questions go off to the side where they can be counted at the end.&lt;/p&gt;

&lt;h3 id=&quot;inputs&quot;&gt;Inputs&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;One user story written on a yellow card at the top of the table. Participants who have read the story in advance are a small bonus, not a prerequisite.&lt;/li&gt;
  &lt;li&gt;Cards in four colours: yellow, blue, green, red. A table the participants can stand around, the layout grows: yellow story at the top, blue rules in a row underneath, green examples dropping in columns under each rule, red questions off to one side.&lt;/li&gt;
  &lt;li&gt;A 25-minute slot with no interruptions and the right people in the room (see &lt;em&gt;Who’s Needed&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the story isn’t yet defined enough to write on a card, run &lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming&lt;/a&gt; or &lt;a href=&quot;/writing/the-workshop-user-story-mapping/&quot;&gt;User Story Mapping&lt;/a&gt; first. Example Mapping doesn’t generate the story; it sharpens one you already have.&lt;/p&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;What lands on the table at the end:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A clear verdict on the story, ready to build, ready with named assumptions, needs splitting, or blocked. State it out loud before anyone leaves the room.&lt;/li&gt;
  &lt;li&gt;Acceptance criteria as the blue rule cards, concrete enough to paste into the tracker.&lt;/li&gt;
  &lt;li&gt;Test scenarios as the green example cards, almost in BDD format already.&lt;/li&gt;
  &lt;li&gt;Open questions as red cards, each one a thing somebody needs to answer before the story enters a sprint.&lt;/li&gt;
  &lt;li&gt;Sibling stories, sometimes. When an example or a rule points to behaviour that doesn’t actually serve the user and outcome on the yellow card, capture it as a new yellow card parked next to the main one. This is a feature, not feature creep, discovering a sibling story mid-session is one of the most valuable ways scope gets split honestly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Photograph the table layout from directly above before the cards come down, yellow at the top, blue rules in a row beneath it, green examples dropping in columns under each rule, red questions and any parked yellows off to the side.&lt;/p&gt;

&lt;p&gt;These outputs feed straight into:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-sprint-planning/&quot;&gt;Sprint Planning&lt;/a&gt;, a story that’s been Example Mapped is ready for the capacity discussion. One that hasn’t shouldn’t be in the sprint conversation.&lt;/li&gt;
  &lt;li&gt;Decision Tables, when a rule has many conditions and the green cards become unmanageable, promote the rule to a decision table.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt;, red cards that can’t be answered by anyone in the room are often assumptions in disguise. They belong on the grid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whos-needed&quot;&gt;Who’s Needed&lt;/h3&gt;

&lt;p&gt;Three to five people, twenty-five minutes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Facilitator. Runs the clock, asks for the next example, keeps people off implementation detours. Does not put cards on the table themselves except to demonstrate the colour convention at the start.&lt;/li&gt;
  &lt;li&gt;Product owner. Mandatory. They own the story and they’re the person who decides which rule applies when two participants disagree. If the product owner can’t attend, reschedule.&lt;/li&gt;
  &lt;li&gt;Developers. At least one, ideally two. They’ll catch the rules that are impossible or expensive to implement as written, and they benefit most from leaving with concrete tests in hand.&lt;/li&gt;
  &lt;li&gt;Tester or QA. Highly valuable if you have one. They will think of edge cases faster than anyone else in the room and they are the natural customer for the green cards.&lt;/li&gt;
  &lt;li&gt;Operations / support / domain expert. When the story touches a part of the system only one person really understands, the warehouse lead, the SRE who owns the cron that matters, the support agent who talks to the subscribers who hit this particular path, pull them in for this one session. They’ll save you a week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fewer than three and you don’t get enough friction between perspectives; more than five and the table becomes a meeting. Leave the rest of the team out, they’ll get the output through the acceptance criteria. Observers warp the conversation: if they care about the story, they should come as participants or read the output afterwards.&lt;/p&gt;

&lt;h3 id=&quot;how-to-run-it&quot;&gt;How To Run It&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Duration&lt;/th&gt;
      &lt;th&gt;Cards&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Explain the colours, read the story&lt;/td&gt;
      &lt;td&gt;2 min&lt;/td&gt;
      &lt;td&gt;Yellow&lt;/td&gt;
      &lt;td&gt;“What are we mapping?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Seed the first rule&lt;/td&gt;
      &lt;td&gt;3 min&lt;/td&gt;
      &lt;td&gt;Blue + green&lt;/td&gt;
      &lt;td&gt;“What’s the most obvious rule? Give me an example.”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Explore the rules&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;Blue + green + red&lt;/td&gt;
      &lt;td&gt;“What happens if…?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Assess readiness&lt;/td&gt;
      &lt;td&gt;5 min&lt;/td&gt;
      &lt;td&gt;Review all&lt;/td&gt;
      &lt;td&gt;“Is this ready to build?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;25 minutes&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Twenty-five minutes is the pattern. The time constraint is not cosmetic, if you can’t map the story in twenty-five minutes, the story is telling you something: it’s too big, too vague, or standing on unresolved questions. That signal is worth the entire session by itself.&lt;/p&gt;

&lt;h4 id=&quot;phase-1-seed-the-rules-3-minutes&quot;&gt;Phase 1. Seed the rules (3 minutes)&lt;/h4&gt;

&lt;p&gt;Read the yellow card aloud. All of it. Don’t paraphrase. If the story is two sentences long, read both sentences.&lt;/p&gt;

&lt;p&gt;Then set the colour convention:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Yellow is the story, it’s already on the table. Blue is for rules. A rule is an acceptance criterion phrased as a business rule: ‘a subscriber can pause for up to eight weeks.’ Green is for examples. An example is a concrete scenario: ‘A subscriber pauses on a Monday for two weeks, her Wednesday box skips.’ Red is for questions nobody in this room can answer right now. We’ll come back to those at the end.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now draw out the first rule. The product owner almost always has the obvious one:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What’s the most basic rule, the thing that has to be true for this story to exist at all?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write it on a blue card. Place it below the yellow story. Then immediately:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Can someone give me a concrete example of that rule? One specific scenario. Real names are fine.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write the example on a green card. Place it under the blue rule. You now have the shape of the session: yellow on top, blue in a row underneath, green in a column under each blue. Once the shape is visible, the room knows what to do.&lt;/p&gt;

&lt;h4 id=&quot;phase-2-explore-the-rules-15-minutes&quot;&gt;Phase 2. Explore the rules (15 minutes)&lt;/h4&gt;

&lt;p&gt;This is the core of the session. You now cycle: example, rule, example, red card, example, rule. The facilitator’s job is to keep the cycle moving by asking one of five questions over and over:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Have you got a real example, and what rule does that example imply?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Can someone give me an example of that?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What happens if…?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Is that always true, or only sometimes?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Is that the same rule or a different rule?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first question is the most important and the easiest to skip. &lt;em&gt;Examples drive rules&lt;/em&gt;, every time, start from a concrete example the product owner has in mind, abstract the rule from it, then offer another example to test the rule. The next example confirms the rule, refines it, or breaks it. A broken rule is replaced with one or two sharper rules, or a red card if nobody knows. Don’t let the team flip the order: when a rule shows up before an example, ask for the example before you write the rule down.&lt;/p&gt;

&lt;p&gt;When an example or a candidate rule clearly belongs to a &lt;em&gt;different&lt;/em&gt; story, it doesn’t serve the user or outcome on the yellow card, grab a yellow card and park it to the side. Don’t argue, don’t fold it in. Discovering a sibling story is a useful outcome, not a derailment, and the parked yellow becomes a candidate for its own session.&lt;/p&gt;

&lt;p&gt;Place blue cards in a row. Place green cards in columns under their rule. Place red cards off to the right, a visible pile the team can count. Parked yellows go alongside the red pile; they’re a different signal but they live in the same margin.&lt;/p&gt;

&lt;p&gt;Example Mapping works the same for infrastructure stories, with the SRE or pipeline owner taking the product owner’s seat. A rule might be &lt;em&gt;“the pipeline rolls back on failed health checks”&lt;/em&gt; and an example &lt;em&gt;“health check fails at 10:02, rollback begins at 10:03, service back on previous version by 10:05.”&lt;/em&gt; Same shape, different domain.&lt;/p&gt;

&lt;p&gt;See &lt;a href=&quot;/writing/example-mapping-making-stories-concrete/&quot;&gt;Example Mapping: Making Stories Concrete&lt;/a&gt; for one team’s first run, including the moment a green card about an already-packed box turns into the red card that reshapes a week of work.&lt;/p&gt;

&lt;h4 id=&quot;phase-3-assess-readiness-5-minutes&quot;&gt;Phase 3. Assess readiness (5 minutes)&lt;/h4&gt;

&lt;p&gt;Step back from the table. Look at the shape of the cards. The shape tells you the verdict.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Few blue cards, examples for each, no red cards, the story is well understood. Build it.&lt;/li&gt;
  &lt;li&gt;Many blue cards, examples for each, no red cards, the story is well understood but too big. Split it, probably by rule.&lt;/li&gt;
  &lt;li&gt;Red cards, the story has open questions. Each red card has two valid closures: get the answer, or make an explicit assumption you can defend and write it on the back of the card. The second closure is fine when the assumption is low-stakes or the team is willing to wear the consequence, but flag any high-stakes assumption (one where the wrong call breaks the plan) for &lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt; before the sprint commits to building. Reds left as &lt;em&gt;neither answered nor assumed&lt;/em&gt; mean the story isn’t ready.&lt;/li&gt;
  &lt;li&gt;Very few cards, session finished in twelve minutes, either the story really is trivial, or the team is being superficial. Probe once: &lt;em&gt;“Is there any scenario we haven’t considered where this would behave differently?”&lt;/em&gt; If the answer is a confident no, you’re done.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;State the verdict out loud. Someone, ideally the product owner, says the phrase: &lt;em&gt;“This is ready to build”&lt;/em&gt;, &lt;em&gt;“Ready with these assumptions”&lt;/em&gt;, &lt;em&gt;“Needs splitting”&lt;/em&gt;, or &lt;em&gt;“Blocked on these red cards”&lt;/em&gt;. Saying it out loud matters. It’s the commitment, and it’s what everyone remembers when someone later asks &lt;em&gt;“wait, did we decide about this?”&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What Can Go Wrong&lt;/h3&gt;

&lt;p&gt;The monologue. The product owner explains the story for ten minutes while everyone listens politely.
  &lt;em&gt;Recovery:&lt;/em&gt; Interrupt and cash the monologue in for cards: &lt;em&gt;“Stop there. Can someone capture what they just said as a rule on a blue card?”&lt;/em&gt; Forcing people to write cards forces them to be precise.
  &lt;em&gt;Stop if:&lt;/em&gt; The monologue restarts after a second redirect. The story isn’t ready for Example Mapping, it needs a longer conversation first, and you should schedule one.&lt;/p&gt;

&lt;p&gt;The rabbit hole. The team is fifteen minutes into debating one edge case.
  &lt;em&gt;Recovery:&lt;/em&gt; Cash it in as a red card: &lt;em&gt;“This is a great question. Let’s put it on red and keep moving. We’ll come back to it or take it offline.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The same edge case keeps surfacing after two red cards. There’s a deeper unknown and you need a time-boxed investigation outside the room before another Example Mapping session will land.&lt;/p&gt;

&lt;p&gt;The empty table. Nobody is writing cards. The conversation is circular.
  &lt;em&gt;Recovery:&lt;/em&gt; The story is too vague. Ask a sharply concrete question: &lt;em&gt;“What’s the simplest version of this story? One subscriber, one action, one outcome. What happens?”&lt;/em&gt; If that produces a green card, you have a seed.
  &lt;em&gt;Stop if:&lt;/em&gt; The room genuinely can’t answer the simplest version. The story needs discovery, not mapping.&lt;/p&gt;

&lt;p&gt;The premature solution. Developers start debating database schemas and API signatures.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“Great implementation thinking, hold it for when you start the story. Right now we’re still mapping what should happen.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; They can’t hold the distinction after three prompts. They’ll get more value from a design session.&lt;/p&gt;

&lt;p&gt;The silent developer. A developer is in the room but not speaking, not writing, not asking for examples.
  &lt;em&gt;Recovery:&lt;/em&gt; Name them and ask directly: &lt;em&gt;“What’s the part of this story you’re most worried about implementing?”&lt;/em&gt; Worry is the fastest route to a red card.
  &lt;em&gt;Stop if:&lt;/em&gt; They disengage completely. Something else is going on. Don’t try to fix it in-session.&lt;/p&gt;

&lt;p&gt;The reverse map. The team writes blue cards directly from existing acceptance criteria and never generates green cards at all. Common in teams who’ve been running Example Mapping for six months, the ritual survives but the discovery has died.
  &lt;em&gt;Recovery:&lt;/em&gt; Cover the blue cards with paper and ask for examples first. &lt;em&gt;“Forget what we wrote. Tell me a real scenario for this story, a specific subscriber doing a specific thing on a specific Tuesday.”&lt;/em&gt; Once a green card lands, uncover the blues and see if they survive.
  &lt;em&gt;Stop if:&lt;/em&gt; The room can’t produce a green card without seeing the rules. Example Mapping has degenerated into AC-rephrasing theatre; the story needs different discovery work first.&lt;/p&gt;

&lt;p&gt;The committee verdict. The product owner defers to the developers when stating the readiness call.
  &lt;em&gt;Recovery:&lt;/em&gt; Ask them directly: &lt;em&gt;“Product owner, what do you want to do with this story?”&lt;/em&gt; The verdict is theirs to state.
  &lt;em&gt;Stop if:&lt;/em&gt; They can’t or won’t make a call. The story isn’t owned. Don’t add it to the sprint until it is.&lt;/p&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;

&lt;p&gt;The session ends; the work begins.&lt;/p&gt;

&lt;p&gt;Same day, the facilitator:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Photographs the card layout (one shot from above is enough, it’s only one table)&lt;/li&gt;
  &lt;li&gt;Transcribes the rules into the story’s acceptance criteria in the tracker&lt;/li&gt;
  &lt;li&gt;Transcribes the green cards as test scenarios, in BDD format if the team uses it&lt;/li&gt;
  &lt;li&gt;Writes the red cards into the tracker as open questions, each one with an assigned owner and a date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This week, the product owner:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Chases every red card to closure. Each red card needs one of two outcomes before the story enters a sprint: &lt;em&gt;answered&lt;/em&gt;, or &lt;em&gt;assumed-and-named&lt;/em&gt;. The product owner owns the choice. An answered red card folds back into the rules and examples; a named assumption gets written on the back of the card and goes into the story description so the team building it knows what they’re betting on. High-stakes assumptions, the kind where the wrong call breaks the plan, get tested via &lt;a href=&quot;/writing/the-workshop-assumption-mapping/&quot;&gt;Assumption Mapping&lt;/a&gt; before the sprint commits. A red card that’s neither answered nor assumed is a production bug rehearsed.&lt;/li&gt;
  &lt;li&gt;States the verdict in writing. Update the story with “Ready to build”, “Needs splitting”, or “Blocked on [red cards]”. The verdict is the single most valuable artefact from the session.&lt;/li&gt;
  &lt;li&gt;Splits the story if it needs splitting. Each rule with its examples can often become its own story. Don’t defer the split until sprint planning, the shape is freshest now.&lt;/li&gt;
  &lt;li&gt;Walks the acceptance criteria back to the developers. They were in the room, but the transcribed criteria may look different from what they remember. Five minutes of &lt;em&gt;“does this match what we decided?”&lt;/em&gt; prevents the slow drift between session and sprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ongoing, the team:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Runs Example Mapping for every story before it enters a sprint. It takes twenty-five minutes and consistently prevents the mid-sprint &lt;em&gt;“but I thought it meant…”&lt;/em&gt; conversations that cost days.&lt;/li&gt;
  &lt;li&gt;Tracks the red card rate. If it’s trending upward, stories are arriving at Example Mapping too raw, push for better discovery upstream.&lt;/li&gt;
  &lt;li&gt;Keeps the green cards visible during the sprint. They’re the tests, and having them on the team board keeps “done” honest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;variants&quot;&gt;Variants&lt;/h3&gt;

&lt;p&gt;Story Level (default). One user story about to enter a sprint, twenty-five minutes, three to five people. Output: rules, examples, questions, build/split/defer verdict. This is what most teams need, and the rest of this post describes it.&lt;/p&gt;

&lt;p&gt;Epic Level. A cluster of related stories in an epic, ninety minutes, multiple passes with breaks between them. Output: a rough split of the epic into buildable stories. Reach for this when you’re trying to decide how to break up a large feature area; it’s really three or four Story Level sessions stacked together.&lt;/p&gt;

&lt;p&gt;Remote. A Miro or Mural board with the four card colours pinned, video call for the conversation. Slightly slower (the rhythm of &lt;em&gt;“write a card, place a card”&lt;/em&gt; is faster in person), but the structure transfers cleanly. Use one shared cursor: only the facilitator places cards, prompted by the team, to keep the layout legible.&lt;/p&gt;

&lt;p&gt;Pre-sprint sweep. Run six or eight Story Level sessions back-to-back with a fifteen-minute break in the middle, twice a week. Three hours total but it surfaces the entire next sprint’s worth of unknowns in one sitting. Best for teams whose backlog grooming has slipped and stories arrive at planning underprepared.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Impact Mapping: Connecting Work to Goals</title>
    <link href="/writing/impact-mapping-connecting-work-to-goals/"/>
    <updated>2026-04-14T06:00:00+08:00</updated>
    <id>/writing/impact-mapping-connecting-work-to-goals/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/shipping-what-matters/&quot;&gt;Shipping What Matters&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;The Greenbox team has been heads-down for a few sprints. Subscriptions work. Basic delivery is running. Event Storming gave them shared understanding. Example Mapping drives their stories. BDD catches bugs before production. The team is shipping faster and cleaner than ever.&lt;/p&gt;

&lt;p&gt;But on Friday afternoon, Maya pulls up the numbers and stares at them. They hit 214 subscribers at the end of sprint three. That was the target. That was the win. A month later, they’re at 197. The slope is going the wrong direction.&lt;/p&gt;

&lt;p&gt;She brings it to the Monday standup. “We hit 214. We celebrated. Now we’re at 197. We’re adding new subscribers, but we’re losing existing ones faster. Churn is eating the growth.”&lt;/p&gt;

&lt;p&gt;The room goes quiet.&lt;/p&gt;

&lt;h3 id=&quot;the-feature-trap&quot;&gt;The feature trap&lt;/h3&gt;

&lt;p&gt;Tom has been lobbying for a farm analytics dashboard, charts showing yield trends, delivery reliability scores, seasonal forecasting. It’s technically interesting. It’s obviously useful. It feels like the next logical feature.&lt;/p&gt;

&lt;p&gt;Priya wants to improve the substitution algorithm. Jas has sketched a redesigned customer homepage. Sam wants an email onboarding sequence for new subscribers.&lt;/p&gt;

&lt;p&gt;Everyone has a credible next thing to build. Each one would Example Map beautifully. But none of them address the problem Maya just put on the table: why aren’t they growing?&lt;/p&gt;

&lt;p&gt;Sam mentions something else. “Last Tuesday the site was slow for about an hour. Three potential subscribers tried to sign up and got a timeout.” Nobody knew it was slow. Sam’s uptime monitor only checks if the site is up, not if it’s fast. Tom adds a response time check. Crude, a single number, checked every five minutes, but now they know when the site is slow, not just when it’s down.&lt;/p&gt;

&lt;p&gt;This is the feature trap. Teams build what seems obvious, what’s technically exciting, or what the loudest person wants. The board looks healthy. Velocity is great. But the metric that matters is flat.&lt;/p&gt;

&lt;h3 id=&quot;what-impact-mapping-is&quot;&gt;What Impact Mapping is&lt;/h3&gt;

&lt;p&gt;Impact Mapping is a technique created by Gojko Adzic. The core idea: before you decide &lt;em&gt;what&lt;/em&gt; to build, work backwards from &lt;em&gt;why&lt;/em&gt; you’re building it.&lt;/p&gt;

&lt;p&gt;Four levels:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Goal, the measurable business objective. Not a feature. A number you can check.&lt;/li&gt;
  &lt;li&gt;Actors, the people whose behaviour needs to change to reach the goal.&lt;/li&gt;
  &lt;li&gt;Impacts, the specific behaviour changes you need from those actors.&lt;/li&gt;
  &lt;li&gt;Deliverables, the features that could create those behaviour changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why → Who → How → What.&lt;/p&gt;

&lt;p&gt;The structure is a tree. One goal at the root. Every feature can trace a path back to the goal. If it can’t, it doesn’t belong.&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; padding: var(--space-md); margin: var(--space-md) 0; overflow-x: auto;&quot;&gt;
  &lt;div style=&quot;display: flex; gap: var(--space-md); align-items: flex-start; min-width: 600px;&quot;&gt;
    &lt;div style=&quot;flex: 0 0 auto; min-width: 100px;&quot;&gt;
      &lt;div style=&quot;background: rgba(184,134,11,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-weight: bold; font-size: 0.85rem;&quot;&gt;GOAL&lt;br /&gt;&lt;span style=&quot;font-weight: normal; color: var(--color-ink-secondary);&quot;&gt;(Why?)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--space-sm); min-width: 100px;&quot;&gt;
      &lt;div style=&quot;background: rgba(65,105,225,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;ACTOR&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(Who?)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(65,105,225,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;ACTOR&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(Who?)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--space-sm); min-width: 100px;&quot;&gt;
      &lt;div style=&quot;background: rgba(46,139,87,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;IMPACT&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(How?)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(46,139,87,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;IMPACT&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(How?)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(46,139,87,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;IMPACT&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(How?)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--space-sm); min-width: 110px;&quot;&gt;
      &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;DELIVERABLE&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(What?)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;DELIVERABLE&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(What?)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;DELIVERABLE&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(What?)&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85rem;&quot;&gt;&lt;strong&gt;DELIVERABLE&lt;/strong&gt;&lt;br /&gt;&lt;span style=&quot;color: var(--color-ink-secondary);&quot;&gt;(What?)&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h3 id=&quot;running-the-session&quot;&gt;Running the session&lt;/h3&gt;

&lt;p&gt;Maya books an hour. The whole team plus Lee.&lt;/p&gt;

&lt;p&gt;Lee starts at the root. “What’s the one goal? Not a feature. A business outcome you can measure.”&lt;/p&gt;

&lt;p&gt;Maya doesn’t hesitate. “Stop the bleeding and get to three hundred active subscribers within three months. The board needs to see the model works before the next funding round.”&lt;/p&gt;

&lt;p&gt;“Good. Now, who are the people whose behaviour affects whether you hit that number?”&lt;/p&gt;

&lt;h3 id=&quot;identifying-actors&quot;&gt;Identifying actors&lt;/h3&gt;

&lt;p&gt;Subscribers, people already paying. If they churn, you’re running to stand still.&lt;/p&gt;

&lt;p&gt;Potential subscribers, people who haven’t signed up yet.&lt;/p&gt;

&lt;p&gt;Farms, the supply side. If farms can’t reliably deliver, the product falls apart and subscribers leave.&lt;/p&gt;

&lt;p&gt;Maya (operations), writing her own name on the board is uncomfortable. The Event Storm already surfaced her as the supply-matching bottleneck. Now the Impact Map puts it more starkly: she’s not just a bottleneck in the process, she’s a risk to the goal. She’s spending hours each week manually matching supply to demand, and last week it caught up with her, she was still finalising substitutions when the courier arrived, two boxes went out wrong, and one subscriber cancelled on the spot.&lt;/p&gt;

&lt;h3 id=&quot;mapping-impacts&quot;&gt;Mapping impacts&lt;/h3&gt;

&lt;p&gt;For each actor, Lee asks: “What behaviour change would help us reach 300 subscribers?”&lt;/p&gt;

&lt;p&gt;Not “what feature do they need.” Behaviour change.&lt;/p&gt;

&lt;p&gt;Subscribers: Stay subscribed (don’t churn). Refer friends.&lt;/p&gt;

&lt;p&gt;Potential subscribers: Discover Greenbox exists. Trust it enough to try.&lt;/p&gt;

&lt;p&gt;Farms: Commit supply reliably. Communicate shortfalls early.&lt;/p&gt;

&lt;p&gt;Maya: Spend less time on manual matching.&lt;/p&gt;

&lt;p&gt;Seven impacts. Each one is a lever that moves the goal.&lt;/p&gt;

&lt;h3 id=&quot;from-impacts-to-deliverables&quot;&gt;From impacts to deliverables&lt;/h3&gt;

&lt;p&gt;Now, and only now, does the team talk about features.&lt;/p&gt;

&lt;p&gt;Subscribers → Stay subscribed: Pause subscription. Box preview notifications. Flexible box sizes.
Subscribers → Refer friends: Referral programme. Shareable box photos.&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; padding: var(--space-md); margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;background: rgba(184,134,11,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); font-weight: bold; display: inline-block; margin-bottom: var(--space-sm);&quot;&gt;300 subscribers in 3 months&lt;/div&gt;
  &lt;div style=&quot;padding-left: var(--space-md); border-left: 2px solid var(--color-rule); margin-left: var(--space-sm);&quot;&gt;
    &lt;div style=&quot;background: rgba(65,105,225,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); font-weight: bold; display: inline-block; margin: var(--space-xs) 0;&quot;&gt;Subscribers&lt;/div&gt;
    &lt;div style=&quot;padding-left: var(--space-md); border-left: 2px solid var(--color-rule); margin-left: var(--space-sm);&quot;&gt;
      &lt;div style=&quot;background: rgba(46,139,87,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); display: inline-block; margin: var(--space-xs) 0;&quot;&gt;&lt;strong&gt;Stay subscribed&lt;/strong&gt;&lt;/div&gt;
      &lt;div style=&quot;padding-left: var(--space-md); border-left: 2px solid var(--color-rule); margin-left: var(--space-sm); margin-bottom: var(--space-sm);&quot;&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Pause subscription&lt;/div&gt;&lt;br /&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Box preview notifications&lt;/div&gt;&lt;br /&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Flexible box sizes&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;background: rgba(46,139,87,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); display: inline-block; margin: var(--space-xs) 0;&quot;&gt;&lt;strong&gt;Refer friends&lt;/strong&gt;&lt;/div&gt;
      &lt;div style=&quot;padding-left: var(--space-md); border-left: 2px solid var(--color-rule); margin-left: var(--space-sm);&quot;&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Referral programme&lt;/div&gt;&lt;br /&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Shareable box photos&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Potential subscribers → Discover Greenbox: SEO landing pages, local press outreach, social media content.
Potential subscribers → Trust enough to try: Customer reviews, first-box discount, money-back guarantee.&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; padding: var(--space-md); margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;background: rgba(184,134,11,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); font-weight: bold; display: inline-block; margin-bottom: var(--space-sm);&quot;&gt;300 subscribers in 3 months&lt;/div&gt;
  &lt;div style=&quot;padding-left: var(--space-md); border-left: 2px solid var(--color-rule); margin-left: var(--space-sm);&quot;&gt;
    &lt;div style=&quot;background: rgba(65,105,225,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); font-weight: bold; display: inline-block; margin: var(--space-xs) 0;&quot;&gt;Potential subscribers&lt;/div&gt;
    &lt;div style=&quot;padding-left: var(--space-md); border-left: 2px solid var(--color-rule); margin-left: var(--space-sm);&quot;&gt;
      &lt;div style=&quot;background: rgba(46,139,87,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); display: inline-block; margin: var(--space-xs) 0;&quot;&gt;&lt;strong&gt;Discover Greenbox&lt;/strong&gt;&lt;/div&gt;
      &lt;div style=&quot;padding-left: var(--space-md); border-left: 2px solid var(--color-rule); margin-left: var(--space-sm); margin-bottom: var(--space-sm);&quot;&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;SEO landing pages&lt;/div&gt;&lt;br /&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Local press outreach&lt;/div&gt;&lt;br /&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Social media content&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;background: rgba(46,139,87,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); display: inline-block; margin: var(--space-xs) 0;&quot;&gt;&lt;strong&gt;Trust enough to try&lt;/strong&gt;&lt;/div&gt;
      &lt;div style=&quot;padding-left: var(--space-md); border-left: 2px solid var(--color-rule); margin-left: var(--space-sm);&quot;&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Customer reviews&lt;/div&gt;&lt;br /&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;First box discount&lt;/div&gt;&lt;br /&gt;
        &lt;div style=&quot;background: rgba(255,140,0,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin: var(--space-xs) 0; display: inline-block; font-size: 0.88rem;&quot;&gt;Money-back guarantee&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Farms → Commit supply reliably: Forward contracts, demand forecasting tools.
Farms → Communicate shortfalls early: Shortfall reporting tool, SMS deadline reminders.&lt;/p&gt;

&lt;p&gt;Maya → Less time on manual matching: Automated supply matching, substitution rules engine.&lt;/p&gt;

&lt;p&gt;Seventeen deliverables across four actors. Every single one traces back to a specific behaviour change, which traces back to the goal.&lt;/p&gt;

&lt;h3 id=&quot;the-insight-that-changes-everything&quot;&gt;The insight that changes everything&lt;/h3&gt;

&lt;p&gt;Tom looks at the map and goes quiet. His farm analytics dashboard isn’t on it.&lt;/p&gt;

&lt;p&gt;He tries to find a place for it. “It could go under farms, help them commit supply reliably?”&lt;/p&gt;

&lt;p&gt;Lee pushes back. “Would a dashboard showing yield trends actually change whether a farm commits supply?”&lt;/p&gt;

&lt;p&gt;Maya is honest. “The farms I work with commit supply because I ring them on Tuesday and ask what they’ve got. A dashboard wouldn’t change that. What would help is if they could just text me when something’s gone wrong.”&lt;/p&gt;

&lt;p&gt;Tom’s dashboard is interesting software. It’s not goal-critical. The map makes that visible.&lt;/p&gt;

&lt;p&gt;Meanwhile, “pause subscription” is under the most important impact: keeping existing subscribers. Jas mentions that three subscribers have already cancelled because they were going on holiday and couldn’t skip a week. They didn’t churn because of bad produce. They churned because there was no pause button.&lt;/p&gt;

&lt;p&gt;Sam pulls up the numbers. They’ve lost 17 subscribers in a month. Three mentioned inflexibility. If they could retain even half the churning subscribers, it would be worth more than acquiring new ones, because retained subscribers also refer friends.&lt;/p&gt;

&lt;p&gt;The pause feature is a day’s work, maybe two. Tom’s dashboard would take three weeks. The map makes the decision obvious.&lt;/p&gt;

&lt;h3 id=&quot;prioritising-with-the-map&quot;&gt;Prioritising with the map&lt;/h3&gt;

&lt;p&gt;Lee draws a two-by-two grid, impact on the goal versus effort to build.&lt;/p&gt;

&lt;div style=&quot;display: grid; grid-template-columns: 1fr 1fr; border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(46,139,87,0.08); border-right: 1px solid var(--color-rule); border-bottom: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-accent);&quot;&gt;Do first&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;High impact, lower effort&lt;/span&gt;
    &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
      &lt;li&gt;Pause subscription&lt;/li&gt;
      &lt;li&gt;Shortfall reporting tool&lt;/li&gt;
      &lt;li&gt;SMS deadline reminders&lt;/li&gt;
      &lt;li&gt;Box preview notifications&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(65,105,225,0.08); border-bottom: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-accent);&quot;&gt;Plan carefully&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;High impact, higher effort&lt;/span&gt;
    &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
      &lt;li&gt;Referral programme&lt;/li&gt;
      &lt;li&gt;Automated supply matching&lt;/li&gt;
      &lt;li&gt;Customer reviews&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(184,134,11,0.06); border-right: 1px solid var(--color-rule);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-ink-tertiary);&quot;&gt;Maybe later&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;Lower impact, lower effort&lt;/span&gt;
    &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
      &lt;li&gt;First box discount&lt;/li&gt;
      &lt;li&gt;Money-back guarantee&lt;/li&gt;
      &lt;li&gt;Shareable box photos&lt;/li&gt;
      &lt;li&gt;SEO landing pages&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: var(--space-md); background: rgba(0,0,0,0.03);&quot;&gt;
    &lt;strong style=&quot;display: block; margin-bottom: 0.5em; color: var(--color-ink-tertiary);&quot;&gt;Probably never&lt;/strong&gt;
    &lt;span style=&quot;font-size: 0.85rem; color: var(--color-ink-secondary);&quot;&gt;Lower impact, higher effort&lt;/span&gt;
    &lt;ul style=&quot;margin-top: 0.5em; padding-left: 1.2em; font-size: 0.88rem;&quot;&gt;
      &lt;li&gt;Demand forecasting for farms&lt;/li&gt;
      &lt;li&gt;Forward contracts&lt;/li&gt;
      &lt;li&gt;Flexible box sizes&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Tom notices something. “My dashboard isn’t even on the grid.”&lt;/p&gt;

&lt;p&gt;“The grid only has deliverables from the Impact Map,” Lee says. “Your dashboard couldn’t trace a line back to the goal.”&lt;/p&gt;

&lt;p&gt;Tom’s dashboard was never rejected or argued down. It simply didn’t appear. There’s nothing personal about it, the reasoning is visible on the whiteboard.&lt;/p&gt;

&lt;p&gt;But it doesn’t quite feel that way to Tom. After the session, he goes back to his desk and quietly closes the design document he’d been working on, the one with the seasonal forecasting charts he’d been excited about. Priya notices. She sends him a message: “The dashboard isn’t dead. It just serves a different goal.” Tom doesn’t respond for an hour. Then: “I know. Thanks.”&lt;/p&gt;

&lt;p&gt;The team commits to the top-left quadrant for the next two sprints. Pause subscription first, then shortfall reporting.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-impact-mapping&quot;&gt;When to use Impact Mapping&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Quarterly planning, when deciding what the team should focus on next, an Impact Map grounds the conversation in outcomes rather than feature wishlists.&lt;/li&gt;
  &lt;li&gt;Roadmap discussions, when stakeholders lobby for competing features, the map provides a framework: “Which impact does this serve?”&lt;/li&gt;
  &lt;li&gt;When the backlog feels disconnected, if nobody can explain why half the items are there, Impact Mapping will either connect them to a goal or expose them as noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;when-not-to-use-it&quot;&gt;When not to use it&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Individual story-level decisions. Impact Mapping operates above the feature level. For “what does this specific story mean,” use Example Mapping.&lt;/li&gt;
  &lt;li&gt;When the goal isn’t clear. Impact Mapping will just expose that gap, useful, but fix the goal first.&lt;/li&gt;
  &lt;li&gt;As a one-off exercise. A map created once and filed away is worthless. Revisit it as you learn. Some hypotheses won’t work. Update the map.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;back-to-greenbox&quot;&gt;Back to Greenbox&lt;/h3&gt;

&lt;p&gt;The session takes about ninety minutes. Tom starts on the pause feature that afternoon. It ships two days later.&lt;/p&gt;

&lt;p&gt;The deploy has a bug. Paused subscribers still get charged. Sam gets three angry emails within an hour. Tom tries to roll back but his deploy script only goes forward, there’s no way to swap back to the previous version. He has to fix the bug and deploy again, which takes forty-five minutes. Three subscribers were incorrectly charged $25 each. Maya refunds them personally. Tom writes the rollback capability that evening. “I never want to be unable to undo a deploy again.” It’s a one-line change, keeping the previous version and being able to swap back. Simple but essential.&lt;/p&gt;

&lt;p&gt;Within a fortnight, churn drops noticeably. Two subscribers who’d been about to cancel stay on because they can pause over the Easter holidays. One tells a friend, who signs up.&lt;/p&gt;

&lt;p&gt;It’s a small win. But it’s a &lt;em&gt;connected&lt;/em&gt; win, the team can trace a line from the feature to the behaviour change to the goal. That’s the difference between shipping features and making progress. The map is a set of hypotheses, and this one checked out. Ship the pause button, churn drops. Hypothesis confirmed. Move to the next.&lt;/p&gt;

&lt;p&gt;But a new problem is emerging. The Impact Map has generated a prioritised list of deliverables, and each one is breaking into multiple stories. The pause feature was simple, two days, done. But the referral programme has five stories. The shortfall reporting tool has three. The backlog is growing fast.&lt;/p&gt;

&lt;p&gt;And it’s causing real problems. Priya ships “pause for one week,” but “resume after pause” is three sprints away because Tom is working on referral tracking. From the subscriber’s perspective, they can pause but there’s no way to unpause, a broken experience, not a feature. Meanwhile, Sam has built the shortfall reporting tool for farms, but nobody built the notification that tells Maya a shortfall was reported. The tool exists but it’s disconnected from the workflow.&lt;/p&gt;

&lt;p&gt;The team is shipping individual stories that make sense in isolation but don’t add up to a coherent experience. They need a way to see how everything connects from the user’s perspective, so they can ship things that actually work end to end.&lt;/p&gt;

&lt;p&gt;They need a way to &lt;a href=&quot;/writing/user-story-mapping-seeing-the-whole/&quot;&gt;see the whole&lt;/a&gt;, to lay out the user journey end to end, so they stop shipping puzzle pieces that don’t connect.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: Event Storming a Process</title>
    <link href="/writing/the-workshop-event-storming-a-process/"/>
    <updated>2026-04-13T06:30:00+08:00</updated>
    <id>/writing/the-workshop-event-storming-a-process/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;This is the second of three posts on running Event Storming. The &lt;a href=&quot;/writing/the-workshop-event-storming-a-domain/&quot;&gt;Event Storming a Domain&lt;/a&gt; post is the entry point in Brandolini’s ordering (Alberto Brandolini, inventor of Event Storming, proposes Big Picture → Process Level → Software Design as the natural order) and introduces the technique at a whole-domain scale; if you haven’t read it, start there. This post picks up where Big Picture drops off: a dot-voted hotspot from a Big Picture session is the natural scope of a Process Level session.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Event Storming an Architecture is the next step — zooming further in, turning a Process Level map into a software design. Coming soon.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For the technique in action inside a small startup, see &lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Storming: Building Shared Understanding&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;where-process-level-sits&quot;&gt;Where Process Level sits&lt;/h3&gt;

&lt;p&gt;Process Level is the middle zoom of Event Storming: one flow, small team, three hours, the full Process Modelling palette of events, commands, actors, policies, and read models. Big Picture (&lt;a href=&quot;/writing/the-workshop-event-storming-a-domain/&quot;&gt;the previous post&lt;/a&gt;) sits above it at whole-domain scale on a stripped-down three-colour palette; Software Design sits below it, turning one flow’s wall into aggregates and code boundaries. Most of the time you run Process Level on its own, on a scoped flow — a billing cycle, a deployment pipeline, an incident that crossed a couple of services — without ever running Big Picture first. When you &lt;em&gt;do&lt;/em&gt; run it after Big Picture, the scope comes from a dot-voted hotspot the Big Picture wall surfaced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; a facilitator plus one or two domain experts, at least one developer (include a junior), product or design, and operations/support where the flow touches them. Four to eight people, three hours.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; a wall of events in time order with commands, actors, and selectively policies and read models underneath, plus 3–7 named hotspot piles each with an owner and a next step.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; a specific flow you’re about to build, inherit, or investigate, where the team’s mental models are quietly different, or a dot-voted hotspot from a Big Picture session. Not for whole-domain scope (run Big Picture first), code design (run Architecture), or a flow one team already shares a strong model of.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;the-process-modelling-palette&quot;&gt;The Process Modelling palette&lt;/h3&gt;

&lt;p&gt;Brandolini’s Process Modelling uses six note colours. Four of them carry the backbone of the wall; the other two appear where they add precision.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Orange — events. Things that happened, past tense. The backbone of the wall. &lt;em&gt;“Payment Captured.”&lt;/em&gt; &lt;em&gt;“Stock Reserved.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Blue — commands. The intent that produced the event, present tense, imperative. &lt;em&gt;“Capture Payment.”&lt;/em&gt; &lt;em&gt;“Reserve Stock.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Small yellow square — actors. The &lt;em&gt;person&lt;/em&gt; who issued the command. &lt;em&gt;“Customer.”&lt;/em&gt; &lt;em&gt;“Warehouse picker.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Larger pink rectangle — external systems. Third parties whose vocabulary you don’t own and whose contracts you negotiate against. &lt;em&gt;“Stripe.”&lt;/em&gt; &lt;em&gt;“The carrier API.”&lt;/em&gt; Distinct from actors, separating them is what later drives anti-corruption-layer conversations.&lt;/li&gt;
  &lt;li&gt;Small pink — hotspots. Disagreements, questions, painpoints, anything the room flags for follow-up.&lt;/li&gt;
  &lt;li&gt;Lilac / purple — policies. The &lt;em&gt;“whenever X, then Y”&lt;/em&gt; rules that issue commands in response to events. The chain is always &lt;em&gt;event → policy → command → event&lt;/em&gt;, events don’t cause events directly; something reads the event (a policy, a person, a clock) and decides to issue a command. Most of a Process Level wall’s policies are implicit; when the room agrees on a rule out loud, or when a rule is contested and resolved, it earns a purple sticky.&lt;/li&gt;
  &lt;li&gt;Pale green — read models. The data a policy (or a person) consults before deciding what to do. &lt;em&gt;“Before reserving stock, check current stock level.”&lt;/em&gt; Green notes live next to the policy or command that reads them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re running your first few Process Level sessions, it’s fine to stay on the four-colour backbone (orange, blue, yellow, pink hotspot) and capture policies verbally in the notes. Brandolini’s full palette is what you grow into as the room gets comfortable — not a gate you have to pass before you run the session.&lt;/p&gt;

&lt;h3 id=&quot;intent&quot;&gt;Intent&lt;/h3&gt;

&lt;p&gt;Build one precise shared model of one specific process — a flow, a pipeline, a cycle, an incident, a feature area — with the people who touch it in the same room, so the team building or operating that process has one model, not five.&lt;/p&gt;

&lt;p&gt;The output is a wall of events in time order, commands under them, actors above them, and a prioritised list of the questions the session raised.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-it&quot;&gt;When to use it&lt;/h3&gt;

&lt;p&gt;Reach for Process Level when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You’re about to build a specific flow and the team’s mental models of it are quietly different&lt;/li&gt;
  &lt;li&gt;You’re inheriting a process nobody documented&lt;/li&gt;
  &lt;li&gt;You’re investigating an incident that crossed two or three services&lt;/li&gt;
  &lt;li&gt;You’ve zoomed in from a &lt;a href=&quot;/writing/the-workshop-event-storming-a-domain/&quot;&gt;Big Picture&lt;/a&gt; session and you have a named hotspot to dig into&lt;/li&gt;
  &lt;li&gt;A piece of work is about to cross multiple people’s areas and you want to spot mismatches early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t reach for Process Level when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The scope is the whole business or a whole product line — run &lt;a href=&quot;/writing/the-workshop-event-storming-a-domain/&quot;&gt;Big Picture&lt;/a&gt; first&lt;/li&gt;
  &lt;li&gt;You’re ready to design code — run Event Storming an Architecture&lt;/li&gt;
  &lt;li&gt;Only one team is involved and they share a strong mental model already&lt;/li&gt;
  &lt;li&gt;The scope is one screen, one function, or one isolated job — it’s too small for the ceremony&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;participants&quot;&gt;Participants&lt;/h3&gt;

&lt;p&gt;Facilitator. Does not participate in content; their job is to manage the session. Ideally someone who’s run one of these before; if not, pair with someone who has.&lt;/p&gt;

&lt;p&gt;Domain expert(s). The people who know how the process actually works. For a billing flow, the finance lead. For a deployment pipeline, the SRE who runs it. For an incident review, the engineers who responded. One or two, not a crowd.&lt;/p&gt;

&lt;p&gt;Developers. At least one, and include a junior if you have one. Juniors ask the questions seniors have stopped asking.&lt;/p&gt;

&lt;p&gt;Product or design — whoever will turn the output into stories.&lt;/p&gt;

&lt;p&gt;Operations, support, frontline. For incident, deployment, or support-heavy flows, &lt;em&gt;these are the domain experts&lt;/em&gt;. Don’t tuck them in as afterthoughts.&lt;/p&gt;

&lt;p&gt;Group size: 4–8. Smaller than Big Picture because the scope is tighter. Fewer than four and the conversation is too thin; more than eight and the voices overlap.&lt;/p&gt;

&lt;p&gt;Who to leave out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;End users and customers. People self-censor around the people they serve. Interview users separately; bring their words in on a sticky note.&lt;/li&gt;
  &lt;li&gt;Senior leaders who can’t stop correcting. If the senior &lt;em&gt;is&lt;/em&gt; the domain expert, brief them first: their job is to answer when asked, not to lead.&lt;/li&gt;
  &lt;li&gt;Spectators. Anyone “just observing” absorbs airtime without contributing. Either in or out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;materials-and-timing&quot;&gt;Materials and timing&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Duration&lt;/th&gt;
      &lt;th&gt;Materials&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Arrivals, intro, ground rules&lt;/td&gt;
      &lt;td&gt;~15 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;“What are we doing and why?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Chaotic exploration&lt;/td&gt;
      &lt;td&gt;20 min&lt;/td&gt;
      &lt;td&gt;Orange&lt;/td&gt;
      &lt;td&gt;“What happens?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Timeline&lt;/td&gt;
      &lt;td&gt;30 min&lt;/td&gt;
      &lt;td&gt;Orange + pink&lt;/td&gt;
      &lt;td&gt;“What order? What’s wrong?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Break&lt;/td&gt;
      &lt;td&gt;10 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Commands and actors&lt;/td&gt;
      &lt;td&gt;20 min&lt;/td&gt;
      &lt;td&gt;Blue + yellow (purple + green as rules emerge)&lt;/td&gt;
      &lt;td&gt;“What triggered it? Who did it?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Hotspots&lt;/td&gt;
      &lt;td&gt;30 min&lt;/td&gt;
      &lt;td&gt;Pink&lt;/td&gt;
      &lt;td&gt;“What scares us most?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Wrap-up, owners, next steps&lt;/td&gt;
      &lt;td&gt;15 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;“Who owns what next?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Buffer&lt;/td&gt;
      &lt;td&gt;20 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;~2h 40min inside a 3-hour block&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The four working phases are 100 minutes. The remaining hour is the unglamorous stuff: arrivals, the intro, the mid-session break, the wrap-up, and the conversations that inevitably run long. Don’t try to fill the 20 minutes of slack — you’ll need it.&lt;/p&gt;

&lt;h3 id=&quot;facilitator-playbook&quot;&gt;Facilitator playbook&lt;/h3&gt;

&lt;h4 id=&quot;phase-1--chaotic-exploration-20-min&quot;&gt;Phase 1 — Chaotic exploration (20 min)&lt;/h4&gt;

&lt;p&gt;Before the first sticky goes up, do two things that look trivial and aren’t.&lt;/p&gt;

&lt;p&gt;Set the safety out loud:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“The only rule for this phase is that every note is valid. Duplicates are fine, half-formed ideas are fine, things that might be wrong are fine — that’s exactly what we’re here to find. If you’re not sure, write it anyway.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Set the granularity. Stick two example events on the wall yourself at the level a domain expert would say them out loud:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Payment Submitted.” “Parcel Dispatched.” “Alert Fired.” “Deployment Rolled Back.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hand out orange pads. Name the most junior person in the room:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“[Name], what’s the first event you can think of for this flow? Doesn’t have to be the start — just the first one that comes to mind.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then give the instruction that covers the rest of the phase:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Write silently. Get everything you can think of onto orange notes and onto the wall. Don’t worry about order. Don’t worry about duplicates. Twenty minutes on the clock, then we stop.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No talking until the timer goes off. By the end you should have 40–80 notes. Some will be duplicates; some will contradict; some will make no sense yet. That’s exactly right.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Someone talking instead of writing. Gently: &lt;em&gt;“Get it on a sticky note.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Someone waiting for permission. &lt;em&gt;“Duplicates are gold. Write yours anyway.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;One person filling the wall while others have three notes. Usually sorts itself out in the timeline phase; keep an eye on it.&lt;/li&gt;
  &lt;li&gt;Someone reaching for a pink note already. &lt;em&gt;“Good instinct — hold that thought.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-2--timeline-30-min&quot;&gt;Phase 2 — Timeline (30 min)&lt;/h4&gt;

&lt;p&gt;Now everyone talks. The job is to arrange the orange notes left-to-right in chronological order.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Let’s put these in order. Talk to each other. If you disagree, put a pink note on it and we’ll come back.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the fifteen-minute mark, the wall will look messy and you’ll worry. That is what success looks like midway through. There’ll be clumps where people stood; gaps where nobody’s arranged yet; two notes stacked because someone tried to merge them and gave up; overlapping candidates for the first event; a couple of pink notes nailed into contested spots. If the wall looks tidy at the fifteen-minute mark, either the scope was too small or one person is doing all the moving.&lt;/p&gt;

&lt;p&gt;Merge obvious duplicates. Leave ambiguous ones — if two notes &lt;em&gt;might&lt;/em&gt; be the same event, that’s a conversation worth having later.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One person placing every note while everyone else watches. The most common failure mode. Pair a developer with the domain expert and ask them to walk a section together.&lt;/li&gt;
  &lt;li&gt;No pink notes appearing. Disagreements are hidden, not absent. Prompt: &lt;em&gt;“Is anything on this wall surprising you?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Rabbit holes into solution design. &lt;em&gt;“Great implementation idea — park it. We’re mapping, not building.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Parallel flows emerging. Let them spread vertically into swim lanes, horizontal bands on the wall, one per actor or system, used to keep parallel flows visually separate. A rollout flow and a rollback flow can share a wall.&lt;/li&gt;
  &lt;li&gt;Events causing events. Someone asks &lt;em&gt;“so does Payment Captured cause Stock Reserved?”&lt;/em&gt; Name the rule: &lt;em&gt;“Events don’t cause events. Something reads Payment Captured — a person, a rule, a clock — and decides to reserve stock. The chain is always event → decision → command → event, not event → event.”&lt;/em&gt; First-timers want to draw arrows between orange notes within the first hour. Name the rule before they do.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-3--commands-and-actors-20-min&quot;&gt;Phase 3 — Commands and actors (20 min)&lt;/h4&gt;

&lt;p&gt;Hand out blue and yellow notes. Introduce them one colour at a time — if you drop both on the table at once, people grab whichever is closest and the wall gets noisy.&lt;/p&gt;

&lt;p&gt;Blue first — commands. For each event, what intent produced it?&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Blue notes go &lt;em&gt;below&lt;/em&gt; the orange event. They’re the command that made it happen. ‘Submit Payment’ caused ‘Payment Submitted’. ‘Pick Order’ caused ‘Items Picked’. Every event has a command somewhere — even if the command is a scheduled job or a reaction to another event.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yellow next — actors. Who issued the command?&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Small yellow squares go &lt;em&gt;above&lt;/em&gt; the command. An actor is a person, a role, or a system. ‘Customer’ is fine; ‘the system’ is not — which system? ‘Warehouse picker.’ ‘Stripe.’ ‘Nightly cron.’ Be specific enough that the name points to someone or something you could actually talk to.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two discipline points most first-time facilitators miss:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Small yellow squares, not full-width notes. The actor sits next to or on top of the command; it’s smaller than the command, because the command is the important bit at this level.&lt;/li&gt;
  &lt;li&gt;Deduplicate. If the same actor issues three commands in a row, you don’t need three yellow squares — stick one next to the first command and let the row speak for itself. Real ES walls have one or two actor squares per band, not one per event.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;“The system” on too many yellow squares. &lt;em&gt;“Which system? Automated or manual? What happens when it fails?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Business rules hiding inside a command. &lt;em&gt;“Wait, this command only runs sometimes — what decides?”&lt;/em&gt; If the room can name the rule out loud, purple-note it in &lt;em&gt;“whenever X, then Y”&lt;/em&gt; form between the triggering event and the command; if the decision needs a fact (a balance, a stock level, a flag), stick a pale-green read model next to the policy. If the rule is contested, leave it as a pink hotspot for the next phase to resolve.&lt;/li&gt;
  &lt;li&gt;One person’s name on multiple recurring actors. Scaling bottleneck. Pink note.&lt;/li&gt;
  &lt;li&gt;Commands that nobody can explain. &lt;em&gt;“Who decides this?”&lt;/em&gt; followed by silence is extremely valuable. Pink note.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-4--hotspots-30-min&quot;&gt;Phase 4 — Hotspots (30 min)&lt;/h4&gt;

&lt;p&gt;Gather the pink notes — the ones you’ve been accumulating on the wall, plus new ones you’ll generate by prompting for them. These are the most valuable output of the session.&lt;/p&gt;

&lt;p&gt;The mechanics matter more than most facilitators realise; clustering pinks under time pressure is where first-time facilitators freeze. A shape that works:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Read the pinks aloud, one by one (5 min). You walk the wall and read each pink note. No commentary — just read. This refreshes the room and gives you time to spot repetition.&lt;/li&gt;
  &lt;li&gt;Move notes into rough piles (10 min). Take the pinks off the wall and put them into 3–7 piles on a table or a clear section of wall. Let the room help. If a note could go in two piles, put it in the bigger one. The goal isn’t clean boundaries; it’s rough themes.&lt;/li&gt;
  &lt;li&gt;Name each pile (5 min). For each pile, write a one-sentence theme on a fresh pink note and put it on top. &lt;em&gt;“Rules nobody has written down.”&lt;/em&gt; &lt;em&gt;“Cross-team handoffs with no SLA.”&lt;/em&gt; &lt;em&gt;“Edge cases we deferred.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Owner and next step per pile (10 min). &lt;em&gt;“Who owns finding the answer? What’s the next step?”&lt;/em&gt; Write both on the theme note. Time-box 90 seconds per pile; if a conversation runs long, park it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don’t try to solve anything in-session. Identify, name, assign, move on. Solving is not this phase’s job.&lt;/p&gt;

&lt;h3 id=&quot;worked-example--pagebounds-order-to-delivery-flow&quot;&gt;Worked example — Pagebound’s order-to-delivery flow&lt;/h3&gt;

&lt;p&gt;Pagebound is a mid-sized online independent bookshop: about 200,000 customers, six warehouses, an engineering team of thirty, a customer support operation that fields returns and lost parcels. A recent &lt;a href=&quot;/writing/the-workshop-event-storming-a-domain/&quot;&gt;Big Picture&lt;/a&gt; session on the whole customer experience produced a prioritised list of hotspots; the top one was &lt;em&gt;“when do we reserve stock?”&lt;/em&gt; — commerce said at checkout, the warehouse said at payment captured, and both teams had been operating on their own model for eighteen months.&lt;/p&gt;

&lt;p&gt;The Process Level session scope, in one phrase: the order-to-delivery flow, from checkout started through parcel delivered. Six people in the room — commerce lead, a commerce engineer, the fulfilment team lead, a warehouse supervisor, the SRE who owns the payment integration, and a customer-success lead who’s been fielding the over-sold complaints.&lt;/p&gt;

&lt;p&gt;Here’s what the wall looks like at the end of the session — fifteen events in rough time order, commands underneath, small yellow actor squares deduplicated per band, one purple policy and its pale-green read model where the room agreed on a contested rule, and four pink hotspots showing the questions the session raised:&lt;/p&gt;

&lt;link href=&quot;https://fonts.googleapis.com/css2?family=Kalam:wght@400;700&amp;amp;display=swap&quot; rel=&quot;stylesheet&quot; /&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1700 780&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Kalam&apos;, &apos;Segoe Print&apos;, &apos;Comic Sans MS&apos;, cursive;&quot; role=&quot;img&quot; aria-label=&quot;Process Level wall for Pagebound&apos;s order-to-delivery flow: fifteen events in rough time order, actor bands for customer, warehouse, and carrier, a purple policy note with its green read model, and four pink hotspots.&quot;&gt;
  &lt;defs&gt;
    &lt;filter id=&quot;wobble-pl&quot; x=&quot;-5%&quot; y=&quot;-5%&quot; width=&quot;110%&quot; height=&quot;110%&quot;&gt;
      &lt;feTurbulence type=&quot;fractalNoise&quot; baseFrequency=&quot;0.02&quot; numOctaves=&quot;2&quot; seed=&quot;7&quot; result=&quot;n&quot; /&gt;
      &lt;feDisplacementMap in=&quot;SourceGraphic&quot; in2=&quot;n&quot; scale=&quot;2&quot; /&gt;
    &lt;/filter&gt;
    &lt;style&gt;
      .pl-sticky { stroke: #1a1a1a; stroke-width: 2; filter: url(#wobble-pl); }
      .pl-event { fill: #ffb84d; }
      .pl-command { fill: #a8c8ec; }
      .pl-actor { fill: #fff1a1; }
      .pl-policy { fill: #c9a3e0; }
      .pl-read { fill: #bfe3b4; }
      .pl-hotspot { fill: #f4a6c0; }
      .pl-band { stroke: #d4b833; stroke-width: 3; stroke-opacity: 0.55; stroke-linecap: round; fill: none; stroke-dasharray: 4 5; }
      .pl-title { font-size: 11px; font-weight: 700; fill: #1a1a1a; text-transform: uppercase; letter-spacing: 0.06em; }
      .pl-body { font-size: 14px; fill: #1a1a1a; }
      .pl-actor-body { font-size: 12px; fill: #1a1a1a; }
      .pl-policy-text { font-size: 12px; fill: #1a1a1a; font-style: italic; }
      .pl-read-text { font-size: 12px; fill: #1a1a1a; }
      .pl-hotspot-text { font-size: 12px; fill: #1a1a1a; font-style: italic; }
      .pl-lane-label { font-size: 14px; fill: #4a4540; font-style: italic; }
    &lt;/style&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;30&quot; y=&quot;260&quot; class=&quot;pl-lane-label&quot;&gt;Order to Delivery&lt;/text&gt;

  &lt;g transform=&quot;translate(130, 40)&quot;&gt;&lt;rect width=&quot;70&quot; height=&quot;44&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-actor&quot; /&gt;&lt;text x=&quot;35&quot; y=&quot;18&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;actor&lt;/text&gt;&lt;text x=&quot;35&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;pl-actor-body&quot;&gt;customer&lt;/text&gt;&lt;/g&gt;
  &lt;path d=&quot;M 200 62 L 430 62&quot; class=&quot;pl-band&quot; /&gt;

  &lt;g transform=&quot;translate(90, 100)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Check Out&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(90, 180)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Checkout&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Started&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(310, 100)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Submit Payment&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(310, 180)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Payment&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Captured&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(570, 40)&quot;&gt;&lt;rect width=&quot;70&quot; height=&quot;44&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-actor&quot; /&gt;&lt;text x=&quot;35&quot; y=&quot;18&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;actor&lt;/text&gt;&lt;text x=&quot;35&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;pl-actor-body&quot;&gt;order svc&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(530, 100)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Reserve Stock&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(530, 180)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Stock&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Reserved&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(810, 40)&quot;&gt;&lt;rect width=&quot;70&quot; height=&quot;44&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-actor&quot; /&gt;&lt;text x=&quot;35&quot; y=&quot;18&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;actor&lt;/text&gt;&lt;text x=&quot;35&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;pl-actor-body&quot;&gt;warehouse&lt;/text&gt;&lt;/g&gt;
  &lt;path d=&quot;M 880 62 L 1330 62&quot; class=&quot;pl-band&quot; /&gt;

  &lt;g transform=&quot;translate(770, 100)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Pick Items&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(770, 180)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Items&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Picked&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(990, 100)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Pack Order&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(990, 180)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Order&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Packed&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(1210, 100)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Print Label&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1210, 180)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Label&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Printed&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(1460, 40)&quot;&gt;&lt;rect width=&quot;70&quot; height=&quot;44&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-actor&quot; /&gt;&lt;text x=&quot;35&quot; y=&quot;18&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;actor&lt;/text&gt;&lt;text x=&quot;35&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;pl-actor-body&quot;&gt;carrier&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1430, 100)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Hand Off&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1430, 180)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Handed to&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Carrier&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(310, 280)&quot;&gt;
    &lt;rect width=&quot;280&quot; height=&quot;56&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-policy&quot; /&gt;
    &lt;text x=&quot;140&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;policy&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;42&quot; text-anchor=&quot;middle&quot; class=&quot;pl-policy-text&quot;&gt;whenever Payment Captured&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;56&quot; text-anchor=&quot;middle&quot; class=&quot;pl-policy-text&quot;&gt;→ reserve stock&lt;/text&gt;
  &lt;/g&gt;
  &lt;g transform=&quot;translate(610, 280)&quot;&gt;
    &lt;rect width=&quot;170&quot; height=&quot;56&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-read&quot; /&gt;
    &lt;text x=&quot;85&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;read model&lt;/text&gt;
    &lt;text x=&quot;85&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-read-text&quot;&gt;Current Stock Level&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(130, 370)&quot;&gt;&lt;rect width=&quot;70&quot; height=&quot;44&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-actor&quot; /&gt;&lt;text x=&quot;35&quot; y=&quot;18&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;actor&lt;/text&gt;&lt;text x=&quot;35&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;pl-actor-body&quot;&gt;carrier&lt;/text&gt;&lt;/g&gt;
  &lt;path d=&quot;M 200 392 L 660 392&quot; class=&quot;pl-band&quot; /&gt;

  &lt;g transform=&quot;translate(90, 430)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Scan Parcel&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(90, 510)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Scanned&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;At Hub&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(310, 430)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Load Van&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(310, 510)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Out For&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Delivery&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(530, 430)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Deliver&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(530, 510)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Parcel&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Delivered&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(810, 370)&quot;&gt;&lt;rect width=&quot;70&quot; height=&quot;44&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-actor&quot; /&gt;&lt;text x=&quot;35&quot; y=&quot;18&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;actor&lt;/text&gt;&lt;text x=&quot;35&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;pl-actor-body&quot;&gt;customer&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(770, 430)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;60&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-command&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;20&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;command&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Open Parcel&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(770, 510)&quot;&gt;&lt;rect width=&quot;180&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-event&quot; /&gt;&lt;text x=&quot;90&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;event&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;48&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Book&lt;/text&gt;&lt;text x=&quot;90&quot; y=&quot;64&quot; text-anchor=&quot;middle&quot; class=&quot;pl-body&quot;&gt;Received&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(990, 430)&quot;&gt;
    &lt;rect width=&quot;260&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-hotspot&quot; /&gt;
    &lt;text x=&quot;130&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;hotspot&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;42&quot; text-anchor=&quot;middle&quot; class=&quot;pl-hotspot-text&quot;&gt;What if payment captured but&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;58&quot; text-anchor=&quot;middle&quot; class=&quot;pl-hotspot-text&quot;&gt;no stock? Backorder vs cancel?&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(1270, 430)&quot;&gt;
    &lt;rect width=&quot;260&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-hotspot&quot; /&gt;
    &lt;text x=&quot;130&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;hotspot&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;42&quot; text-anchor=&quot;middle&quot; class=&quot;pl-hotspot-text&quot;&gt;Substitution for out-of-stock:&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;58&quot; text-anchor=&quot;middle&quot; class=&quot;pl-hotspot-text&quot;&gt;who authorises, how is customer told?&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(990, 520)&quot;&gt;
    &lt;rect width=&quot;260&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-hotspot&quot; /&gt;
    &lt;text x=&quot;130&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;hotspot&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;42&quot; text-anchor=&quot;middle&quot; class=&quot;pl-hotspot-text&quot;&gt;Handoff gap: ~4% parcels vanish&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;58&quot; text-anchor=&quot;middle&quot; class=&quot;pl-hotspot-text&quot;&gt;between Hand Off and Scanned.&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(1270, 520)&quot;&gt;
    &lt;rect width=&quot;260&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;pl-sticky pl-hotspot&quot; /&gt;
    &lt;text x=&quot;130&quot; y=&quot;22&quot; text-anchor=&quot;middle&quot; class=&quot;pl-title&quot;&gt;hotspot&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;42&quot; text-anchor=&quot;middle&quot; class=&quot;pl-hotspot-text&quot;&gt;Delivery to wrong address:&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;58&quot; text-anchor=&quot;middle&quot; class=&quot;pl-hotspot-text&quot;&gt;whose problem, what&apos;s the SLA?&lt;/text&gt;
  &lt;/g&gt;

  &lt;text x=&quot;850&quot; y=&quot;640&quot; text-anchor=&quot;middle&quot; class=&quot;pl-lane-label&quot;&gt;Fifteen events across two rows (the process is too wide for one horizontal line), three actor bands per row,&lt;/text&gt;
  &lt;text x=&quot;850&quot; y=&quot;660&quot; text-anchor=&quot;middle&quot; class=&quot;pl-lane-label&quot;&gt;a purple policy with its pale-green read model capturing the rule the room agreed out loud,&lt;/text&gt;
  &lt;text x=&quot;850&quot; y=&quot;680&quot; text-anchor=&quot;middle&quot; class=&quot;pl-lane-label&quot;&gt;and four pink hotspots surfaced during the session. The other implicit policies are left unspoken.&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;Notice what’s on the wall and what isn’t. The actor bands show that the customer initiates the first two events, then the order service quietly does its work, then the warehouse takes over for three events, then the carrier, then the customer again at the end. That’s five handoffs — and every handoff is a candidate for something to go wrong, which is why three of the four pink notes cluster around handoff boundaries.&lt;/p&gt;

&lt;p&gt;One purple policy made it onto the wall, and getting it there is what the session set out to do: the room resolved the stock-reservation timing question out loud, decided that &lt;em&gt;“whenever Payment Captured → reserve stock”&lt;/em&gt; was the right rule, and stuck it up so nobody could quietly forget later. A pale-green read model beside it names the fact the policy depends on — &lt;em&gt;current stock level&lt;/em&gt; — because the next thing the team will argue about is what happens when that number is zero, and pinning the read model now saves an argument later. The other implicit policies (events quietly triggering subsequent commands) are left unspoken — that’s fine for Process Level; the Architecture session is where every crossing gets a policy and every policy gets its read model.&lt;/p&gt;

&lt;p&gt;The four pink hotspots are the real output. Two (over-sold orders, substitution) will turn into Example Mapping sessions with business rules attached. One (the handoff gap) becomes an investigation with the carrier. One (wrong-address SLA) becomes a conversation between customer success and legal. None of them get “solved” at this session, and trying to would burn the next two hours on arguments that belong in their own meetings.&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What can go wrong&lt;/h3&gt;

&lt;p&gt;Named failure modes.&lt;/p&gt;

&lt;p&gt;The silent room. Nobody is writing or talking.
  &lt;em&gt;Recovery:&lt;/em&gt; The prompt is too abstract. Make it concrete: &lt;em&gt;“What’s the first thing that happens when a customer clicks ‘place order’?”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; 20 minutes in and the wall is still empty. Scope is wrong, people are wrong, or there’s a political problem you haven’t named.&lt;/p&gt;

&lt;p&gt;The lecture. One expert explains while everyone listens politely.
  &lt;em&gt;Recovery:&lt;/em&gt; Pair people up, give each pair a section of the wall.
  &lt;em&gt;Stop if:&lt;/em&gt; Two pairs in and it’s still the same voice. The session is producing one person’s model.&lt;/p&gt;

&lt;p&gt;The argument. Two people disagree about how something works.
  &lt;em&gt;Recovery:&lt;/em&gt; Let it play for 2–3 minutes. This is often the session working. If it’s not resolving, pink note it.
  &lt;em&gt;Stop if:&lt;/em&gt; The argument has gone personal. Break; resume only if the air has cleared.&lt;/p&gt;

&lt;p&gt;The solution-jumper. Someone keeps designing the code instead of mapping the process.
  &lt;em&gt;Recovery:&lt;/em&gt; &lt;em&gt;“Great implementation idea — park it.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; They can’t hold the distinction after a third prompt. They belong in an Architecture session, not this one.&lt;/p&gt;

&lt;p&gt;The missing person. Nobody in the room knows how a key part of the process works.
  &lt;em&gt;Recovery:&lt;/em&gt; Pink note it with a name. &lt;em&gt;“Need to talk to [person] about [topic].”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; Multiple key parts are owned by people not in the room. Reschedule with the right attendees.&lt;/p&gt;

&lt;p&gt;The political silence. A senior is in the room and the juniors have stopped writing.
  &lt;em&gt;Recovery:&lt;/em&gt; Pair juniors with peers away from the senior; or ask the senior to step out for a call (briefed in advance); or enforce silent writing with no exceptions.
  &lt;em&gt;Stop if:&lt;/em&gt; None of the above shifts the dynamic. Photograph what’s on the wall, reschedule.&lt;/p&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;Same day, 24 hours:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Panoramic high-resolution photographs of the wall.&lt;/li&gt;
  &lt;li&gt;A transcribed event list, command list, and hotspot list (each pile named, owner, next step) in a shared document.&lt;/li&gt;
  &lt;li&gt;A short summary to participants: &lt;em&gt;“Here’s what we found, here’s what happens next.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product owner’s (or equivalent’s) week:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Turn each event into a vocabulary entry. &lt;em&gt;“Stock Reserved”&lt;/em&gt; means exactly one thing; defend the phrase against drift.&lt;/li&gt;
  &lt;li&gt;Triage the hotspots. Each pile becomes one of: (a) work for this sprint, (b) a time-boxed investigation, (c) a follow-up workshop (Example Mapping, Decision Tables, Architecture), (d) a conversation. Resolve the ones that block the next sprint; defer the rest.&lt;/li&gt;
  &lt;li&gt;Book the follow-ups. Don’t let momentum dissipate.&lt;/li&gt;
  &lt;li&gt;Walk the wall with anyone who couldn’t attend. Their perspective often surfaces hotspots the original room missed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-event-storming-a-domain/&quot;&gt;Event Storming a Domain&lt;/a&gt; — zoom out when you realise the scope of your Process Level problem is actually organisational, not procedural.&lt;/li&gt;
  &lt;li&gt;Event Storming an Architecture — the natural next step when the Process Level flow is clear and you’re about to turn it into code boundaries.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Storming: Building Shared Understanding&lt;/a&gt; — the narrative version on a smaller team.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Workshop: Event Storming a Domain</title>
    <link href="/writing/the-workshop-event-storming-a-domain/"/>
    <updated>2026-04-11T06:30:00+08:00</updated>
    <id>/writing/the-workshop-event-storming-a-domain/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;This is the first of three posts on running Event Storming. Brandolini presents the technique starting from here. Big Picture, the widest zoom, because it’s the session you usually reach for first when you step into an unfamiliar domain. The other two posts zoom in:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;&lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming a Process&lt;/a&gt;: the default, smaller session you’ll run most often. Holds the full four-colour palette and the shape of a standard three-hour session.&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Event Storming an Architecture: zooms further in, turning a Process Level map into a software design. Coming soon.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;For the technique in action inside a small startup, see &lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Storming: Building Shared Understanding&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;about-event-storming&quot;&gt;About Event Storming&lt;/h3&gt;

&lt;p&gt;Event Storming gathers everyone who touches a domain in front of a long wall and asks them, silently and in parallel, to write down &lt;em&gt;things that happened&lt;/em&gt; on orange sticky notes, in past tense, one fact per note. &lt;em&gt;“Order Placed.”&lt;/em&gt; &lt;em&gt;“Payment Captured.”&lt;/em&gt; &lt;em&gt;“Parcel Delivered.”&lt;/em&gt; The notes go up; the timeline gets enforced left-to-right; the arguments that break out over where a note belongs become the thing you came for. The output is a shared wall, pink hotspots marking the places that hurt, and a dot-voted shortlist of what to investigate next. Big Picture is the widest of the three Event Storming zooms; if you already know which single flow you want to map, you want &lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Process Level&lt;/a&gt; instead, and if you’re ready to turn a process into code boundaries, you want Architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Who, for how long:&lt;/em&gt; one or two facilitators (always two above ten people), domain experts from every slice of the domain (two per slice), developers and architects to listen, frontline operations and support, and a sponsor who opens and leaves. Eight to twenty people, a full day minimum, often two.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;What you walk out with:&lt;/em&gt; a wall the room agrees on (orange events, pink hotspots, yellow systems and people, green opportunities, red pivotal moments), panoramic photos and a transcribed event/system/hotspot list within 24 hours, a glossary of the vocabulary that emerged, and three to five follow-up Process Level sessions booked within two weeks, one per dot-voted hotspot.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;When to reach for it:&lt;/em&gt; a major initiative is starting and several teams need one picture before anyone commits, you’re new to an organisation and nobody can describe the domain end-to-end, or an incident crossed services and the timeline lives in Slack and people’s heads. Not for designing code (that’s Architecture), not for mapping a single known flow (that’s Process Level), and not when leadership will sit in and correct the frontline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;intent&quot;&gt;Intent&lt;/h3&gt;

&lt;p&gt;Build one shared picture of a whole domain (a product, a platform, a business line, a customer experience) with everyone who owns a piece of it in the same room, so the organisation can see itself end-to-end and pick the hotspots worth investigating.&lt;/p&gt;

&lt;p&gt;The output isn’t a design, a roadmap, or a plan. It’s a long wall of orange stickies the room &lt;em&gt;agrees on&lt;/em&gt;, pink notes marking the places that hurt, and a prioritised shortlist of follow-up sessions.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-it&quot;&gt;When to use it&lt;/h3&gt;

&lt;p&gt;Reach for Big Picture when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A major initiative is starting and several teams need one picture before anyone commits&lt;/li&gt;
  &lt;li&gt;You’re new to an organisation and nobody can describe the domain end-to-end without stopping three times to ask someone else&lt;/li&gt;
  &lt;li&gt;An incident crossed six services and the timeline lives in Slack, git history, and people’s heads&lt;/li&gt;
  &lt;li&gt;Two companies are integrating and both sides need to see each other’s domains&lt;/li&gt;
  &lt;li&gt;You’re a consultant and the client has asked for “help with architecture” but you don’t yet know what help means&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t reach for Big Picture when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You know which specific flow you need to work on: run &lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Process Level&lt;/a&gt; on that flow&lt;/li&gt;
  &lt;li&gt;You’re ready to design code: run Event Storming an Architecture&lt;/li&gt;
  &lt;li&gt;You can’t get the right people in the room for most of a day&lt;/li&gt;
  &lt;li&gt;Leadership will sit in and correct people; you’ll get political theatre, not discovery&lt;/li&gt;
  &lt;li&gt;The scope is one team, one product, one well-understood flow; it’s too much machine for the job&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;scope-the-hardest-decision-before-the-session&quot;&gt;Scope: the hardest decision before the session&lt;/h3&gt;

&lt;p&gt;The single most common way Big Picture sessions go wrong is the scope being wrong. Not too ambitious; wrong-shaped. Two failure modes to avoid:&lt;/p&gt;

&lt;p&gt;Too big. &lt;em&gt;“Map the whole enterprise.”&lt;/em&gt; An enterprise with five product lines, three channels, and two regulatory contexts is five or six separate Big Pictures, not one. If you find yourself asking &lt;em&gt;“whose slice do we even start with?”&lt;/em&gt;, split.&lt;/p&gt;

&lt;p&gt;Too small. &lt;em&gt;“Map the deployment pipeline.”&lt;/em&gt; That’s a single process; it’ll fit comfortably in a Process Level session and won’t need twelve people in a room for a day.&lt;/p&gt;

&lt;p&gt;The sweet spot. Something you can describe in one short phrase that (a) spans 3–6 teams, (b) is coherent enough to fit on one wall over a day, and (c) nobody in the organisation currently owns end-to-end. &lt;em&gt;“The billing platform.”&lt;/em&gt; &lt;em&gt;“Customer onboarding.”&lt;/em&gt; &lt;em&gt;“Our order-to-cash.”&lt;/em&gt; &lt;em&gt;“The claims lifecycle.”&lt;/em&gt; If several different people in the organisation each own a piece and none owns the whole, you’re on.&lt;/p&gt;

&lt;h3 id=&quot;participants&quot;&gt;Participants&lt;/h3&gt;

&lt;p&gt;Facilitator(s). Two for groups above ten, always. One watches the wall; one watches the room. Big Picture is harder to facilitate than Process Level because the group is bigger and the failure modes are more political. Don’t run your first one alone.&lt;/p&gt;

&lt;p&gt;Domain experts from every part of the domain. The rule: if a slice isn’t represented in the room, it’ll be missing from the wall. For an e-commerce business that means product, engineering, operations, support, finance, logistics, maybe marketing. For a bank it means front office, back office, compliance, risk, IT. Two people per slice: one with deep domain knowledge, one with freshest-to-the-job eyes.&lt;/p&gt;

&lt;p&gt;Developers and architects. Not to design; to listen, write, and discover where the business model and the code model have quietly diverged.&lt;/p&gt;

&lt;p&gt;Operations and frontline support. Where the surprises live. If the leadership team says the product works one way and the support team sees something different, Big Picture is where both of those truths land on the same wall. Don’t tuck them in as afterthoughts.&lt;/p&gt;

&lt;p&gt;Sometimes leadership, with care. A sponsor who opens the session and then leaves is useful. A leader who sits in and corrects every event they disagree with kills the session. Brief them before; if they can’t hold the discipline, run without them.&lt;/p&gt;

&lt;p&gt;Group size: 8–20. Below 8 and you’re not spanning enough of the domain; above 20 and the conversations fragment and some voices stop contributing.&lt;/p&gt;

&lt;h3 id=&quot;before-the-session&quot;&gt;Before the session&lt;/h3&gt;

&lt;p&gt;The single biggest lever on outcome quality isn’t what happens in the room; it’s the week before. Meet the sponsor and agree four things:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Scope, in one short phrase. If you can’t both say it the same way, don’t schedule the session yet.&lt;/li&gt;
  &lt;li&gt;The guest list. Every slice represented by one or two names; no political attendees.&lt;/li&gt;
  &lt;li&gt;The sponsor’s role during the session. Ideally: open, leave, come back for the wrap-up. Explicitly negotiate this. If they won’t hold it, reschedule.&lt;/li&gt;
  &lt;li&gt;The question the output has to answer. Not &lt;em&gt;“do a Big Picture”&lt;/em&gt; (that’s the method, not the outcome). &lt;em&gt;“Give us a prioritised list of cross-team investigations worth running next.”&lt;/em&gt; &lt;em&gt;“Give us one shared picture we can point at when we disagree later.”&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without this, you’re flipping coins. With it, you’ve done half the facilitation before the first sticky goes up.&lt;/p&gt;

&lt;h3 id=&quot;materials-and-timing&quot;&gt;Materials and timing&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;Duration&lt;/th&gt;
      &lt;th&gt;Materials&lt;/th&gt;
      &lt;th&gt;Key question&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Sponsor opens; ground rules&lt;/td&gt;
      &lt;td&gt;15–20 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;“Why are we here?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Chaotic exploration&lt;/td&gt;
      &lt;td&gt;60–90 min&lt;/td&gt;
      &lt;td&gt;Orange notes&lt;/td&gt;
      &lt;td&gt;“What happens in this domain?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Enforce the timeline&lt;/td&gt;
      &lt;td&gt;45–60 min&lt;/td&gt;
      &lt;td&gt;Orange notes, pink notes&lt;/td&gt;
      &lt;td&gt;“What order? What’s contested?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Reverse narrative&lt;/td&gt;
      &lt;td&gt;20–30 min&lt;/td&gt;
      &lt;td&gt;Orange, pink&lt;/td&gt;
      &lt;td&gt;“What had to be true for this?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Break&lt;/td&gt;
      &lt;td&gt;30–60 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Explicit walkthrough&lt;/td&gt;
      &lt;td&gt;60–120 min&lt;/td&gt;
      &lt;td&gt;A walker, listeners&lt;/td&gt;
      &lt;td&gt;“Does this match what you know?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Pain and systems&lt;/td&gt;
      &lt;td&gt;60 min&lt;/td&gt;
      &lt;td&gt;Pink, yellow&lt;/td&gt;
      &lt;td&gt;“Where does this hurt?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Dot-voting&lt;/td&gt;
      &lt;td&gt;20–30 min&lt;/td&gt;
      &lt;td&gt;Sticky dots&lt;/td&gt;
      &lt;td&gt;“Which hotspots matter most?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Wrap-up, owners, next steps&lt;/td&gt;
      &lt;td&gt;20–30 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;“Who does what next?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Buffer&lt;/td&gt;
      &lt;td&gt;30–60 min&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;Plan for a full day, minimum. Two days is common. Three for complex domains.&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Big Picture is the most expensive of the three Event Storming levels by a wide margin, and the easiest to do badly. It isn’t something you cram into an afternoon.&lt;/p&gt;

&lt;h3 id=&quot;a-note-on-note-colours&quot;&gt;A note on note colours&lt;/h3&gt;

&lt;p&gt;At Big Picture, you deliberately use &lt;em&gt;fewer&lt;/em&gt; colours than you would at Process Level. Brandolini’s rule: you’re looking for shape, not precision. The palette:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Orange: domain events, in past tense. The backbone of the wall. &lt;em&gt;“Order Placed.”&lt;/em&gt; &lt;em&gt;“Parcel Delivered.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Pink: hotspots, painpoints, disagreements, questions, places where the room stops agreeing. Every pink note is a candidate for follow-up.&lt;/li&gt;
  &lt;li&gt;Yellow: systems and people, loosely. &lt;em&gt;“Stripe.”&lt;/em&gt; &lt;em&gt;“Our warehouse.”&lt;/em&gt; &lt;em&gt;“The customer.”&lt;/em&gt; Don’t worry about the person/system distinction at this level; stick it on the wall and let Process Level sort it out.&lt;/li&gt;
  &lt;li&gt;Green: opportunities. &lt;em&gt;“Could we let subscribers preview next week’s box?”&lt;/em&gt; &lt;em&gt;“Could the carrier handle returns themselves?”&lt;/em&gt; The lightbulb sticky, the thing that isn’t happening yet but the room thinks should be. A Big-Picture-only colour. Encourage them throughout exploration; they’re where productive arguments often start.&lt;/li&gt;
  &lt;li&gt;Red (or a tall vertical line): pivotal events. The four to eight key moments where the state of the domain fundamentally changes. They emerge during the timeline phase and divide the wall into phases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the whole palette. No blue commands. No purple policies. (Blue means commands, intentions, things someone is doing; purple means policies, “when X happens, do Y” rules; green-as-read-model means query-shaped projections of state. All three belong at Process Level, not here.) Those belong at Process Level. If you reach for them here, you’re on the wrong level. Note that the green sticky at Big Picture means &lt;em&gt;opportunity&lt;/em&gt;, distinct from Process Level’s green &lt;em&gt;read model&lt;/em&gt; sticky. Same colour, different meaning, different level.&lt;/p&gt;

&lt;h3 id=&quot;facilitator-playbook&quot;&gt;Facilitator playbook&lt;/h3&gt;

&lt;p&gt;The exact phase structure varies by practitioner. Here’s a shape that works for a one-day session on a medium-sized domain (8–15 people). Scale the timings up for two-day sessions.&lt;/p&gt;

&lt;h4 id=&quot;phase-1-chaotic-exploration-6090-min&quot;&gt;Phase 1: Chaotic exploration (60–90 min)&lt;/h4&gt;

&lt;p&gt;Set the safety out loud:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Every note is valid. Duplicates are fine. Things that might be wrong are fine; that’s exactly the kind of note this session lives on. If you’re not sure whether something counts as an event, stick it up anyway and we’ll sort it out later. Silent writing for the next hour. No talking; the wall does the talking.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Set the granularity with a mix of examples from across the domain:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“These are events: things that happened, past tense. &lt;em&gt;Order Placed&lt;/em&gt;. &lt;em&gt;Adjuster Assigned&lt;/em&gt;. &lt;em&gt;Complaint Filed&lt;/em&gt;. &lt;em&gt;Integration Deployed&lt;/em&gt;. &lt;em&gt;Account Suspended&lt;/em&gt;. Write at the level a domain expert would say it out loud, not a database-row level, not a strategy level.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Name the most junior or most frontline person in the room and ask them to stick up the first note. The pattern you’re setting: this is a working session, not an executive meeting, and the least senior person writes first.&lt;/p&gt;

&lt;p&gt;Then silence. Set a visible timer for sixty minutes; if the wall isn’t full at the hour mark, run it to ninety.&lt;/p&gt;

&lt;p&gt;By the end you should have somewhere between 150 and 400 notes, depending on the domain. If you have fewer than 100, either the scope was wrong, the guest list was wrong, or the room hasn’t yet believed you that writing is the job.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Talking instead of writing. &lt;em&gt;“Get it on a note. We’ll talk during the timeline.”&lt;/em&gt; Repeat as needed.&lt;/li&gt;
  &lt;li&gt;Whole departments not writing. If everyone from support has three notes between them and sales has forty, something is off. Move the facilitator over. Make eye contact. Invite specific events: &lt;em&gt;“What’s the first thing you see when a customer calls in?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Executive-only events. &lt;em&gt;“Strategy Agreed.”&lt;/em&gt; &lt;em&gt;“Board Met.”&lt;/em&gt; If the wall is all leadership verbs, the frontline isn’t contributing yet. Something is blocking them, usually whoever is standing at the other end of the room.&lt;/li&gt;
  &lt;li&gt;People writing wishes, not events. &lt;em&gt;“That sounds like what we’d like to happen. What actually happens?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-2-enforce-the-timeline-4560-min&quot;&gt;Phase 2: Enforce the timeline (45–60 min)&lt;/h4&gt;

&lt;p&gt;Everyone talks. The job is to arrange the notes left-to-right in rough chronological order, spreading vertically into parallel tracks wherever the flow genuinely forks. It will be messy. That’s the point.&lt;/p&gt;

&lt;p&gt;Open it:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Put these in order. Don’t aim for perfection; rough chronology is enough. Parallel things go in parallel. If you disagree about where something goes, put a pink note on it and move on.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Walk the room. Prompt clusters to form around parts of the domain: &lt;em&gt;“discovery over here, money in the middle, fulfilment to the right.”&lt;/em&gt; Accept that the timeline will have several overlapping tracks.&lt;/p&gt;

&lt;p&gt;About thirty minutes in, pause and find the pivotal events. This is the single most productive move in timeline construction, and first-time facilitators almost always skip it.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What are the 4–8 most important events on this wall? The moments where the state of the customer, the product, or the business fundamentally changes? Call them out.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mark each with a tall red dashed line or a big dashed box around it. &lt;em&gt;Once the pivotals are visible, the rest of the timeline organises itself into the phases between them.&lt;/em&gt; Teams that skip this step spend another twenty minutes arguing about whether &lt;em&gt;Card Expired&lt;/em&gt; goes before or after &lt;em&gt;Renewal Notice Sent&lt;/em&gt;; teams that do it first stop caring, because both events belong to the same phase.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One department dominating the timeline. Pair people from different departments and give them sections.&lt;/li&gt;
  &lt;li&gt;No pink notes appearing at all. Disagreements are hidden, not absent. Prompt: &lt;em&gt;“Is anything on this wall surprising you?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Rabbit holes into policy debates. &lt;em&gt;“Great policy conversation; park it. We’re looking for rough chronology.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;People trying to make it tidy too early. &lt;em&gt;“It’s supposed to be messy. Tidy comes at Process Level, not here.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Duplicates proliferating. Leave ambiguous ones; if two notes &lt;em&gt;might&lt;/em&gt; be the same event, that’s a pink note, not a merge.&lt;/li&gt;
  &lt;li&gt;No pivotal events getting called out. The team may be too deep in the weeds. Name two you think are obvious and ask which other ones they’d add. Then let them disagree.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;phase-3-reverse-narrative-2030-min&quot;&gt;Phase 3: Reverse narrative (20–30 min)&lt;/h4&gt;

&lt;p&gt;Walk the wall backwards once. This is a Brandolini move that sounds strange and is the single most effective way to find missing events.&lt;/p&gt;

&lt;p&gt;Start at the rightmost event and ask: &lt;em&gt;“What had to be true for this to happen? What had to happen just before it?”&lt;/em&gt; Work right to left.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Going forwards, we tell a story we already believe. Going backwards, we discover the bits we’ve been handwaving. Every ‘we don’t know’ is a pink note. Every ‘oh wait, it must be…’ is a new orange sticky.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Expect 20–40 new events on the reverse pass, most of them on the left-hand side of the wall where the early steps got skipped because nobody in the room owns them. The reverse narrative is where the cross-team gaps become undeniable: three teams each discover they don’t know how something actually starts, and the answer almost always involves a team that isn’t in the room.&lt;/p&gt;

&lt;h4 id=&quot;phase-4-explicit-walkthrough-60120-min&quot;&gt;Phase 4: Explicit walkthrough (60–120 min)&lt;/h4&gt;

&lt;p&gt;This is what Big Picture is &lt;em&gt;for&lt;/em&gt;. Everything before was preparation.&lt;/p&gt;

&lt;p&gt;One person, ideally someone who thinks they know the whole flow, walks the wall end to end, out loud, narrating each event in order as if explaining it to a newcomer. Everyone else’s job is to listen and interrupt when something doesn’t match what they know.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“One of us is about to walk this wall start to finish, out loud. Their job is to narrate what happens at each event. Your job is to interrupt when it doesn’t match what you know. Interruptions are the point of this phase; hold nothing back.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pick the walker carefully. Not the most senior person. Not someone who’ll perform. Someone who knows a lot but not everything, who’ll narrate what they think is happening and be genuinely surprised when corrected.&lt;/p&gt;

&lt;p&gt;The walker moves slowly: ten seconds per event, minimum. For a 300-event wall that’s 50 minutes without interruptions, and with real interruptions it’ll run 90–180. Budget double the no-interruption time.&lt;/p&gt;

&lt;p&gt;Every interruption is precious. Pink note the disagreement, stick it on the event, and move on; don’t try to resolve during the walkthrough.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The walker turning it into a lecture. &lt;em&gt;“Keep moving; the interruptions are the output.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Nobody interrupting. Either the walker is genuinely correct (rare) or the room has stopped listening. Pause; ask a specific person by name: &lt;em&gt;“From where you sit in support, does this match what you hear on the phones?”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Interruptions becoming arguments. &lt;em&gt;“Pink note it. Keep walking.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;The walker skipping sections. &lt;em&gt;“Good, stop there. Who knows what happens next?”&lt;/em&gt; Let someone else take over for that stretch.&lt;/li&gt;
  &lt;li&gt;The room running out of energy. Break into 45-minute segments with stretches between.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase is what people remember for years. Protect it.&lt;/p&gt;

&lt;h4 id=&quot;phase-5-pain-and-systems-60-min&quot;&gt;Phase 5: Pain and systems (60 min)&lt;/h4&gt;

&lt;p&gt;Now add the pink notes deliberately. You already have some from the timeline and the walkthrough; add more. Also add yellow notes for the systems and people that keep reappearing across the wall.&lt;/p&gt;

&lt;p&gt;Prompt:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Where does this hurt? Where do people work around the system? Where is information lost? Where does a decision get made with the wrong context? Every pain point is a pink note.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don’t try to solve anything; just surface it. The wall should look dense with pink by the end.&lt;/p&gt;

&lt;h4 id=&quot;phase-6-dot-voting-2030-min&quot;&gt;Phase 6: Dot-voting (20–30 min)&lt;/h4&gt;

&lt;p&gt;There will be too many pinks. That’s normal. Dot-voting turns the wall into a prioritised shortlist.&lt;/p&gt;

&lt;p&gt;Give everyone five or six coloured dots and let them place them on the pinks that matter most to them. Frame it:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“We’re not fixing anything in this room. We’re picking the top 3 to 5 places worth digging into next, the places where a Process Level session will be most valuable. Put your dots where you’d most want to zoom in.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Count. The clusters with the most dots become the candidates for follow-up Process Level work.&lt;/p&gt;

&lt;p&gt;What to watch for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Leadership dots dominating. If leadership votes first, the result is their priorities with a veneer. Ask the frontline to vote first, or do it anonymously.&lt;/li&gt;
  &lt;li&gt;Dots concentrating on one department. That department’s pinks may genuinely be the worst, or the voting has been political. Worth a one-minute conversation about the distribution.&lt;/li&gt;
  &lt;li&gt;Pinks with zero dots. Don’t throw them away; photograph them. They survive in the record.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;worked-example-pagebound-online-indie-bookshop&quot;&gt;Worked example: Pagebound, online indie bookshop&lt;/h3&gt;

&lt;p&gt;Pagebound is a mid-sized online independent bookshop: about 200,000 customers, six warehouses, a handful of physical partner shops, an engineering team of thirty split across product, commerce, fulfilment, and data, plus a customer support operation that fields returns and refunds.&lt;/p&gt;

&lt;p&gt;The sponsor is the CTO. The reason for the session: &lt;em&gt;“We keep hearing that things go wrong in order-to-delivery but no two teams describe the problem the same way. Before we commit to a big migration we want everyone in one room looking at the same wall.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The scope, in one phrase: the whole Pagebound customer experience, from the moment someone first hears about a book through to the day they either recommend it to a friend or decline a repeat purchase. That’s wider than order-to-delivery on purpose; the CTO believes the real problems sit at the edges (discovery, returns, loyalty), not the middle.&lt;/p&gt;

&lt;p&gt;Fourteen people in the room: a product lead, two engineers, a data analyst, the warehouse manager, a fulfilment team lead, a customer-success lead, a support agent who volunteered, a finance analyst, a marketing lead, a buyer (the person who decides which books Pagebound stocks), and the SRE on call that week. The CTO opens, then leaves.&lt;/p&gt;

&lt;p&gt;By the end of the day the wall looks something like this, simplified from several hundred events to around thirty key ones grouped into six phases, with four pink hotspots and two pivotal events marked:&lt;/p&gt;

&lt;link href=&quot;https://fonts.googleapis.com/css2?family=Kalam:wght@400;700&amp;amp;display=swap&quot; rel=&quot;stylesheet&quot; /&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1800 640&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Kalam&apos;, &apos;Segoe Print&apos;, &apos;Comic Sans MS&apos;, cursive;&quot; role=&quot;img&quot; aria-label=&quot;Big Picture wall for Pagebound, online bookshop: six phases across a long timeline — Discovery, Cart &amp;amp; Checkout, Payment &amp;amp; Fulfilment, Delivery, Post-delivery, and Loyalty &amp;amp; Winback — each with four or five events, team labels, pink hotspots at cross-team boundaries, and two pivotal event markers.&quot;&gt;
  &lt;defs&gt;
    &lt;filter id=&quot;wobble-bp&quot; x=&quot;-5%&quot; y=&quot;-5%&quot; width=&quot;110%&quot; height=&quot;110%&quot;&gt;
      &lt;feTurbulence type=&quot;fractalNoise&quot; baseFrequency=&quot;0.02&quot; numOctaves=&quot;2&quot; seed=&quot;11&quot; result=&quot;n&quot; /&gt;
      &lt;feDisplacementMap in=&quot;SourceGraphic&quot; in2=&quot;n&quot; scale=&quot;2&quot; /&gt;
    &lt;/filter&gt;
    &lt;style&gt;
      .bp-sticky { stroke: #1a1a1a; stroke-width: 1.8; filter: url(#wobble-bp); }
      .bp-event { fill: #ffb84d; }
      .bp-hotspot { fill: #f4a6c0; }
      .bp-team { fill: #fff1a1; }
      .bp-phase-label { font-size: 16px; font-weight: 700; fill: #4a4540; letter-spacing: 0.03em; text-transform: uppercase; }
      .bp-event-text { font-size: 12px; fill: #1a1a1a; }
      .bp-hotspot-text { font-size: 11px; fill: #1a1a1a; font-style: italic; }
      .bp-team-text { font-size: 12px; font-weight: 700; fill: #1a1a1a; text-transform: uppercase; letter-spacing: 0.04em; }
      .bp-pivotal { stroke: #b84040; stroke-width: 2.5; stroke-dasharray: 6 4; fill: none; }
      .bp-pivotal-label { font-size: 12px; fill: #b84040; font-weight: 700; font-style: italic; }
      .bp-caption { font-size: 13px; fill: #4a4540; font-style: italic; }
    &lt;/style&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;155&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;bp-phase-label&quot;&gt;Discovery&lt;/text&gt;
  &lt;text x=&quot;455&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;bp-phase-label&quot;&gt;Cart &amp;amp; Checkout&lt;/text&gt;
  &lt;text x=&quot;755&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;bp-phase-label&quot;&gt;Payment &amp;amp; Fulfilment&lt;/text&gt;
  &lt;text x=&quot;1055&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;bp-phase-label&quot;&gt;Delivery&lt;/text&gt;
  &lt;text x=&quot;1345&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;bp-phase-label&quot;&gt;Post-delivery&lt;/text&gt;
  &lt;text x=&quot;1635&quot; y=&quot;35&quot; text-anchor=&quot;middle&quot; class=&quot;bp-phase-label&quot;&gt;Loyalty &amp;amp; Winback&lt;/text&gt;

  &lt;g transform=&quot;translate(40, 60)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Ad Seen&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(40, 106)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Search Performed&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(40, 152)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Book Viewed&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(40, 198)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Review Read&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(40, 244)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Wishlist Item Added&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(340, 60)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Cart Started&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(340, 106)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Item Added&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(340, 152)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Discount Applied&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(340, 198)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Address Entered&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(340, 244)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Checkout Started&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(640, 60)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Payment Captured&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(640, 106)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Order Confirmed&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(640, 152)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Stock Reserved&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(640, 198)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Items Picked&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(640, 244)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Order Packed&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(940, 60)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Label Printed&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(940, 106)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Handed To Carrier&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(940, 152)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Scanned At Hub&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(940, 198)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Out For Delivery&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(940, 244)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Parcel Delivered&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(1230, 60)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Book Received&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1230, 106)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Review Submitted&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1230, 152)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Return Requested&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1230, 198)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Refund Issued&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1230, 244)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Support Ticket Raised&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(1520, 60)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Loyalty Points Awarded&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1520, 106)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Recommendation Sent&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1520, 152)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Dormant Flagged&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1520, 198)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Winback Offer Sent&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1520, 244)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-event&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-event-text&quot;&gt;Customer Returned&lt;/text&gt;&lt;/g&gt;

  &lt;g transform=&quot;translate(40, 320)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-team&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;23&quot; text-anchor=&quot;middle&quot; class=&quot;bp-team-text&quot;&gt;Marketing + Data&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(340, 320)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-team&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;23&quot; text-anchor=&quot;middle&quot; class=&quot;bp-team-text&quot;&gt;Commerce&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(640, 320)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-team&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;23&quot; text-anchor=&quot;middle&quot; class=&quot;bp-team-text&quot;&gt;Commerce + Warehouse&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(940, 320)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-team&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;23&quot; text-anchor=&quot;middle&quot; class=&quot;bp-team-text&quot;&gt;Warehouse + Carrier&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1230, 320)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-team&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;23&quot; text-anchor=&quot;middle&quot; class=&quot;bp-team-text&quot;&gt;Customer Success&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1520, 320)&quot;&gt;&lt;rect width=&quot;230&quot; height=&quot;36&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-team&quot; /&gt;&lt;text x=&quot;115&quot; y=&quot;23&quot; text-anchor=&quot;middle&quot; class=&quot;bp-team-text&quot;&gt;CRM + Marketing&lt;/text&gt;&lt;/g&gt;

  &lt;path d=&quot;M 620 50 L 620 360&quot; class=&quot;bp-pivotal&quot; /&gt;
  &lt;text x=&quot;620&quot; y=&quot;400&quot; text-anchor=&quot;middle&quot; class=&quot;bp-pivotal-label&quot;&gt;Pivotal&lt;/text&gt;
  &lt;text x=&quot;620&quot; y=&quot;416&quot; text-anchor=&quot;middle&quot; class=&quot;bp-pivotal-label&quot;&gt;becomes a paying customer&lt;/text&gt;

  &lt;path d=&quot;M 1205 50 L 1205 360&quot; class=&quot;bp-pivotal&quot; /&gt;
  &lt;text x=&quot;1205&quot; y=&quot;400&quot; text-anchor=&quot;middle&quot; class=&quot;bp-pivotal-label&quot;&gt;Pivotal&lt;/text&gt;
  &lt;text x=&quot;1205&quot; y=&quot;416&quot; text-anchor=&quot;middle&quot; class=&quot;bp-pivotal-label&quot;&gt;parcel in their hands&lt;/text&gt;

  &lt;g transform=&quot;translate(290, 450)&quot;&gt;
    &lt;rect width=&quot;260&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-hotspot&quot; /&gt;
    &lt;text x=&quot;130&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;HOTSPOT — when do we reserve stock?&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;Commerce says at checkout;&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;Warehouse says at Payment Captured.&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(860, 450)&quot;&gt;
    &lt;rect width=&quot;260&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-hotspot&quot; /&gt;
    &lt;text x=&quot;130&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;HOTSPOT — carrier handoff is opaque.&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;4% of parcels vanish between&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;&quot;Handed To Carrier&quot; and &quot;Scanned At Hub&quot;.&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(1150, 450)&quot;&gt;
    &lt;rect width=&quot;260&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-hotspot&quot; /&gt;
    &lt;text x=&quot;130&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;HOTSPOT — returns window vs review.&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;Can a customer return a book&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;they&apos;ve already reviewed?&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(1490, 450)&quot;&gt;
    &lt;rect width=&quot;260&quot; height=&quot;70&quot; rx=&quot;3&quot; class=&quot;bp-sticky bp-hotspot&quot; /&gt;
    &lt;text x=&quot;130&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;HOTSPOT — winback driven by?&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;44&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;CRM watches purchase gap;&lt;/text&gt;
    &lt;text x=&quot;130&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; class=&quot;bp-hotspot-text&quot;&gt;support watches ticket recency. Divergent.&lt;/text&gt;
  &lt;/g&gt;

  &lt;text x=&quot;900&quot; y=&quot;570&quot; text-anchor=&quot;middle&quot; class=&quot;bp-caption&quot;&gt;Six phases, 30 events shown (of several hundred on the real wall), four pink hotspots, two pivotal markers.&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;590&quot; text-anchor=&quot;middle&quot; class=&quot;bp-caption&quot;&gt;The team lane says which function does most of the work in that phase — the whole point of Big Picture&lt;/text&gt;
  &lt;text x=&quot;900&quot; y=&quot;610&quot; text-anchor=&quot;middle&quot; class=&quot;bp-caption&quot;&gt;is to find the moments where work crosses those lanes and nobody notices.&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;The moment this wall earns its cost is during the explicit walkthrough, when the customer-success lead stops the walker at &lt;em&gt;Return Requested&lt;/em&gt; and says: &lt;em&gt;“Wait, our returns logic treats a reviewed book as non-returnable because we assume the customer has opened it. Is that actually in the terms?”&lt;/em&gt; The finance analyst checks; it isn’t in the terms. The warehouse manager says his team has been refusing those returns for eighteen months. The support lead says she’s been authorising them case-by-case because customers complain.&lt;/p&gt;

&lt;p&gt;Three people in a corridor would have argued about that for a month. On the wall, with marketing and commerce watching, it takes ninety seconds to surface and a pink note to capture.&lt;/p&gt;

&lt;p&gt;That’s the thing Big Picture is for: the mismatches that only become visible when the whole wall is on view.&lt;/p&gt;

&lt;p&gt;The four dot-voted hotspots become the candidates for follow-up work. The one with the most dots, the stock-reservation timing question, is what the &lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Process Level post&lt;/a&gt; uses as its own running example.&lt;/p&gt;

&lt;h3 id=&quot;what-can-go-wrong&quot;&gt;What can go wrong&lt;/h3&gt;

&lt;p&gt;Named failure modes. Each has a symptom, a recovery move, and a threshold where you stop rather than limp through.&lt;/p&gt;

&lt;p&gt;Nobody will commit to the whole day. Half the room drifts in and out.
  &lt;em&gt;Recovery:&lt;/em&gt; Stop and reset. Rebook with people who’ll commit.
  &lt;em&gt;Stop if:&lt;/em&gt; Two hours in and half the room is still on their laptops. Apologise, reschedule.&lt;/p&gt;

&lt;p&gt;Political theatre. A senior is in the room, corrects every event they disagree with, and the frontline has stopped writing.
  &lt;em&gt;Recovery:&lt;/em&gt; Name it carefully. &lt;em&gt;“We need the frontline view right now. Let’s hear from support and operations first.”&lt;/em&gt;
  &lt;em&gt;Stop if:&lt;/em&gt; The dynamic doesn’t shift. Photograph the wall, thank everyone, reschedule without the leader.&lt;/p&gt;

&lt;p&gt;The wall of one department. 90% of notes come from engineering, or 90% from sales.
  &lt;em&gt;Recovery:&lt;/em&gt; Pause writing. Give each under-represented department 20 minutes with a facilitator at their shoulder, adding events from their slice.
  &lt;em&gt;Stop if:&lt;/em&gt; A department genuinely has nothing to add. Either they shouldn’t be here, or the scope is wrong.&lt;/p&gt;

&lt;p&gt;Hotspot overwhelm. Eighty pinks and nobody knows what to do with them.
  &lt;em&gt;Recovery:&lt;/em&gt; Cluster into themes before voting. Vote on themes, not individual notes.
  &lt;em&gt;Stop if:&lt;/em&gt; The themes don’t cohere. Photograph everything; make the follow-up &lt;em&gt;“sort offline”&lt;/em&gt; rather than &lt;em&gt;“decide now”&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Leadership sidebar. Two or three senior people cluster together and start having their own meeting.
  &lt;em&gt;Recovery:&lt;/em&gt; Interrupt it, politely, out loud. &lt;em&gt;“Sidebar forming — can we bring that into the room?”&lt;/em&gt; Most sidebars collapse when named.
  &lt;em&gt;Stop if:&lt;/em&gt; The sidebar absorbs the session. Two rooms isn’t a workshop.&lt;/p&gt;

&lt;h3 id=&quot;outputs&quot;&gt;Outputs&lt;/h3&gt;

&lt;p&gt;Within 24 hours of the session ending:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Panoramic high-resolution photographs of the wall, overlapping so it can be reassembled digitally. One per metre for a very long wall.&lt;/li&gt;
  &lt;li&gt;A transcribed event list, system list, and hotspot list, organised by rough zone.&lt;/li&gt;
  &lt;li&gt;A short summary message to participants: &lt;em&gt;“Here’s what we found, here are the dot-voted hotspots, here’s what happens next.”&lt;/em&gt; Send within 24 hours, while the energy is fresh.&lt;/li&gt;
  &lt;li&gt;A schedule of 3–5 follow-up Process Level sessions, one per top hotspot. Book them within two weeks; momentum dies fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the weeks after:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Pin the vocabulary that emerged. The words that kept reappearing on the wall are the start of the organisation’s shared language. Circulate a glossary.&lt;/li&gt;
  &lt;li&gt;Walk the wall with anyone who couldn’t attend. Especially peers of the attendees; their reactions tell you whether the picture lands outside the room.&lt;/li&gt;
  &lt;li&gt;Don’t try to keep the wall “current”. It’s a snapshot of a moment, not a live document. Run another Big Picture when the snapshot is stale enough to mislead, usually six to twelve months later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/the-workshop-event-storming-a-process/&quot;&gt;Event Storming a Process&lt;/a&gt;: the natural follow-up. Big Picture finds the hotspots; Process Level zooms into one and maps it precisely. In the Pagebound example, the stock-reservation hotspot is the candidate.&lt;/li&gt;
  &lt;li&gt;Event Storming an Architecture: two zooms further in, turning a Process Level map into a software design.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Storming: Building Shared Understanding&lt;/a&gt;: the narrative post showing a smaller team running their first session.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Teaching Your LLM the Codebase: CLAUDE.md and AGENTS.md</title>
    <link href="/writing/teaching-your-llm-the-codebase-claude-md-and-agents-md/"/>
    <updated>2026-04-09T06:00:00+08:00</updated>
    <id>/writing/teaching-your-llm-the-codebase-claude-md-and-agents-md/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;The &lt;a href=&quot;/writing/teaching-your-llm-the-codebase/&quot;&gt;previous post&lt;/a&gt; introduced the idea: teach the &lt;label for=&quot;sn-writing-teaching-your-llm-the-codebase-claude-md-and-agents-md-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-teaching-your-llm-the-codebase-claude-md-and-agents-md-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-teaching-your-llm-the-codebase-claude-md-and-agents-md-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-teaching-your-llm-the-codebase-claude-md-and-agents-md-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; your conventions through a file it reads on every task. This post shows the files themselves.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-root-claudemd&quot;&gt;The root CLAUDE.md&lt;/h3&gt;

&lt;p&gt;This is the file at the root of the Greenbox repository. It’s the first thing the LLM reads when it starts working:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- file: CLAUDE.md --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;gh&quot;&gt;# Greenbox&lt;/span&gt;

Produce-box subscription service. Go monorepo.

&lt;span class=&quot;gu&quot;&gt;## Build &amp;amp; Test&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`go test ./...`&lt;/span&gt; to run all tests
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`go vet ./...`&lt;/span&gt; before committing
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`golangci-lint run`&lt;/span&gt; for full lint check

&lt;span class=&quot;gu&quot;&gt;## Project Structure&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`cmd/greenbox/`&lt;/span&gt; — Main application entry point
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`subscription/`&lt;/span&gt; — Subscription lifecycle (create, pause, resume, cancel, box size)
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`billing/`&lt;/span&gt; — Invoices, payment confirmation, pricing
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`delivery/`&lt;/span&gt; — Delivery scheduling, packing, dispatch
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`db/`&lt;/span&gt; — Database access and migrations

&lt;span class=&quot;gu&quot;&gt;## Conventions&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; Guard clauses for early returns. No deep nesting.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Custom types for IDs and dates: &lt;span class=&quot;sb&quot;&gt;`SubscriptionID`&lt;/span&gt;, &lt;span class=&quot;sb&quot;&gt;`CustomerID`&lt;/span&gt;, &lt;span class=&quot;sb&quot;&gt;`DeliveryDate`&lt;/span&gt;, not raw strings.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Unexported struct fields. Constructor functions enforce invariants.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Error wrapping: &lt;span class=&quot;sb&quot;&gt;`fmt.Errorf(&quot;doing thing: %w&quot;, err)`&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Table-driven tests with &lt;span class=&quot;sb&quot;&gt;`t.Run`&lt;/span&gt; subtests.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Test names describe behaviour: &lt;span class=&quot;sb&quot;&gt;`TestPausedSubscription_CannotChangeBoxSize`&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;## Domain Language&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; &quot;subscription&quot; not &quot;order&quot;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &quot;box&quot; not &quot;product&quot; or &quot;package&quot;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &quot;delivery day&quot; not &quot;shipping date&quot;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &quot;subscriber&quot; not &quot;user&quot; or &quot;customer&quot; (except in CustomerID, which is the billing reference)
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &quot;pause&quot; not &quot;suspend&quot; or &quot;hold&quot;

&lt;span class=&quot;gu&quot;&gt;## Do Not&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; No &lt;span class=&quot;sb&quot;&gt;`interface{}`&lt;/span&gt; or &lt;span class=&quot;sb&quot;&gt;`any`&lt;/span&gt;. Use concrete types or narrow interfaces.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; No &lt;span class=&quot;sb&quot;&gt;`utils`&lt;/span&gt;, &lt;span class=&quot;sb&quot;&gt;`helpers`&lt;/span&gt;, or &lt;span class=&quot;sb&quot;&gt;`common`&lt;/span&gt; packages.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; No global state or package-level variables (except constants).
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Thirty lines. Everything a developer, or an LLM, needs to write code that fits the project. The conventions section is the most valuable: it prevents the style drift that Tom and Priya discovered when their LLM-generated code looked like it came from different teams.&lt;/p&gt;

&lt;h3 id=&quot;why-each-section-matters&quot;&gt;Why each section matters&lt;/h3&gt;

&lt;p&gt;Build &amp;amp; Test seems obvious, but LLMs use it. When asked to verify a change, the LLM runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go test ./...&lt;/code&gt; because the file told it to. Without this section, it might run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go build&lt;/code&gt; and call it done, or guess at a test command that doesn’t exist.&lt;/p&gt;

&lt;p&gt;Project Structure tells the LLM where to put new code. When asked to add a delivery feature, it goes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delivery/&lt;/code&gt;, not a new top-level package. The structure section is a map.&lt;/p&gt;

&lt;p&gt;Conventions is the style guide. Guard clauses, typed IDs, table-driven tests, these are the patterns the team agreed on. Without this section, the LLM generates valid Go that doesn’t match the team’s Go. With it, generated code passes review faster because it already looks like the codebase.&lt;/p&gt;

&lt;p&gt;Domain Language is subtle but powerful. Before this section existed, the LLM would generate variable names like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orderID&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;productName&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shippingDate&lt;/code&gt;. Each one required a review comment: “We call this a subscription, not an order.” Now the LLM uses the right words the first time. This also helps new developers absorb the team’s vocabulary, they see it in the generated code before they’ve read every file.&lt;/p&gt;

&lt;p&gt;Do Not is the anti-pattern list. This prevents the LLM’s most common bad habits. Without it, Go LLMs love to create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;utils&lt;/code&gt; packages, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface{}&lt;/code&gt; for flexibility, and introduce package-level variables. The explicit prohibition stops these before they start.&lt;/p&gt;

&lt;h3 id=&quot;package-level-claudemd&quot;&gt;Package-level CLAUDE.md&lt;/h3&gt;

&lt;p&gt;The root file covers the whole project. Package-level files add context for specific packages:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- file: subscription/CLAUDE.md --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;gh&quot;&gt;# Subscription&lt;/span&gt;

Manages subscription lifecycle.

&lt;span class=&quot;gu&quot;&gt;## Status Transitions&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; Pending → Active → Paused → Active (resume) or Cancelled
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Paused subscriptions cannot change box size.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Cancelled subscriptions cannot be modified at all.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`NewSubscription`&lt;/span&gt; starts in &lt;span class=&quot;sb&quot;&gt;`StatusPending`&lt;/span&gt;.

&lt;span class=&quot;gu&quot;&gt;## Conventions&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; All mutations go through methods on &lt;span class=&quot;sb&quot;&gt;`Subscription`&lt;/span&gt;. No direct field access from outside.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Status is a typed constant (&lt;span class=&quot;sb&quot;&gt;`StatusPending`&lt;/span&gt;, &lt;span class=&quot;sb&quot;&gt;`StatusActive`&lt;/span&gt;, etc.), not a raw string.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And for the billing package:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- file: billing/CLAUDE.md --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;gh&quot;&gt;# Billing&lt;/span&gt;

Invoices, payment confirmation, pricing.

&lt;span class=&quot;gu&quot;&gt;## Money&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; All amounts stored in cents (int64), not dollars (float64).
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Display formatting happens at the HTTP layer, not in billing logic.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Currency is always AUD. No multi-currency support yet.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When the LLM works in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;billing&lt;/code&gt; package, it reads both the root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; and the package-level one. The root provides general conventions. The package file provides package-specific rules. The LLM stores amounts in cents because the file says so, no more pull request comments asking “should this be cents or dollars?”&lt;/p&gt;

&lt;h3 id=&quot;agentsmd-specialised-roles&quot;&gt;AGENTS.md: specialised roles&lt;/h3&gt;

&lt;p&gt;Where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; is the general brief, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; defines specialised roles, agents the LLM can adopt for specific tasks. The Greenbox team defines two:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# file: AGENTS.md&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[[agents]]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;test-writer&quot;&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Writes tests for Greenbox code following team conventions&quot;&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[agents.instructions]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
You write tests for the Greenbox codebase.

Conventions:
- Use table-driven tests with t.Run subtests for any function with more than two cases.
- Test names describe behaviour, not implementation: TestPausedSubscription_CannotChangeBoxSize
- Use precise language in test names:
  - &quot;Cannot&quot; = hard constraint, test failure means a bug
  - &quot;Returns&quot; = pure output check
- Create test fixtures using constructor functions, not struct literals with exported fields.
- Prefer assertion messages that explain the business rule: &quot;paused subscriptions cannot change box size&quot;
- Do not use testify or other assertion libraries. Use stdlib testing only.
- Test through public methods. Never access unexported fields.
&quot;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[[agents]]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;reviewer&quot;&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Reviews code for convention drift&quot;&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[agents.instructions]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
You review pull requests for the Greenbox codebase.

Check for:
1. Exported fields that should be unexported. Structs should have unexported fields with constructors.
2. Raw strings where typed IDs should be used: SubscriptionID, CustomerID, BoxSize.
3. Deep nesting: more than two levels of if/else suggests missing guard clauses.
4. Missing error handling or unwrapped errors.
5. Tests that test implementation instead of behaviour.

Do not nitpick formatting or style — the linter handles that.
&quot;&quot;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each &lt;label for=&quot;sn-writing-teaching-your-llm-the-codebase-claude-md-and-agents-md-ai-agent&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-teaching-your-llm-the-codebase-claude-md-and-agents-md-ai-agent-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;agent&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-teaching-your-llm-the-codebase-claude-md-and-agents-md-ai-agent&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-teaching-your-llm-the-codebase-claude-md-and-agents-md-ai-agent-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Agent&lt;/span&gt;A system that wraps an LLM with tools, memory, and a loop, so it can take multi-step actions toward a goal rather than just answering one prompt.
&lt;/span&gt; encodes expertise the team has built up. The test writer knows about precise naming because the team keeps finding that vague test names make failures harder to diagnose. The reviewer catches the convention drift that slips through when everyone’s moving fast, exported fields, raw strings where typed IDs belong, nested conditionals that should be guard clauses.&lt;/p&gt;

&lt;h3 id=&quot;how-agents-are-invoked&quot;&gt;How agents are invoked&lt;/h3&gt;

&lt;p&gt;When Priya asks the LLM to write tests, she invokes the test-writer agent:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; /test-writer Write tests for the new pause subscription handler

# The agent reads:
# 1. Root CLAUDE.md (general conventions)
# 2. subscription/CLAUDE.md (package-specific rules)
# 3. The test-writer agent instructions from AGENTS.md
# 4. The relevant source files
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The generated tests use table-driven structure, descriptive names, and stdlib assertions, because the agent’s instructions specify all of that. Without the agent, the LLM would still generate tests (it read the root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;), but the agent adds the thoroughness.&lt;/p&gt;

&lt;p&gt;Tom uses the reviewer agent during code review:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; /reviewer Review this PR for billing/invoices.go

# The agent checks:
# - Invoice struct has unexported fields
# - Amounts stored in cents, not dollars
# - Typed IDs used instead of raw strings
# - No deep nesting
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The reviewer catches an exported &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Amount&lt;/code&gt; field that should be unexported with a constructor, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;string&lt;/code&gt; parameter where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SubscriptionID&lt;/code&gt; should be used. Tom would have caught these too, eventually. The agent catches them in seconds, every time, without fatigue.&lt;/p&gt;

&lt;h3 id=&quot;the-maintenance-cycle&quot;&gt;The maintenance cycle&lt;/h3&gt;

&lt;p&gt;Priya warns the team early: “A stale &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; is worse than no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;. If the file says ‘use guard clauses’ but the codebase has moved to a different pattern, the LLM generates code that doesn’t match anything.”&lt;/p&gt;

&lt;p&gt;The team adopts a rule: when you change a convention, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; in the same commit. It’s like updating tests when you change behaviour, the documentation and the code move together.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Tom&apos;s commit message when they adopt a new error type&lt;/span&gt;
git log &lt;span class=&quot;nt&quot;&gt;--oneline&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# a1b2c3d Add DomainError type, update CLAUDE.md conventions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; diff in that commit:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; ## Conventions

 - Error wrapping: `fmt.Errorf(&quot;doing thing: %w&quot;, err)`
&lt;span class=&quot;gi&quot;&gt;+- Domain errors: use `DomainError{Code, Message}` for business rule violations.
+  Reserve `fmt.Errorf` for infrastructure errors (database, network).
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Two lines. The LLM now generates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DomainError&lt;/code&gt; for business rule violations and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fmt.Errorf&lt;/code&gt; for infrastructure errors. The convention is encoded the moment it’s decided.&lt;/p&gt;

&lt;h3 id=&quot;before-and-after&quot;&gt;Before and after&lt;/h3&gt;

&lt;p&gt;The clearest proof is in the generated code. Here’s what the LLM generates for “add a Resume method to Subscription”, first without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;, then with it.&lt;/p&gt;

&lt;p&gt;Without CLAUDE.md:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// file: subscription/subscription.go&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Subscription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Resume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;active&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PauseReason&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Exported fields. String status. No error handling. No guard clause.&lt;/p&gt;

&lt;p&gt;With CLAUDE.md:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// file: subscription/subscription.go&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Subscription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Resume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;StatusPaused&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;cannot resume subscription in status %v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;StatusActive&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pauseReason&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updatedAt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Guard clause. Unexported fields. Status constant. Error returned. The code matches the codebase because the LLM read the brief.&lt;/p&gt;

&lt;p&gt;The difference isn’t intelligence, it’s context. The LLM is equally capable in both cases. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; gives it the context to be capable in the right direction.&lt;/p&gt;

&lt;h3 id=&quot;the-compound-effect&quot;&gt;The compound effect&lt;/h3&gt;

&lt;p&gt;The team notices something over the following months. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; doesn’t just make LLM-generated code better. It makes the whole codebase more consistent, because:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;New developers read it as an onboarding doc.&lt;/li&gt;
  &lt;li&gt;The LLM follows it, so generated code demonstrates the conventions.&lt;/li&gt;
  &lt;li&gt;Code reviewers reference it when explaining why a pattern should change.&lt;/li&gt;
  &lt;li&gt;The conventions themselves get sharper, because writing them down forces the team to resolve ambiguity. “Use typed IDs” is vague. “Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SubscriptionID&lt;/code&gt; not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;string&lt;/code&gt; for subscription identifiers” is precise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tom puts it simply: “We wrote a page of conventions for the LLM and accidentally standardised the whole team.”&lt;/p&gt;

&lt;p&gt;Lee’s version: “The best documentation is documentation that has a reader. The LLM reads the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; on every task. That makes it the most-read document in the repository.”&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Teaching Your LLM the Codebase</title>
    <link href="/writing/teaching-your-llm-the-codebase/"/>
    <updated>2026-04-08T06:00:00+08:00</updated>
    <id>/writing/teaching-your-llm-the-codebase/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Two developers. Same codebase. Same &lt;label for=&quot;sn-writing-teaching-your-llm-the-codebase-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-teaching-your-llm-the-codebase-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-teaching-your-llm-the-codebase-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-teaching-your-llm-the-codebase-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt;. Different code. That’s not a bug in the LLM, it’s a missing brief.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After the &lt;a href=&quot;/writing/behaviour-driven-development-from-stories-to-working-software/&quot;&gt;BDD work&lt;/a&gt;, Tom and Priya are both leaning hard on LLMs. The Feature files make it easy: hand the LLM a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.feature&lt;/code&gt; file, ask for an implementation, get code back. Tom noticed the LLM generates code faster than he can review it. That’s true. But he’s about to notice something else.&lt;/p&gt;

&lt;h3 id=&quot;the-code-review-that-took-an-hour&quot;&gt;The code review that took an hour&lt;/h3&gt;

&lt;p&gt;Tom opens Priya’s pull request. The code is correct, tests pass, behaviour matches the feature file. But it looks nothing like his code. Her handler functions return early on errors. His use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if-else&lt;/code&gt; chains. Her test names read like sentences: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestPausedSubscription_CannotChangeBoxSize&lt;/code&gt;. His read like labels: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestChangeBoxSizePaused&lt;/code&gt;. Her structs have unexported fields with constructor functions. His have exported fields.&lt;/p&gt;

&lt;p&gt;None of this is wrong. It’s all defensible. But the review takes an hour because Tom keeps stopping to ask: “Is this a style choice or a behaviour choice?” Every difference is a potential bug he has to investigate.&lt;/p&gt;

&lt;p&gt;He brings it up at standup. “Priya’s code and my code look like they were written by different teams.”&lt;/p&gt;

&lt;p&gt;Priya frowns. “We’re using the same LLM. Same model, same tool.”&lt;/p&gt;

&lt;p&gt;“But not the same prompts,” Lee says. He’s been listening. “You’re each telling it something different about how you want the code to look. The LLM doesn’t have opinions, it reflects whatever you give it.”&lt;/p&gt;

&lt;h3 id=&quot;the-experiment&quot;&gt;The experiment&lt;/h3&gt;

&lt;p&gt;Lee suggests they test this. Same task, both developers, compare the results. The task: write a function that calculates the next delivery date, skipping public holidays. Same requirements. Same language. Same LLM.&lt;/p&gt;

&lt;p&gt;Tom prompts: “Write a Go function that calculates the next delivery date after a given date, skipping any dates in a public holidays list.”&lt;/p&gt;

&lt;p&gt;Priya prompts: “In our Greenbox codebase we use custom types for dates and guard clauses for validation. Write a Go function that calculates the next delivery date after a given date, skipping public holidays. Return an error if the input date is in the past.”&lt;/p&gt;

&lt;p&gt;Tom gets back a clean function. It takes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;time.Time&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[]time.Time&lt;/code&gt;, returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;time.Time&lt;/code&gt;. No error handling. No validation. Works fine.&lt;/p&gt;

&lt;p&gt;Priya gets back a function that takes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeliveryDate&lt;/code&gt; type and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HolidayCalendar&lt;/code&gt; interface. Guard clause at the top rejects past dates. Returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(DeliveryDate, error)&lt;/code&gt;. The generated code matches the patterns in the rest of the codebase because she described those patterns in the &lt;label for=&quot;sn-writing-teaching-your-llm-the-codebase-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-teaching-your-llm-the-codebase-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-teaching-your-llm-the-codebase-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-teaching-your-llm-the-codebase-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt;.&lt;/p&gt;

&lt;p&gt;“You gave it context,” Tom says.&lt;/p&gt;

&lt;p&gt;“I gave it the same context I’d give a new developer on their first day,” Priya says. “Here’s how we do things. Here’s what the conventions are. Here’s what the types look like.”&lt;/p&gt;

&lt;p&gt;“But you had to type all of that every time.”&lt;/p&gt;

&lt;p&gt;“Right. And that’s the problem.” Priya pulls up the Claude Code documentation on her screen. “There’s a way to make it permanent.”&lt;/p&gt;

&lt;h3 id=&quot;the-brief&quot;&gt;The brief&lt;/h3&gt;

&lt;p&gt;Lee draws a parallel to his consulting work. “When I join a new client, the first thing I look for is a brief: how the team works, what they’ve decided, what they’ve explicitly rejected. When the brief exists, I’m productive in days. When it doesn’t, I spend weeks asking ‘why did you do it this way?’”&lt;/p&gt;

&lt;p&gt;“The LLM needs the same thing,” Priya says. “And there’s a file for it.”&lt;/p&gt;

&lt;p&gt;In Claude Code, this brief is a file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;. It lives in the root of the repository. Every time the LLM starts a task, it reads this file first. The file becomes the persistent context that Tom was missing and Priya was typing out by hand.&lt;/p&gt;

&lt;p&gt;“Think of it as the onboarding document for your AI pair programmer,” Priya says. “Everything you’d tell a new hire in their first week goes in this file.”&lt;/p&gt;

&lt;h3 id=&quot;what-goes-in-the-brief&quot;&gt;What goes in the brief&lt;/h3&gt;

&lt;p&gt;The team sits down and writes their first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; together. Lee facilitates, he’s good at drawing out the things people know but haven’t said aloud. He asks three questions:&lt;/p&gt;

&lt;p&gt;“What patterns have you settled on?”&lt;/p&gt;

&lt;p&gt;Priya lists what she’s been pushing for over the past few months: guard clauses for early returns, table-driven tests, custom types for IDs and dates instead of raw strings, unexported struct fields with constructor functions, error wrapping with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fmt.Errorf(&quot;context: %w&quot;, err)&lt;/code&gt;. Tom nods along. He’s not sold on all of it, the typed IDs still feel like boilerplate to him, but he can’t argue with the consistency.&lt;/p&gt;

&lt;p&gt;“What patterns have you explicitly rejected?”&lt;/p&gt;

&lt;p&gt;This one surprises Tom. He hadn’t thought about anti-patterns as something to document. But Priya points out: “The LLM keeps generating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface{}&lt;/code&gt; parameters. We never use those. It keeps creating utility packages. We don’t have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;utils&lt;/code&gt; package and we don’t want one.”&lt;/p&gt;

&lt;p&gt;Lee nods. “Telling the LLM what &lt;em&gt;not&lt;/em&gt; to do is as important as telling it what to do. Same as onboarding. A new developer who’s told ‘we don’t use global state’ won’t introduce global state. An LLM that’s told the same thing won’t either.”&lt;/p&gt;

&lt;p&gt;“What does someone need to know about the domain?”&lt;/p&gt;

&lt;p&gt;This is where Maya’s language matters. The LLM shouldn’t call it an “order”, it’s a “subscription.” It shouldn’t call it a “product”, it’s a “box.” The delivery happens on a “delivery day,” not a “shipping date.” The team has been building a shared vocabulary, and the LLM needs to speak it too.&lt;/p&gt;

&lt;h3 id=&quot;the-first-version&quot;&gt;The first version&lt;/h3&gt;

&lt;p&gt;They write a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; that fits on one screen. Lee insists on this. “If it’s longer than a page, nobody will maintain it. Not the developers, and not the LLM, it’ll dilute the important stuff with noise.”&lt;/p&gt;

&lt;p&gt;The file covers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Project structure: where things live, what each package does.&lt;/li&gt;
  &lt;li&gt;Coding conventions: guard clauses, error handling, test naming, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;utils&lt;/code&gt; package.&lt;/li&gt;
  &lt;li&gt;Domain language: subscription not order, box not product, delivery day not shipping date.&lt;/li&gt;
  &lt;li&gt;Build and test commands: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go test ./...&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go vet ./...&lt;/code&gt;, how to run the linter.&lt;/li&gt;
  &lt;li&gt;What not to do: no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface{}&lt;/code&gt;, no global state, no utility packages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tom commits it. The next morning, he prompts the LLM with the same delivery date task. Without changing his prompt at all, the generated code comes back with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeliveryDate&lt;/code&gt; type, a guard clause, and the domain terminology.&lt;/p&gt;

&lt;p&gt;“It read the brief,” he says.&lt;/p&gt;

&lt;p&gt;“It read the brief,” Priya confirms.&lt;/p&gt;

&lt;h3 id=&quot;when-the-team-grows&quot;&gt;When the team grows&lt;/h3&gt;

&lt;p&gt;A month later, Kai joins the project. He’s a contractor, less familiar with the codebase. His first day, he sets up Claude Code, opens the repo, and starts working. His first PR looks like it was written by someone who’s been on the project for months. The naming is right. The patterns match. The test structure follows the team’s convention.&lt;/p&gt;

&lt;p&gt;Tom reviews it in fifteen minutes. No style questions. No “we don’t do it that way” comments. Just a review of the logic.&lt;/p&gt;

&lt;p&gt;“This is the real win,” Lee says. “The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; isn’t just for the LLM. It’s for every developer who works &lt;em&gt;with&lt;/em&gt; the LLM. When the brief is right, the generated code teaches the patterns to new team members faster than any onboarding document.”&lt;/p&gt;

&lt;p&gt;Kai reads the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; himself, separate from the LLM. “This is the best onboarding doc I’ve ever seen,” he says. “And it’s thirteen lines of conventions.”&lt;/p&gt;

&lt;h3 id=&quot;beyond-the-project-root&quot;&gt;Beyond the project root&lt;/h3&gt;

&lt;p&gt;The team discovers that some conventions are package-specific. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscription&lt;/code&gt; package has rules about status transitions that don’t apply elsewhere. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;billing&lt;/code&gt; package has rules about how invoice amounts are stored (cents, not dollars).&lt;/p&gt;

&lt;p&gt;Claude Code supports &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; files in subdirectories. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscription/&lt;/code&gt; applies when working in that package. The root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; applies everywhere. The specificity model is the same as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt;, closest file wins for its scope, with the root as the baseline.&lt;/p&gt;

&lt;p&gt;Tom adds a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscription&lt;/code&gt; package:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Status transitions: Pending → Active → Paused → Active (resume) or Cancelled.
Paused subscriptions cannot change box size.
Cancelled subscriptions cannot be modified at all.
NewSubscription starts in StatusPending.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Four lines. The LLM generates subscription code that respects the status rules every time.&lt;/p&gt;

&lt;h3 id=&quot;specialised-agents&quot;&gt;Specialised agents&lt;/h3&gt;

&lt;p&gt;Priya finds the next piece. “What if the LLM could behave differently depending on the task? When it’s writing tests, it should be thorough and consider edge cases. When it’s reviewing code, it should check for convention drift. When it’s writing migration code, it should be conservative and prefer backwards compatibility.”&lt;/p&gt;

&lt;p&gt;This is what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; does. Where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; is the general brief, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; defines specialised roles, agents with specific instructions, tools, and constraints.&lt;/p&gt;

&lt;p&gt;The team starts with two:&lt;/p&gt;

&lt;p&gt;A test writer agent that knows about the team’s test conventions, table-driven tests, descriptive names, the distinction between hard constraints and soft expectations in test naming.&lt;/p&gt;

&lt;p&gt;A reviewer agent that checks PRs for convention drift, exported fields that should be unexported, missing error handling, deep nesting that could be a guard clause.&lt;/p&gt;

&lt;p&gt;Priya sets these up. When she asks the LLM to write tests, it applies the test writer’s conventions automatically. When Tom asks for a code review, the reviewer checks for the patterns the team has agreed on.&lt;/p&gt;

&lt;p&gt;“The agents encode what we’ve learned,” Tom realises. “If someone new joins, they don’t just get the conventions, they get the reasoning built into the tool.”&lt;/p&gt;

&lt;p&gt;Lee smiles. “That’s the best kind of process. The kind that outlives the person who set it up.”&lt;/p&gt;

&lt;h3 id=&quot;what-the-team-learned&quot;&gt;What the team learned&lt;/h3&gt;

&lt;p&gt;Three months later, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; has been updated fourteen times. Each update is small, a line added when a new convention is agreed, a line removed when a pattern is abandoned. The file is a living document of the team’s coding standards, maintained not by discipline but by self-interest: when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; is accurate, the LLM generates better code, and reviews go faster.&lt;/p&gt;

&lt;p&gt;Tom, who started the week typing bare prompts and getting inconsistent results, now treats the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; as seriously as the test suite. “Tests tell you if the code is correct. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; tells the LLM how to write code that’s correct &lt;em&gt;and&lt;/em&gt; consistent.”&lt;/p&gt;

&lt;p&gt;The insight that sticks: the style of your codebase is a few-shot prompt. When the codebase is consistent, the LLM generates consistent code. When the conventions are explicit, the LLM follows them. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; is just making that implicit prompt explicit, and shareable across a team.&lt;/p&gt;

&lt;h3 id=&quot;what-the-files-look-like&quot;&gt;What the files look like&lt;/h3&gt;

&lt;p&gt;The team’s actual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; files, what goes in them, how they’re structured, and how they shape the LLM’s output, are worth seeing in detail. Next: &lt;a href=&quot;/writing/teaching-your-llm-the-codebase-claude-md-and-agents-md/&quot;&gt;CLAUDE.md and AGENTS.md in practice&lt;/a&gt;.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Behaviour-Driven Development: From Stories to Working Software</title>
    <link href="/writing/behaviour-driven-development-from-stories-to-working-software/"/>
    <updated>2026-04-07T06:00:00+08:00</updated>
    <id>/writing/behaviour-driven-development-from-stories-to-working-software/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/shipping-what-matters/&quot;&gt;Shipping What Matters&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;The Greenbox team hit 214 subscribers. The sprint cadence is working. &lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Storming&lt;/a&gt; gave them shared understanding. &lt;a href=&quot;/writing/example-mapping-making-stories-concrete/&quot;&gt;Example Mapping&lt;/a&gt; made their stories concrete. &lt;a href=&quot;/writing/sprint-planning-turning-sticky-notes-into-delivery/&quot;&gt;The sprint rhythm&lt;/a&gt; turned sticky notes into delivery.&lt;/p&gt;

&lt;p&gt;But bugs keep appearing.&lt;/p&gt;

&lt;p&gt;Not catastrophic bugs: the payment system works, the delivery scheduling is solid. But edge cases slip through. The delivery date calculation breaks on public holidays because nobody checked. The box-size switch fails if a customer changes on Wednesday instead of Monday. A paused subscriber gets charged because the retry logic doesn’t check pause state. Each one is a twenty-minute fix. Each one costs trust.&lt;/p&gt;

&lt;p&gt;The team has concrete examples from their Example Mapping sessions: context, action, outcome, written on cards. But those cards are on a table. The code is on a screen. Somewhere between the two, the details get lost.&lt;/p&gt;

&lt;h3 id=&quot;a-language-for-examples&quot;&gt;A language for examples&lt;/h3&gt;

&lt;p&gt;The Example Map gave the team examples as Context/Action/Outcome. There’s a step between “cards on a table” and “something a test framework can run.” The team needs a way to express those examples formally enough for a computer to use, while keeping them readable enough that Maya can look at them and say “yes, that’s what I meant.”&lt;/p&gt;

&lt;p&gt;The language for this is Gherkin. Three keywords. Given, When, and Then, mapping directly to Context/Action/Outcome.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Given sets up the context: what’s true before anything happens.&lt;/li&gt;
  &lt;li&gt;When describes the action: what someone does.&lt;/li&gt;
  &lt;li&gt;Then states the outcome: what should be true afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A trivial example:&lt;/p&gt;

&lt;div class=&quot;language-gherkin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;it is raining
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I go outside
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I should get wet
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No code. No special syntax. Anyone can read it. It’s the same pattern the team already used on their green cards, just formalised with keywords a test framework can parse.&lt;/p&gt;

&lt;h3 id=&quot;from-example-map-to-gherkin&quot;&gt;From Example Map to Gherkin&lt;/h3&gt;

&lt;p&gt;The Example Map output for “Subscribe to a produce box” is already there:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Rule: Customer must choose a box size (Small $25/week, Large $45/week)&lt;/li&gt;
  &lt;li&gt;Rule: Payment must succeed (valid card → confirmed, declined card → retry)&lt;/li&gt;
  &lt;li&gt;Rule: Customer sees their first delivery date (Monday → this Thursday, Friday → next Thursday)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take the delivery date example from the green card:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Context: delivery day is Thursday, minimum lead time is 3 days. Sarah subscribes on Friday. → First delivery is next Thursday.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translated to Gherkin:&lt;/p&gt;

&lt;div class=&quot;language-gherkin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;today is Friday
&lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;deliveries happen on Thursdays
&lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;the minimum lead time is 3 days
&lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;a customer has a valid payment method
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;they subscribe to the &lt;span class=&quot;s&quot;&gt;&quot;Small&quot;&lt;/span&gt; box
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;their first delivery date should be next Thursday
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Mechanical translation. The hard thinking already happened round the table with Maya and the team.&lt;/p&gt;

&lt;h3 id=&quot;the-feature-file&quot;&gt;The Feature file&lt;/h3&gt;

&lt;p&gt;Individual scenarios group into a Feature file: one coherent piece of behaviour. A Background section captures context shared across every scenario.&lt;/p&gt;

&lt;div class=&quot;language-gherkin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Subscribe to a produce box
  Customers want a regular supply of fresh, local produce
  without having to think about it each week.

  &lt;span class=&quot;kn&quot;&gt;Background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;Given the following box sizes are available&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;price&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Small&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;$25/week&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Large&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;$45/week&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;

  &lt;span class=&quot;kn&quot;&gt;Scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Subscribing with a valid payment method
    &lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;a customer has a valid payment method
    &lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;they subscribe to the &lt;span class=&quot;s&quot;&gt;&quot;Small&quot;&lt;/span&gt; box
    &lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;their subscription should be confirmed
    &lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;they should see their first delivery date

  &lt;span class=&quot;kn&quot;&gt;Scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Payment is declined
    &lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;a customer has an expired credit card
    &lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;they subscribe to the &lt;span class=&quot;s&quot;&gt;&quot;Small&quot;&lt;/span&gt; box
    &lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;no subscription should be created
    &lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;they should be asked to update their payment method

  &lt;span class=&quot;kn&quot;&gt;Scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Subscribing without enough lead time
    &lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;today is Friday
    &lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;deliveries happen on Thursdays
    &lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;the minimum lead time is 3 days
    &lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;a customer has a valid payment method
    &lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;they subscribe to the &lt;span class=&quot;s&quot;&gt;&quot;Small&quot;&lt;/span&gt; box
    &lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;their first delivery date should be next Thursday
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each rule from the Example Map maps to one or more scenarios. Each green card becomes concrete data inside a scenario. You’re not staring at a blank file wondering what to write. The conversation already happened. You’re transcribing.&lt;/p&gt;

&lt;h3 id=&quot;the-bdd-cycle-story-unit-code&quot;&gt;The BDD cycle: story, unit, code&lt;/h3&gt;

&lt;p&gt;Now the team has scenarios: acceptance tests describing the agreed behaviour. But you don’t implement them top-down. You work inward, using two loops.&lt;/p&gt;

&lt;p&gt;The outer loop is the acceptance test, the Gherkin scenario itself. The inner loop is unit tests driving the implementation. The acceptance test tells you when you’re done. The unit tests tell you how to get there.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Pick a scenario. Run it. RED: it fails because nothing exists yet.&lt;/li&gt;
  &lt;li&gt;Drop to unit tests. Write a small, focused test. RED.&lt;/li&gt;
  &lt;li&gt;Write the simplest code that makes it pass. GREEN.&lt;/li&gt;
  &lt;li&gt;Refactor if needed.&lt;/li&gt;
  &lt;li&gt;Repeat 2-4 until the acceptance test passes. GREEN.&lt;/li&gt;
  &lt;li&gt;Move to the next scenario.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;worked-example-greenbox-subscription-in-go&quot;&gt;Worked example: Greenbox subscription in Go&lt;/h3&gt;

&lt;p&gt;The code that follows is deliberately simple; it shows the BDD rhythm without the noise of a real production system. The discovery techniques produce the same concrete examples regardless of implementation complexity.&lt;/p&gt;

&lt;p&gt;Tom and Priya are implementing the subscription story together. They’re sitting side by side for the first time. Priya usually works with headphones on, Tom usually works alone. He notices she names her tests differently. “How do you name tests?” he asks. “I describe what the customer expects, not what the code does,” she says. It’s a small thing. Tom starts doing it too.&lt;/p&gt;

&lt;h4 id=&quot;delivery-date-calculator&quot;&gt;Delivery date calculator&lt;/h4&gt;

&lt;p&gt;They start with the third scenario, delivery date calculation, because it’s pure logic with no external dependencies. Self-contained, well-specified by the Example Map, easy to test in isolation.&lt;/p&gt;

&lt;p&gt;The rules:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Deliveries happen on Thursdays&lt;/li&gt;
  &lt;li&gt;Minimum lead time is 3 days&lt;/li&gt;
  &lt;li&gt;Subscribe on Monday → this Thursday (3 days, just enough)&lt;/li&gt;
  &lt;li&gt;Subscribe on Friday → next Thursday (less than 3 days to this Thursday, rolls forward)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RED. Unit test first.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// delivery_test.go&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;greenbox&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;testing&quot;&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;time&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TestFirstDeliveryDate_MondaySubscription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testing&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;monday&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;23&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UTC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;deliveryDay&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Thursday&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;minLeadDays&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;got&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FirstDeliveryDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;monday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deliveryDay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;minLeadDays&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;want&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;26&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UTC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;got&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Equal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;want&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;FirstDeliveryDate(%v, Thursday, 3) = %v, want %v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;monday&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Weekday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;got&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Weekday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;want&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Weekday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Won’t compile. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirstDeliveryDate&lt;/code&gt; doesn’t exist yet. That’s the point.&lt;/p&gt;

&lt;p&gt;GREEN. Write the function.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// delivery.go&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;greenbox&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;time&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FirstDeliveryDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deliveryDay&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Weekday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;minLeadDays&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;earliest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AddDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;minLeadDays&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;daysUntil&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deliveryDay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;earliest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Weekday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;7&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;daysUntil&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;earliest&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;earliest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AddDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;daysUntil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Test passes.&lt;/p&gt;

&lt;p&gt;RED. Edge case from the Example Map: Friday subscription.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TestFirstDeliveryDate_FridaySubscription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testing&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;friday&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;27&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UTC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;deliveryDay&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Thursday&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;minLeadDays&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;got&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FirstDeliveryDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deliveryDay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;minLeadDays&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;want&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UTC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;got&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Equal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;want&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;FirstDeliveryDate(%v, Thursday, 3) = %v, want %v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;friday&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Monday&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;got&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Monday 2006-01-02&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;want&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Monday 2006-01-02&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GREEN. Already passes. The modular arithmetic handles it naturally. One of the pleasures of TDD: you write a test expecting failure, and it passes, telling you your implementation is more general than you thought.&lt;/p&gt;

&lt;h4 id=&quot;subscription-creation&quot;&gt;Subscription creation&lt;/h4&gt;

&lt;p&gt;The second piece: creating the subscription, including payment.&lt;/p&gt;

&lt;p&gt;RED.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// subscription_test.go&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;greenbox&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;testing&quot;&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;time&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fakeGateway&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shouldSucceed&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;chargedAmount&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fakeGateway&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Charge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;amountCents&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chargedAmount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;amountCents&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shouldSucceed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TestSubscribe_ValidPayment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testing&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fakeGateway&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shouldSucceed&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;delivery&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;26&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UTC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Small&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delivery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Fatalf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;unexpected error: %v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BoxSize&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Small&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;BoxSize = %q, want %q&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BoxSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Small&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PricePerWeek&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2500&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;PricePerWeek = %d, want %d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PricePerWeek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirstDelivery&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Equal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delivery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;FirstDelivery = %v, want %v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirstDelivery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delivery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chargedAmount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2500&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;charged %d, want %d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chargedAmount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GREEN. Simplest thing that passes.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// subscription.go&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;greenbox&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;errors&quot;&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;time&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrPaymentDeclined&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;New&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;payment declined&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Subscription&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;BoxSize&lt;/span&gt;       &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;PricePerWeek&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;FirstDelivery&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PaymentGateway&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Charge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;amountCents&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ok&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boxSize&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;priceCents&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PaymentGateway&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;firstDelivery&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Subscription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Charge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;priceCents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Subscription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;BoxSize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;       &lt;span class=&quot;n&quot;&gt;boxSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;PricePerWeek&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;priceCents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;FirstDelivery&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;firstDelivery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Test passes. But the implementation is deliberately naive; it ignores the payment result. The next test will force the fix.&lt;/p&gt;

&lt;p&gt;RED. Declined payment.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TestSubscribe_DeclinedPayment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testing&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fakeGateway&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shouldSucceed&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;delivery&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;26&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UTC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Small&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delivery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrPaymentDeclined&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;err = %v, want %v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrPaymentDeclined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;subscription should be nil when payment declined&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Fails. The current implementation always returns a subscription.&lt;/p&gt;

&lt;p&gt;GREEN.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boxSize&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;priceCents&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PaymentGateway&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;firstDelivery&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Subscription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Charge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;priceCents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ok&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrPaymentDeclined&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Subscription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;BoxSize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;       &lt;span class=&quot;n&quot;&gt;boxSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;PricePerWeek&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;priceCents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;FirstDelivery&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;firstDelivery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Both tests pass. Four unit tests, two source files, clean types, narrow interfaces.&lt;/p&gt;

&lt;h3 id=&quot;step-definitions-the-glue&quot;&gt;Step definitions: the glue&lt;/h3&gt;

&lt;p&gt;Step definitions connect Gherkin keywords to your application. When the test runner sees &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;When they subscribe to the &quot;Small&quot; box&lt;/code&gt;, it needs a function that calls your real &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Subscribe&lt;/code&gt; code.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;iSubscribeToTheBox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stripeGateway&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;greenbox&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boxPrice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;greenbox&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirstDeliveryDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Thursday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;lastError&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;lastSubscription&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Thin on purpose. It delegates to the real functions the team already wrote and tested. No business logic. Just glue.&lt;/p&gt;

&lt;p&gt;Three guidelines for keeping them healthy:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Keep them thin. If you’re writing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; statements or business logic inside a step definition, the logic belongs in domain code where it’s unit-tested.&lt;/li&gt;
  &lt;li&gt;Use consistent language. If the team says “subscribe,” every step says “subscribe.” Inconsistent language means duplicate step definitions doing the same thing with different words.&lt;/li&gt;
  &lt;li&gt;Maintain them like production code. Review in PRs. Refactor when the domain language evolves. Delete when scenarios are removed. If step definitions drift from reality, the team stops trusting the scenarios, stops maintaining them, and BDD quietly dies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Priya suggests running the Gherkin tests automatically. “We’re writing tests that prove the code does what Maya expects. Why are we running them by hand?” She sets up a GitHub Action so tests run on every pull request. It takes her an afternoon. The first automated run catches a bug in Tom’s payment retry logic that manual testing missed. Tom: “That saved me a day.” Priya: “That saved a customer.”&lt;/p&gt;

&lt;h3 id=&quot;llms-as-implementation-partners&quot;&gt;LLMs as implementation partners&lt;/h3&gt;

&lt;p&gt;Here’s the thing about everything you just read: an &lt;label for=&quot;sn-writing-behaviour-driven-development-from-stories-to-working-software-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-behaviour-driven-development-from-stories-to-working-software-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-behaviour-driven-development-from-stories-to-working-software-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-behaviour-driven-development-from-stories-to-working-software-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; could have written most of it.&lt;/p&gt;

&lt;p&gt;Not the Example Map. Not the discovery conversation where Maya explained that deliveries happen on Thursdays and the minimum lead time is three days. Not the moment when Tom asked “what about Friday?” and surfaced an edge case. The LLM wasn’t in the room for that.&lt;/p&gt;

&lt;p&gt;But the code? You could hand an LLM the Feature file and say: “Write me a Go implementation with tests that makes these scenarios pass.” And it would produce something remarkably close to what you just read. The behaviour would be correct, because the scenarios are concrete and unambiguous. There’s no room for the LLM to guess wrong about what “subscribe” means when the Feature file spells it out.&lt;/p&gt;

&lt;p&gt;A caveat. LLMs are good at the happy path. They’ll miss things you &lt;em&gt;didn’t&lt;/em&gt; specify: network timeouts, concurrency issues, flaky payment gateways. Code review isn’t optional. Budget roughly half your time for reviewing and hardening what comes back. The discovery work is what makes this review &lt;em&gt;possible&lt;/em&gt;. Because you have concrete examples, you can check the LLM’s output against something specific. Without that, you’re reviewing code against vibes.&lt;/p&gt;

&lt;p&gt;The pipeline:&lt;/p&gt;

&lt;div style=&quot;display: flex; align-items: center; gap: var(--space-xs); margin: var(--space-md) 0; flex-wrap: wrap;&quot;&gt;
  &lt;div style=&quot;background: rgba(255, 243, 176, 0.25); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85em;&quot;&gt;
    &lt;strong&gt;&lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot; style=&quot;text-decoration: none; color: inherit;&quot;&gt;Event Storming&lt;/a&gt;&lt;/strong&gt;
  &lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(255, 243, 176, 0.25); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85em;&quot;&gt;
    &lt;strong&gt;&lt;a href=&quot;/writing/example-mapping-making-stories-concrete/&quot; style=&quot;text-decoration: none; color: inherit;&quot;&gt;Example Mapping&lt;/a&gt;&lt;/strong&gt;
  &lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(179, 217, 255, 0.25); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85em;&quot;&gt;
    &lt;strong&gt;BDD Scenarios&lt;/strong&gt;
  &lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(184, 230, 184, 0.25); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85em;&quot;&gt;
    &lt;strong&gt;Hand to LLM&lt;/strong&gt;
  &lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(179, 217, 255, 0.25); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85em;&quot;&gt;
    &lt;strong&gt;Review Output&lt;/strong&gt;
  &lt;/div&gt;
  &lt;span style=&quot;color: var(--color-ink-tertiary);&quot;&gt;&amp;rarr;&lt;/span&gt;
  &lt;div style=&quot;background: rgba(184, 230, 184, 0.25); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); text-align: center; font-size: 0.85em;&quot;&gt;
    &lt;strong&gt;Ship&lt;/strong&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Everything left of “Hand to LLM” is human thinking. Everything right is review and refinement. The human work is the thinking. The LLM work is the typing. Both are necessary. Neither is sufficient alone.&lt;/p&gt;

&lt;p&gt;While implementing the payment integration, Tom makes a deliberate shortcut: he hardcodes the currency to AUD instead of making it configurable. He writes a comment: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;// SHORTCUT: AUD only. If we ever go international, this needs to change.&lt;/code&gt; Lee sees it during review and says: “That’s a good shortcut. You know it’s there, you know when it’ll matter, and you’ve documented it. Technical debt is fine when it’s conscious.” Tom carries the idea forward: debt is a choice, not an accident. The dangerous kind is the kind you don’t know you’re taking on.&lt;/p&gt;

&lt;p&gt;That same week, a subscriber emails Sam on Saturday: “Your website has been showing an error since yesterday afternoon.” Nobody noticed; they don’t monitor the site outside business hours. Sam signs up for a free uptime monitor that pings the site every five minutes and texts her if it’s down. It isn’t observability; it’s a text message. But it’s the first time a machine is watching instead of a person.&lt;/p&gt;

&lt;h3 id=&quot;but-are-we-building-the-correct-things&quot;&gt;But are we building the correct things?&lt;/h3&gt;

&lt;p&gt;One thing Tom notices: the LLM generates code faster than he can review it. The code arrives clean and confident, but he can’t always tell if it’s correct until he traces through it line by line. The Feature file gives him something concrete to check against. But the speed creates an odd sensation: the bottleneck isn’t writing code any more; it’s knowing whether the code is correct.&lt;/p&gt;

&lt;p&gt;A few weeks in, the rhythm is working. Example Mapping eliminates the surprises. BDD catches bugs before production. The code quality is up. The board looks healthy.&lt;/p&gt;

&lt;p&gt;But the number that actually matters, active subscribers, is going backwards. They hit 214 at the end of the first sprint cycle. A month later, they’re at 197.&lt;/p&gt;

&lt;p&gt;Maya checks the number at her kitchen table one evening. Nadia looks over her shoulder. “Is that good?”&lt;/p&gt;

&lt;p&gt;“It’s going the wrong way.”&lt;/p&gt;

&lt;p&gt;Churn is eating the growth. For every ten new subscribers, three or four cancel. The team is building well, but subscriber count doesn’t care about code quality.&lt;/p&gt;

&lt;p&gt;The frustrating thing is that the team &lt;em&gt;is&lt;/em&gt; doing good work. They’ve built a solid subscription system, payment processing, delivery date logic. Tom has been improving the admin tools. Jas redesigned the onboarding flow. Sam is pushing for a farm analytics dashboard. Everyone has a reasonable next thing to build.&lt;/p&gt;

&lt;p&gt;But nobody has stepped back to ask: &lt;em&gt;which of these things will actually stop the bleeding?&lt;/em&gt; A prettier onboarding flow won’t fix churn. A farm dashboard won’t either. The team is efficiently building features that don’t address the problem.&lt;/p&gt;

&lt;p&gt;Maya raises it at the Monday standup. “We’re shipping faster than ever. But we’re shrinking. Something’s wrong and I don’t think the answer is to ship even faster.”&lt;/p&gt;

&lt;p&gt;Which stories should the team be building? How do they connect work to the business goal? For that, they need a technique that works backwards from outcomes, one that forces the question “why are we building this?”&lt;/p&gt;

&lt;p&gt;Lee suggests a technique that works backwards from the goal. It’s called &lt;a href=&quot;/writing/impact-mapping-connecting-work-to-goals/&quot;&gt;Impact Mapping&lt;/a&gt;, and it starts with one question: why are we building this?&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Quiet Jar in the Fridge</title>
    <link href="/writing/the-quiet-jar-in-the-fridge/"/>
    <updated>2026-04-05T06:00:00+08:00</updated>
    <id>/writing/the-quiet-jar-in-the-fridge/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/consulting-and-craft/&quot;&gt;Consulting and Craft&lt;/a&gt; &amp;middot; &lt;a href=&quot;/writing/through-the-kitchen/&quot;&gt;Through the Kitchen&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;I am making a sourdough starter today. Fresh jar, a scoop of wholemeal flour, a splash of water, a stir with a cheap rubber spatula. Not precious about it. In six weeks, if I do this properly, I’ll have bread again.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The last one was two and a half years old when it died, not through drama, through a quiet chain of postponed feeds that started with a busy week and ended two months later when I opened the jar to a monstrous mess of black mould that was by this point very nearly ambulatory and would, given another week, probably have begun drafting grievances about the state of the fridge. I scraped it into the bin, washed the jar, and here we are.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’m not new to sourdough. I’ve made every beginner mistake and a few advanced ones. This post is about what I intend to do differently, and why almost all of it is actually about software.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;what-a-starter-actually-is&quot;&gt;What a starter actually is&lt;/h3&gt;

&lt;p&gt;A sourdough starter is a colony of wild yeast and lactic acid bacteria living in a paste of flour and water. You feed it, it eats the sugars, it rises and falls. You bake with some, set the rest aside, feed it again. Flour, water, time, consistency. There is no secret. The mystique around sourdough is almost entirely vibes.&lt;/p&gt;

&lt;p&gt;Starters aren’t hard to make. They’re hard to &lt;em&gt;keep&lt;/em&gt;. And everything I failed to do with the last one is something I was failing to do in a codebase somewhere at the same time.&lt;/p&gt;

&lt;h3 id=&quot;lesson-one-consistency-beats-intensity&quot;&gt;Lesson one: consistency beats intensity&lt;/h3&gt;

&lt;p&gt;Somewhere in the first week or two of a new starter, it will begin to smell strongly of acetone, the acrid chemical note of nail polish remover. All new starters do this. It’s a normal stage of the culture establishing itself, but if you haven’t seen it before it can make you worry.&lt;/p&gt;

&lt;p&gt;This is the moment most new bakers panic. They read the first thing the internet tells them, “your starter is sick, feed it more”, and do the wrong thing with great care: more frequent feeds, stronger flour, warmer water, twice-daily instead of once. It feels active. It’s almost exactly the opposite of what the starter needs. I know, because it was me in my very first week, and the starter responded by smelling worse for longer than if I’d left it alone.&lt;/p&gt;

&lt;p&gt;The fix is boring. One feed a day, same time, same ratio, same flour, until the phase passes. A small ritual I do while I make my wife tea.&lt;/p&gt;

&lt;p&gt;The best starters are not the ones fed most dramatically, but the ones fed most reliably.&lt;/p&gt;

&lt;p&gt;The team that runs a three-week “tech debt sprint” every quarter is feeding their codebase intensely but inconsistently. The team that quietly deletes one dead file, writes one missing test, and closes one stale TODO every week is feeding it consistently. Six months later the second team has the cleaner codebase &lt;em&gt;and&lt;/em&gt; a deeper understanding of it. Twelve months later it isn’t even close.&lt;/p&gt;

&lt;p&gt;Consistency compounds. Intensity burns out. The last starter died because I fed it generously on Sundays and missed too many Thursdays in a row.&lt;/p&gt;

&lt;h3 id=&quot;lesson-two-maintenance-is-not-waste&quot;&gt;Lesson two: maintenance is not waste&lt;/h3&gt;

&lt;p&gt;Every time you feed a starter, you throw most of it away. It feels profligate. It feels like you’re killing the thing you’re trying to grow.&lt;/p&gt;

&lt;p&gt;The reason isn’t volume; it’s ratios. Between feeds the microbes exhaust the sugars and leave their waste behind. The culture turns tired and acidic, and the yeast, which is what actually makes bread rise, struggles in those conditions because bacteria tolerate them better. Leave it long enough and the yeast is outcompeted and the starter goes sour and sluggish.&lt;/p&gt;

&lt;p&gt;The discard resets the balance. Throw most of the culture away, keep a small inoculum of still-healthy microbes, feed it generously. The microbes have a huge meal ahead and plenty of space. They multiply back to strength, the yeast keeps up, and the discard itself isn’t waste, it makes excellent crackers, pancakes, and pizza dough.&lt;/p&gt;

&lt;p&gt;Codebases are the same. Every week I delete some code, dead feature flags, tests that no longer test what the code does, config files for services we stopped running. Each deletion is uncomfortable in the moment, because &lt;em&gt;I wrote this, and it meant something once&lt;/em&gt;. But what remains is closer to the shape I can work with. The point isn’t the &lt;em&gt;size&lt;/em&gt; of the codebase; it’s the ratio of living code to tired nobody-remembers-why-this-is-here code. Removal isn’t the opposite of care. It &lt;em&gt;is&lt;/em&gt; the care.&lt;/p&gt;

&lt;p&gt;And keep the whole thing small. My starter lives in a small jar in the fridge. Unimpressive. Not Instagram-worthy. It waits quietly for Friday afternoon before Saturday’s bake. The counter-top sourdough that looks impressive in a sunlit photograph is also the one that usually dies when life gets busy. Good maintenance is almost always quieter than the thing it’s maintaining.&lt;/p&gt;

&lt;h3 id=&quot;lesson-three-the-practice-not-the-artefact&quot;&gt;Lesson three: the practice, not the artefact&lt;/h3&gt;

&lt;p&gt;If the new starter lives twenty years, good. If it dies in two months and I start another, also fine. The point isn’t this jar; it’s whether I can keep the practice going.&lt;/p&gt;

&lt;p&gt;The San Francisco sourdough at Boudin Bakery has been continuously fed since 1849, older than California’s state government. The actual organisms don’t live anything like that long: yeast cells bud and split every two to three hours, and no cell in today’s culture has any ancestor alive more than a few weeks ago. What persists is the &lt;em&gt;practice&lt;/em&gt; of feeding the jar. The culture is remade every week. The practice is the thing.&lt;/p&gt;

&lt;p&gt;The code you wrote ten years ago is mostly gone by now, rewritten, replaced, deleted, refactored into something unrecognisable. What remains is the habit of care. Code is the artefact. The practice is the point.&lt;/p&gt;

&lt;h3 id=&quot;today-the-jar-has-nothing-in-it&quot;&gt;Today, the jar has nothing in it&lt;/h3&gt;

&lt;p&gt;The starter has made nothing so far. It isn’t even, strictly, a starter yet, a scoop of wholemeal flour, a splash of water, whatever wild yeast happened to be on the flour. What it has is an &lt;em&gt;idea&lt;/em&gt; of what it will become and a set of practices I intend to follow to get it there.&lt;/p&gt;

&lt;p&gt;Tomorrow morning I’ll discard most of it, add fresh flour and water, and stir. The morning after, the same again. The acetone phase will come and I’ll resist the urge to panic-feed. In six weeks I’ll bake bread I’m happy with.&lt;/p&gt;

&lt;p&gt;The thing I’m committing to today is not a jar. It’s a practice. The practice is what will produce bread; the jar is just where the evidence lives. The code I look after is the same: it needs my presence on a schedule I can keep, long enough for the compounding to catch up to the cleverness.&lt;/p&gt;

&lt;p&gt;Feed the practice. Learn from the maintenance. Stay focussed. Don’t get attached to the artefact. Start again when you have to.&lt;/p&gt;

&lt;p&gt;The practice is the point. Everything else is decoration.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Sprint Planning: Turning Sticky Notes into Delivery</title>
    <link href="/writing/sprint-planning-turning-sticky-notes-into-delivery/"/>
    <updated>2026-04-04T06:00:00+08:00</updated>
    <id>/writing/sprint-planning-turning-sticky-notes-into-delivery/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/from-chaos-to-clarity/&quot;&gt;From Chaos to Clarity&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;The Greenbox team has done remarkable work over the past two weeks. They &lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Stormed&lt;/a&gt; the whole domain onto a wall. They &lt;a href=&quot;/writing/example-mapping-making-stories-concrete/&quot;&gt;Example Mapped&lt;/a&gt; their stories into concrete scenarios with rules, examples, and edge cases.&lt;/p&gt;

&lt;p&gt;The wall looks beautiful. Sticky notes everywhere. Concrete examples for the first batch of stories. A shared understanding of what the team is building and why.&lt;/p&gt;

&lt;p&gt;It’s week six of twelve.&lt;/p&gt;

&lt;p&gt;Maya pulls Lee aside after the Monday standup. “We’ve spent two weeks on workshops. The wall looks great. But we haven’t shipped anything new since the subscription prototype. When do we start building?”&lt;/p&gt;

&lt;p&gt;Lee doesn’t answer directly. Instead he asks: “What’s Tom working on right now?”&lt;/p&gt;

&lt;p&gt;Maya knows. “The farm availability screen.”&lt;/p&gt;

&lt;p&gt;“And Priya?”&lt;/p&gt;

&lt;p&gt;“Delivery logistics.”&lt;/p&gt;

&lt;p&gt;“And which of those matters more for hitting 200 subscribers by the deadline?”&lt;/p&gt;

&lt;p&gt;Silence. Maya doesn’t know. Neither does Tom, when she looks at him. They’ve been building. Tom finished the subscription flow last week, pulled the next story off the map, started on farm availability. Priya is deep in delivery. Work is happening. But it’s happening the way it happened in &lt;a href=&quot;/writing/retrospectives-catching-the-wrong-kind-of-fast/&quot;&gt;week one&lt;/a&gt;: individually, without a shared sense of what the team is doing this week, or whether the pace is enough to hit the deadline.&lt;/p&gt;

&lt;p&gt;Six weeks left. 200 subscribers. And the team has no way of knowing whether they’re going to make it.&lt;/p&gt;

&lt;h3 id=&quot;the-missing-layer&quot;&gt;The missing layer&lt;/h3&gt;

&lt;p&gt;Lee draws a rough diagram on the whiteboard. Three circles, nested.&lt;/p&gt;

&lt;p&gt;“You’ve been working out here,” he says, pointing to the outer ring. “Event Storming gave you the big picture: the whole domain. Example Mapping gave you the detail: concrete rules and examples for each story.” He taps the innermost circle. “This is the bit you’re missing. The delivery layer. What are we doing &lt;em&gt;this fortnight&lt;/em&gt;? What does ‘done’ look like in two weeks? How do we know if we’re on pace?”&lt;/p&gt;

&lt;p&gt;Maya folds her arms. “We don’t have time for more process. We’ve got six weeks.”&lt;/p&gt;

&lt;p&gt;“This isn’t more process; it’s less chaos.”&lt;/p&gt;

&lt;h3 id=&quot;introducing-the-sprint&quot;&gt;Introducing the sprint&lt;/h3&gt;

&lt;p&gt;The concept is simple: two-week iterations. The team calls them sprints, though the name matters less than the rhythm.&lt;/p&gt;

&lt;p&gt;Every two weeks:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Plan together what they’ll build in the next fortnight.&lt;/li&gt;
  &lt;li&gt;Demo together what they actually shipped, to the whole team, not just the developers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every day:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Check in to surface blockers before they fester. Fifteen minutes, standing up, first thing in the morning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three practices. Lee keeps the ceremony light deliberately. Five people don’t need a Scrum Master, a Product Owner, and a burndown chart. They need a rhythm.&lt;/p&gt;

&lt;h3 id=&quot;monday-morning-the-first-sprint-planning&quot;&gt;Monday morning: the first sprint planning&lt;/h3&gt;

&lt;p&gt;The team gathers round the wall. Lee runs the session.&lt;/p&gt;

&lt;p&gt;“The goal for this sprint isn’t a list of stories; it’s a sentence. What do we need to be true in two weeks that isn’t true today?”&lt;/p&gt;

&lt;p&gt;Maya translates it: “This sprint, we ship the subscription flow end to end so we can onboard our first twenty pilot subscribers.”&lt;/p&gt;

&lt;p&gt;Lee writes it on a card and sticks it above the story map: Sprint 1 goal: ship subscription flow, onboard first 20 pilot subscribers.&lt;/p&gt;

&lt;p&gt;“Every story you pick should serve that goal. If it doesn’t, it doesn’t go in.”&lt;/p&gt;

&lt;p&gt;Six stories:&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; padding: var(--space-md); margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;font-weight: bold; margin-bottom: var(--space-sm); font-size: 0.88rem; color: var(--color-accent);&quot;&gt;Sprint 1 backlog&lt;/div&gt;
  &lt;div style=&quot;font-size: 0.85rem;&quot;&gt;
    &lt;div style=&quot;background: rgba(245,215,110,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;1. Customer selects box size and subscribes&lt;/div&gt;
    &lt;div style=&quot;background: rgba(245,215,110,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;2. Payment integration (Stripe, initial charge)&lt;/div&gt;
    &lt;div style=&quot;background: rgba(245,215,110,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;3. Confirmation email with first delivery date&lt;/div&gt;
    &lt;div style=&quot;background: rgba(245,215,110,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;4. Landing page with box descriptions and pricing&lt;/div&gt;
    &lt;div style=&quot;background: rgba(245,215,110,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;5. Farm submits weekly availability (basic version)&lt;/div&gt;
    &lt;div style=&quot;background: rgba(245,215,110,0.12); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm);&quot;&gt;6. Maya&apos;s matching tool (supply to demand, draft version)&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Tom looks at the list. “Six? We could do ten.”&lt;/p&gt;

&lt;p&gt;Lee shakes his head. “First sprint. You don’t know your pace yet. If you finish early, pull more. But it’s better to finish everything than to finish five of ten and feel behind.”&lt;/p&gt;

&lt;p&gt;Tom doesn’t look convinced. Priya catches his eye and gives a small nod. She’s been on teams before where overcommitting in sprint one set a miserable tone for the whole project.&lt;/p&gt;

&lt;p&gt;Six stories it is.&lt;/p&gt;

&lt;p&gt;They spend twenty minutes Example Mapping the stories that haven’t been mapped yet. The confirmation email story is quick: three rules, five examples, one red card about bounced emails. The farm availability story surfaces the same questions from earlier sessions: units, deadlines, update policies. Maya resolves the critical ones on the spot. The rest become red cards for next sprint.&lt;/p&gt;

&lt;p&gt;This is important: sprint planning isn’t just picking stories. It’s the moment where Example Mapping happens for the stories you’re about to build. Discovery and planning, in the same conversation.&lt;/p&gt;

&lt;h3 id=&quot;the-daily-standup&quot;&gt;The daily standup&lt;/h3&gt;

&lt;p&gt;Fifteen minutes, every morning. Three questions per person: what did I do yesterday, what am I doing today, is anything blocking me?&lt;/p&gt;

&lt;p&gt;The first three days feel odd. The team stands awkwardly in a circle. Tom gives a forty-five second summary of his code changes. Nobody has any blockers. The standup takes four minutes. Tom mutters something about it being a waste of time.&lt;/p&gt;

&lt;p&gt;Day four is different.&lt;/p&gt;

&lt;p&gt;Priya says: “I’m stuck. Stripe’s webhook for failed payments doesn’t include the subscription ID in the format we expected. I’ve been debugging it since yesterday afternoon.”&lt;/p&gt;

&lt;p&gt;Tom looks up from his phone. “I hit that last month on a side project. The subscription ID moved to a nested object. Want me to show you after this?”&lt;/p&gt;

&lt;p&gt;“Yes. Please.”&lt;/p&gt;

&lt;p&gt;Thirty seconds during the standup. Tom and Priya pair on it afterwards and resolve it in twenty minutes. Without the standup, Priya would have spent another half-day on it alone.&lt;/p&gt;

&lt;p&gt;That’s the pitch for daily check-ins: not the days when everything is fine, but the one day in five when someone’s stuck and the answer is sitting three metres away.&lt;/p&gt;

&lt;h3 id=&quot;the-first-sprint-review&quot;&gt;The first sprint review&lt;/h3&gt;

&lt;p&gt;Two weeks pass. Friday afternoon. The team gathers.&lt;/p&gt;

&lt;p&gt;Lee keeps it simple: “Show what you built. Not slides. Working software.”&lt;/p&gt;

&lt;p&gt;Tom shares his screen and walks through the subscription flow. A customer lands on the page, picks a box size, enters payment details, gets a confirmation with a delivery date. It works.&lt;/p&gt;

&lt;p&gt;Then Sam says: “Can I try it?”&lt;/p&gt;

&lt;p&gt;She picks up her laptop, goes to the landing page, and starts subscribing. She gets to the box selection screen and pauses. She’s been fielding exactly this question from potential subscribers all week: explaining the difference between small and large boxes over email, over the phone, at the Margaret River farmers’ market. Three people this week alone.&lt;/p&gt;

&lt;p&gt;“Which one’s the good one? Small or Large, what’s the difference? How many people does each one feed? There’s nothing on this page that helps them decide.”&lt;/p&gt;

&lt;p&gt;Jas pulls up her design file. “I had comparison copy in the original mockup. It got cut when we were trying to keep the first version simple.”&lt;/p&gt;

&lt;p&gt;Maya: “That’s not simple, that’s confusing.”&lt;/p&gt;

&lt;p&gt;Tom: “I can add it. Half a day, maybe less.”&lt;/p&gt;

&lt;p&gt;Sam spotted in thirty seconds what nobody caught during two weeks of development. That’s why the whole team demos, not just the developers. Sam thinks like a customer. Tom and Priya think like engineers. You need both perspectives seeing the same thing.&lt;/p&gt;

&lt;p&gt;Here’s the sprint review scorecard:&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; overflow: hidden; margin: var(--space-md) 0;&quot;&gt;
  &lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 0.85rem;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background: rgba(74,144,217,0.08);&quot;&gt;
        &lt;th style=&quot;text-align: left; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Story&lt;/th&gt;
        &lt;th style=&quot;text-align: center; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); width: 100px;&quot;&gt;Status&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Customer selects box and subscribes&lt;/td&gt;&lt;td style=&quot;text-align: center; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); color: green;&quot;&gt;Done&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Payment integration&lt;/td&gt;&lt;td style=&quot;text-align: center; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); color: green;&quot;&gt;Done&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Confirmation email&lt;/td&gt;&lt;td style=&quot;text-align: center; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); color: green;&quot;&gt;Done&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Landing page&lt;/td&gt;&lt;td style=&quot;text-align: center; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); color: green;&quot;&gt;Done&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule);&quot;&gt;Farm availability (basic)&lt;/td&gt;&lt;td style=&quot;text-align: center; padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--color-rule); color: green;&quot;&gt;Done&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;padding: var(--space-xs) var(--space-sm);&quot;&gt;Maya&apos;s matching tool (draft)&lt;/td&gt;&lt;td style=&quot;text-align: center; padding: var(--space-xs) var(--space-sm); color: orange;&quot;&gt;Partial&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;Five done, one partial. The matching tool has the basic algorithm working but no UI yet; Maya is running it from a command line script. Lee marks it as carried over to sprint two.&lt;/p&gt;

&lt;p&gt;Tom, who wanted ten stories, sees six was exactly correct. If they’d committed to ten, the story would be “we missed our target” instead of “we nearly hit it.”&lt;/p&gt;

&lt;p&gt;That evening, Tom mentions to Sarah that Lee was correct about the six stories. “I would have overcommitted and then blamed the process,” he says.&lt;/p&gt;

&lt;p&gt;Sarah looks up from marking papers. “You sound surprised that someone else was right.”&lt;/p&gt;

&lt;p&gt;“I’m surprised I listened,” Tom says.&lt;/p&gt;

&lt;h3 id=&quot;seeing-the-trajectory&quot;&gt;Seeing the trajectory&lt;/h3&gt;

&lt;p&gt;After the review, Lee draws a simple chart on the whiteboard. Horizontal: weeks remaining. Vertical: subscriber count. He plots where they are, week 6, 38 pilot subscribers from the manual Google Form days, and draws a dotted line from 38 to 200.&lt;/p&gt;

&lt;p&gt;“You’ve got six weeks and you need to more than quintuple what you’ve got now. Thirty-eight said yes when there was nothing but Maya’s promise and a spreadsheet. Now you’ve got software and a team. Every two weeks, we’ll plot where you actually are. If you’re falling behind, you’ll know in two weeks, not four.”&lt;/p&gt;

&lt;p&gt;Priya takes a photo of the chart and pins it in the team Slack channel. She updates it every Friday. It becomes her quiet ritual, the act that makes the numbers visible to everyone. Nobody asks her to. She just does it.&lt;/p&gt;

&lt;p&gt;The first data point goes on the chart at the end of sprint one: 42 pilot subscribers. Four new signups through the self-service flow in the last few days of the sprint, after the landing page went live. The software works. Four is not very many. The line to 200 still sits far above them, and the gap between “we can take signups” and “people are actually signing up” is now a visible, numerical fact. Tom stares at the whiteboard for a moment and then goes back to his desk.&lt;/p&gt;

&lt;h3 id=&quot;sprint-two-the-rhythm-clicks&quot;&gt;Sprint two: the rhythm clicks&lt;/h3&gt;

&lt;p&gt;Sam is answering subscriber emails from her personal Gmail. By the end of sprint one, she’s getting fifteen emails a day. She sets up a shared inbox, support@greenbox.com.au, password on a sticky note stuck to Maya’s monitor. The same three questions every week: &lt;em&gt;When does my box arrive? Can I skip a week? What’s in the box?&lt;/em&gt; She starts a spreadsheet to track them. Mrs Patterson emails twice about her delivery day, polite both times.&lt;/p&gt;

&lt;p&gt;Sprint two planning happens on Monday morning. Forty minutes instead of ninety.&lt;/p&gt;

&lt;p&gt;Sprint goal: Ship the farm portal, wire up referrals, and grow to 80 subscribers.&lt;/p&gt;

&lt;p&gt;Eight stories this time, two more than sprint one. Lee raises an eyebrow but doesn’t object.&lt;/p&gt;

&lt;p&gt;The daily standups get faster. By day three, four minutes. On Wednesday, Jas mentions that the farm portal design has a problem: she’s designed it for a desktop browser, but Dave does everything on his phone. She knows this because Sam mentioned it in passing during Monday’s standup. Sam also remembers something from the Event Storm: Rachel’s comment about her dodgy satellite broadband, the twenty minutes to load a map. “If Rachel’s going to use this portal,” Sam says, “it needs to work on a connection that drops out halfway through a form submission.” Nobody had written that down. Sam just remembered. Jas redesigns the submission flow to save progress locally and retry when the connection comes back. It adds half a day of work and saves Rachel from losing her availability data every time her internet blinks.&lt;/p&gt;

&lt;p&gt;On Thursday morning, Lee asks a quiet question at the standup: “What happens if Tom is sick on a Thursday and you need to deploy?”&lt;/p&gt;

&lt;p&gt;Silence.&lt;/p&gt;

&lt;p&gt;Priya: “I’ve never deployed.”&lt;/p&gt;

&lt;p&gt;Tom writes a README that afternoon and walks Priya through the deploy script. By the end of sprint two, Priya has deployed twice. Their bus factor for deployments goes from one to two. It’s not a pipeline; it’s a shared script and a document. But it’s the difference between “one person can ship” and “two people can ship.”&lt;/p&gt;

&lt;p&gt;Later that day, Tom says something that surprises everyone.&lt;/p&gt;

&lt;p&gt;“I thought standups were a waste of time. I still think most of them are. I’ve been on teams where it was twenty minutes of people reading Jira tickets aloud. These aren’t that. Four minutes, and last week it saved Priya a day. I’m in.”&lt;/p&gt;

&lt;p&gt;Lee smiles but says nothing.&lt;/p&gt;

&lt;p&gt;The sprint review is smoother. The farm portal works on desktop and mobile. The landing page has comparison copy. Maya demonstrates the matching tool with a real UI. Sam has brought in 26 new subscribers through a combination of local Facebook groups and door-to-door conversations at the Margaret River farmers’ market.&lt;/p&gt;

&lt;p&gt;Subscriber count on the whiteboard: 68. Priya updates the chart. Thirty subscribers added across four weeks of sprinting. The curve is bending the correct way. Then she draws where Lee’s dotted line sits at this point in the six-week stretch, 108, and the relief thins. They’re forty short of the linear trajectory, with one sprint left to close that gap &lt;em&gt;and&lt;/em&gt; find another 132 subscribers on top. They always knew early growth would be slow and the curve would have to steepen at the end. Seeing it is different from knowing it.&lt;/p&gt;

&lt;p&gt;Tom looks at the chart. “We need to more than triple this in two weeks.”&lt;/p&gt;

&lt;p&gt;“I know,” Maya says.&lt;/p&gt;

&lt;h3 id=&quot;sprint-three-the-final-push&quot;&gt;Sprint three: the final push&lt;/h3&gt;

&lt;p&gt;Two weeks. One hundred and thirty-two subscribers to find. By sprint three, the rhythm is second nature (Monday morning planning and Example Mapping, daily standups, Friday demo and chart update) but nothing about the mood is routine. Maya’s been at the office until midnight on Sunday, going over the sprint plan with Lee and redrawing the assumptions behind the referral programme on the back of a receipt.&lt;/p&gt;

&lt;p&gt;Sprint three goal: Ship delivery logistics, pause-and-resume, and the referral programme. Hit 200.&lt;/p&gt;

&lt;p&gt;The story map for this sprint is the longest they’ve written. Eleven stories. Lee raises both eyebrows but doesn’t object; he can see what the team sees.&lt;/p&gt;

&lt;p&gt;Tom also starts feeding Example Map output into Claude during planning: “Break this story into implementation tasks and estimate the relative complexity of each.” The &lt;label for=&quot;sn-writing-sprint-planning-turning-sticky-notes-into-delivery-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-sprint-planning-turning-sticky-notes-into-delivery-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-sprint-planning-turning-sticky-notes-into-delivery-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-sprint-planning-turning-sticky-notes-into-delivery-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; comes back with reasonable task breakdowns that give the team a starting point for conversation. Jas uses it differently: she feeds in the Example Map cards and asks for draft acceptance criteria, then edits them. It saves fifteen minutes of typing per story. The pattern is the same as everywhere else: the LLM is an assistant, not a decision-maker.&lt;/p&gt;

&lt;p&gt;By Wednesday of week one, delivery logistics is working end to end. Three farms are submitting availability. Maya’s matching tool produces a packing list each Tuesday evening. The first deliveries go out on Thursday: real deliveries, not the pilot ones, through the software the team built. Pause-and-resume ships on Friday afternoon, quietly, because Mrs Patterson is going on holiday and needs it by Monday.&lt;/p&gt;

&lt;p&gt;Subscriber count on the whiteboard, end of week one: 96. Twenty-eight new in the first week of sprint three, the fastest growth yet, but Priya runs the maths and it isn’t enough. At that pace they’ll land at 154 by Friday of week two. Maya sees the number and says nothing for a full minute.&lt;/p&gt;

&lt;p&gt;The referral programme goes live on the Monday of week two. It’s a simple thing: if a subscriber refers a friend who signs up, both get $10 off next month’s box. Sam designed it, Jas built the flow, Tom wired it into the subscription system. By Tuesday afternoon, every new subscriber is bringing an average of 0.4 friends into the funnel. By Wednesday, the number is 0.7. Sam starts tracking referrals on a second whiteboard next to Lee’s chart: names, dates, which subscriber referred them. The board fills up faster than she can write.&lt;/p&gt;

&lt;p&gt;Priya updates the chart on Wednesday afternoon. Subscriber count on the whiteboard: 143. Still fifty-seven short of the target, with two and a half days to go. Maya looks at it for a long time. Then she says, quietly, “It’s going to be close.”&lt;/p&gt;

&lt;p&gt;On Thursday morning, Sam opens her laptop before her feet touch the floor and sees thirty-seven new sign-ups overnight. She refreshes, thinks she’s mis-read it, refreshes again. Thirty-seven. She calls Maya before she even stands up.&lt;/p&gt;

&lt;p&gt;Thursday rolls. The referral flywheel is spinning for itself now. Every one of those overnight sign-ups arrived with friends they’d already told about the box, and several of those friends sign up within hours of getting the $10-off code. Eight more sign-ups land during the morning school run. Five over lunch. Three in the afternoon slot. By the end of Thursday the count is 196. Sam writes it on the chart in pencil, because she’s afraid that if she uses marker she’ll jinx it.&lt;/p&gt;

&lt;p&gt;Friday morning, 8:47am: they cross 200. Priya is unlocking the office when her phone pings. Tom, who has been awake since 5am refreshing the dashboard on his phone, is already at the cafe downstairs with two coffees in a cardboard tray. “We got the hardest one,” he says, handing her a flat white. Priya laughs so hard she nearly drops the keys.&lt;/p&gt;

&lt;p&gt;The final sign-ups trickle in across Friday. A cluster of four late in the morning: someone’s book club. Two in the early afternoon: Dave’s neighbour and her daughter. Then a slow drip through the rest of the day, referrals chasing referrals, every ping of the dashboard another small cheer from wherever on the floor people are sitting. By 4pm Priya draws the final data point slowly, as if she can’t quite believe it.&lt;/p&gt;

&lt;p&gt;The subscriber count on the whiteboard: 214.&lt;/p&gt;

&lt;p&gt;For a second nobody moves. Then Sam lets out a sound that isn’t quite a word, clamps her hand over her mouth, and starts to cry. Tom says “no way” very quietly, to nobody, and then says it again, louder. Jas is already in Maya’s arms. Priya stands by the whiteboard, marker still in her hand, looking at the numbers. She’s the one who plotted every single Friday for twelve weeks. She knows what this curve looks like because she drew it. Lee is standing by the door with his hands in his pockets, smiling in the way he smiles when he’s trying not to cry himself.&lt;/p&gt;

&lt;p&gt;Maya looks at the wall: at the sticky notes from the first Event Storm, still laminated there, at the pink hotspots, at the chart with the jagged line climbing from 38 to 214 across three sprints. It’s a real number on a real whiteboard in a real office. Two hundred and fourteen people in Perth paid them money this week because they trusted a company that, twelve weeks ago, barely existed. Greenbox is real. Not a pitch deck. Not a spreadsheet. Not Maya’s idea. &lt;em&gt;A company.&lt;/em&gt; With customers. With a team. With software that ships boxes of produce to people’s doorsteps every Thursday.&lt;/p&gt;

&lt;p&gt;Tom, whose default is skepticism, walks up to the whiteboard and writes “214” in much bigger letters underneath Priya’s dot. Then he adds an exclamation mark. Then a second one.&lt;/p&gt;

&lt;p&gt;Someone orders pizza. Someone else goes down to the cafe below the office and comes back with a bottle of something that is technically champagne and practically just sparkling wine. Maya, who has been running on coffee and adrenaline for twelve weeks, takes a glass and sits on the floor with her back against the wall and laughs for the first time in about six days. Sam, who hasn’t stopped smiling, keeps pulling out her phone and looking at the subscriber dashboard and then putting it away and then pulling it out again like she can’t quite trust the number to stay there.&lt;/p&gt;

&lt;p&gt;At some point in the evening, Dave calls from Margaret River. Maya had emailed him the number an hour earlier. He says: “I don’t know what I expected when I first met you at the market, but it wasn’t this. Congratulations, kid. I told Rachel. She cried too.” Maya laughs and wipes her eyes and tells him the next box of his tomatoes is going out to a family in North Perth who specifically requested them after reading about Dave’s farm on the about page.&lt;/p&gt;

&lt;p&gt;Lee raises his glass at one point. “To the team that nearly broke itself in month one and put itself back together.” Everyone drinks. Nobody says anything for a while.&lt;/p&gt;

&lt;p&gt;They did it. Not comfortably. There was a rough patch at the start of sprint two when a payment bug knocked out twenty subscribers for a day. Sam fielded the angry emails. Maya personally called every affected subscriber to apologise. One of them said: “I’m switching to something else if this happens again.” Maya asked what else. “I don’t know yet. But there must be something.” There was also a three-day window in sprint three where referral growth stalled and Maya stayed up until midnight emailing every contact she had. But the sprint rhythm gave them visibility. They could see the problem coming, adjust, and respond, instead of discovering at week eleven that they were behind.&lt;/p&gt;

&lt;p&gt;Tom says something in the final retrospective that sticks with Lee: “In week one, I was shipping code faster than I ever had. But I had no idea if it was the correct code, or if we were going to make it. Now I’m shipping at about the same pace, but I know it’s the correct stuff and I can see that we’re on track. That feels completely different.” He pauses. “Week one was the wrong kind of fast.”&lt;/p&gt;

&lt;p&gt;The total ceremony overhead: about three hours per fortnight. For that investment, the team got shared visibility, early blocker detection, regular feedback from non-developers, and a clear picture of whether they’d hit the deadline. Compare that to the four weeks the team lost in month one, and it’s not even close.&lt;/p&gt;

&lt;h3 id=&quot;what-the-sprint-cant-tell-you&quot;&gt;What the sprint can’t tell you&lt;/h3&gt;

&lt;p&gt;Two hundred and fourteen subscribers. Three sprints. A rhythm that went from awkward silences to four-minute standups. A team that started as five people shipping code in different directions and ended as five people who know what they’re building, why, and whether they’re on pace.&lt;/p&gt;

&lt;p&gt;That’s a different company from the one that started twelve weeks ago.&lt;/p&gt;

&lt;p&gt;But the sprint cadence tells the team &lt;em&gt;what&lt;/em&gt; they’re building and &lt;em&gt;whether&lt;/em&gt; they’re on pace. It doesn’t tell them whether the code they’re shipping is &lt;em&gt;correct&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Tom has been writing code fast. With an LLM as a pair, he’s generating more code in a day than he used to write in a week. But speed creates a new problem. The code arrives quickly and looks correct, but bugs are slipping through: the kind that the Example Maps would have caught if anyone had checked the implementation against the green cards. Three subscribers hit a payment edge case in sprint three that was right there on a red card from the Example Mapping session. The team has concrete scenarios with context, actions, and outcomes. What they’re missing is the bridge between those cards on a table and verified, working software.&lt;/p&gt;

&lt;p&gt;Priya starts running through the Example Map cards one by one against the code. “This scenario works,” she says. “This one doesn’t.” She’s testing by hand. She’s catching bugs. And she’s spending two hours per story doing it. At eight stories per sprint, that’s two full days of manual checking every fortnight: a quarter of Priya’s capacity, spent reading cards and comparing them to screens. And it’s only going to get worse. The team is shipping faster every sprint. More stories means more cards means more checking. Priya can see the trajectory: by sprint six she’ll be spending half her time clicking through a browser instead of writing code.&lt;/p&gt;

&lt;p&gt;She didn’t move to Perth for this. She moved to Perth to build things.&lt;/p&gt;

&lt;p&gt;There has to be a better way.&lt;/p&gt;

&lt;p&gt;There is. It starts with turning those Example Map cards into &lt;a href=&quot;/writing/behaviour-driven-development-from-stories-to-working-software/&quot;&gt;working software&lt;/a&gt;.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>To LLMs… and Beyond!</title>
    <link href="/writing/to-llms-and-beyond/"/>
    <updated>2026-04-02T06:00:00+08:00</updated>
    <id>/writing/to-llms-and-beyond/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;You’ve heard of ChatGPT. Someone at work mentioned “diffusion models” and you nodded. A blog post told you to use a “multimodal” something. Your cousin sent you an AI-generated image of a cat riding a submarine and you wondered, vaguely, how that works. You’ve been meaning to look into all of this but every explanation assumes you already know the bit you don’t.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the field guide you needed six months ago.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;a href=&quot;/writing/how-llms-actually-work/&quot;&gt;previous post in this series&lt;/a&gt;, we opened up a &lt;label for=&quot;sn-writing-to-llms-and-beyond-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Large Language Model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; and looked at the machinery inside — tokens, &lt;label for=&quot;sn-writing-to-llms-and-beyond-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embeddings&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt;, &lt;label for=&quot;sn-writing-to-llms-and-beyond-attention&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-attention-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;attention&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-attention&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-attention-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Attention&lt;/span&gt;The mechanism inside a transformer that lets each token weigh how much every other token in the context matters to it.
&lt;/span&gt;, &lt;label for=&quot;sn-writing-to-llms-and-beyond-transformer&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-transformer-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;transformer&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-transformer&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-transformer-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Transformer&lt;/span&gt;The neural network architecture that underpins modern LLMs – stacks of self-attention layers that let every token look at every other token in the context.
&lt;/span&gt; blocks, the training pipeline. That post answered one question: how does an LLM actually work?&lt;/p&gt;

&lt;p&gt;This post answers the next one: what &lt;em&gt;else&lt;/em&gt; is out there?&lt;/p&gt;

&lt;p&gt;Because LLMs are one corner of a much larger field. There are models that generate images, models that generate video, models that produce music, models that reason step by step for minutes before answering, and models that combine several of these capabilities at once. The terminology is a mess. The marketing is worse. And if you’re trying to figure out what tool you actually need for a specific job, the landscape can feel impenetrable.&lt;/p&gt;

&lt;p&gt;Let’s fix that. We’ll start with a word that gets thrown around constantly and rarely defined.&lt;/p&gt;

&lt;h3 id=&quot;modality-types-of-information&quot;&gt;Modality: types of information&lt;/h3&gt;

&lt;p&gt;In AI, a modality is a type of input or output — a channel of information. The word comes from philosophy and cognitive science, where it refers to the senses: sight, hearing, touch. In AI, it’s been stretched to cover any distinct form of data.&lt;/p&gt;

&lt;p&gt;The main modalities you’ll encounter:&lt;/p&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Modality&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;What it is&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Example models&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Text&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Natural language, prose, dialogue&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Claude, GPT-4, Llama&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Programming languages — arguably text, but the rules are different enough to matter&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Claude, Codex, Code Llama&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Image&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Photographs, illustrations, diagrams, sprites&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;DALL-E, Stable Diffusion, Midjourney&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Audio&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Speech, music, sound effects&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Whisper (speech→text), Suno (text→music)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Video&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Moving images, often with audio&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Sora, Runway, Kling&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;3D&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Meshes, point clouds, scenes&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Point-E, NeRFs (emerging)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Structured data&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Tables, databases, graphs&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Various specialised models&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Embeddings&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Numerical representations that capture meaning — the hidden modality that powers search&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;text-embedding-3, Cohere Embed&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;A &lt;label for=&quot;sn-writing-to-llms-and-beyond-model&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-model-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;model&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-model&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-model-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Model&lt;/span&gt;A trained set of weights plus the architecture that makes them useful – the thing you load up and run inference against.
&lt;/span&gt; can be single-modality — text in, text out. Or it can be multimodal — accepting and producing multiple types. When someone says “multimodal model,” they mean a model that crosses these boundaries. GPT-4o takes text and images as input and produces text, images, and audio as output. Claude takes text and images as input and produces text. Gemini handles text, images, audio, and video.&lt;/p&gt;

&lt;p&gt;The direction matters. A model that takes text in and produces images out (DALL-E) is doing something fundamentally different from a model that takes images in and produces text out (image captioning). Both are “multimodal,” but the underlying machinery is very different.&lt;/p&gt;

&lt;p&gt;This brings us to the machinery itself.&lt;/p&gt;

&lt;h3 id=&quot;architectures-the-engine-designs&quot;&gt;Architectures: the engine designs&lt;/h3&gt;

&lt;p&gt;An architecture is the fundamental design of the neural network — the blueprint for how data flows through the model and how it learns. It’s like engine designs in cars: petrol, diesel, electric, hybrid. Different engineering, different trade-offs, different things they’re good at.&lt;/p&gt;

&lt;h4 id=&quot;transformers&quot;&gt;Transformers&lt;/h4&gt;

&lt;p&gt;If you read the &lt;a href=&quot;/writing/how-llms-actually-work/&quot;&gt;previous post&lt;/a&gt;, you already know this one. The transformer architecture, introduced in &lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;“Attention Is All You Need”&lt;/a&gt; (Vaswani et al., 2017), is the engine behind virtually every major text-generating AI. Claude, GPT-4, Llama, Gemini, Mistral — all transformers.&lt;/p&gt;

&lt;p&gt;The key innovation is the attention mechanism: instead of processing text sequentially (one word at a time, left to right), the transformer looks at the entire input at once and figures out which parts relate to which. This parallelism makes them fast to train and excellent at capturing long-range dependencies in text.&lt;/p&gt;

&lt;p&gt;Transformers aren’t limited to text. Vision Transformers (ViT, &lt;a href=&quot;https://arxiv.org/abs/2010.11929&quot;&gt;Dosovitskiy et al., 2021&lt;/a&gt;) apply the same architecture to images by splitting an image into patches and treating each patch like a token. The attention mechanism then figures out which patches relate to which — exactly the same principle, different input.&lt;/p&gt;

&lt;p&gt;The transformer has been remarkably dominant. But it has a known weakness: the attention mechanism scales quadratically with sequence length. Double the input, quadruple the compute. For very long inputs (millions of tokens), this becomes expensive. Which is part of why alternatives exist.&lt;/p&gt;

&lt;h4 id=&quot;diffusion-models&quot;&gt;Diffusion models&lt;/h4&gt;

&lt;p&gt;Diffusion models are the engine behind most modern image generation: Stable Diffusion, DALL-E 3, Midjourney, and Flux.&lt;/p&gt;

&lt;p&gt;The core idea is beautifully counterintuitive. During training, the model learns to reverse the process of adding noise to an image. You take a real image, gradually add random noise over many steps until it’s pure static, and train the model to predict what the image looked like one step earlier — slightly less noisy.&lt;/p&gt;

&lt;p&gt;At generation time, you start with pure random noise and ask the model to denoise it, step by step. Each step removes a little noise and adds a little structure. After enough steps (typically 20-50), you have a coherent image.&lt;/p&gt;

&lt;p&gt;The text &lt;label for=&quot;sn-writing-to-llms-and-beyond-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; enters the picture through conditioning. The model doesn’t just denoise randomly — it denoises &lt;em&gt;in a direction guided by a text description&lt;/em&gt;. The text “a cat riding a submarine in the style of Studio Ghibli” gets encoded into a numerical representation (usually by a text encoder like CLIP), and that representation steers every denoising step. The model has learned, from millions of image-caption pairs, which visual patterns correspond to which text descriptions.&lt;/p&gt;

&lt;p&gt;This is fundamentally different from how LLMs work. An LLM generates output one token at a time, left to right. A diffusion model generates the entire image at once, refining it in passes from noise to clarity. There’s no concept of “next pixel” the way there’s a “next token.”&lt;/p&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;LLM (transformer)&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Diffusion model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Generates&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;One token at a time&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Entire output at once, refined iteratively&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Training signal&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;Predict the next token&quot;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&quot;Remove the noise&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Output type&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Sequential (text, code)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Spatial (images, video frames)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Guided by&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;All previous tokens&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Text embedding + previous denoising step&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Fast per token, slow for long outputs&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Fixed number of steps regardless of complexity&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;The idea was first made practical by &lt;a href=&quot;https://arxiv.org/abs/2006.11239&quot;&gt;Ho et al. (2020)&lt;/a&gt;. The breakthrough that made it work for high-resolution images was latent diffusion (&lt;a href=&quot;https://arxiv.org/abs/2112.10752&quot;&gt;Rombach et al., 2022&lt;/a&gt;) — instead of denoising the full image pixel by pixel (which is absurdly expensive at high resolution), you first compress the image into a much smaller representation, do the denoising there, and then decompress the result. It’s the difference between sculpting a full-size statue and sculpting a maquette that gets scaled up. This is the approach behind Stable Diffusion.&lt;/p&gt;

&lt;h4 id=&quot;gans-generative-adversarial-networks&quot;&gt;GANs (Generative Adversarial Networks)&lt;/h4&gt;

&lt;p&gt;Before diffusion models, GANs were the dominant approach to image generation. Introduced by &lt;a href=&quot;https://arxiv.org/abs/1406.2661&quot;&gt;Goodfellow et al. (2014)&lt;/a&gt;, the idea is elegant: train two neural networks against each other.&lt;/p&gt;

&lt;p&gt;The generator creates fake images. The discriminator tries to tell real images from fake ones. The generator gets better at fooling the discriminator. The discriminator gets better at detecting fakes. They push each other to improve, like a counterfeiter and a detective in an arms race.&lt;/p&gt;

&lt;p&gt;GANs produced stunning results — &lt;a href=&quot;https://arxiv.org/abs/1812.04948&quot;&gt;StyleGAN&lt;/a&gt; (Karras et al., 2019) generated photorealistic faces that were indistinguishable from real photographs. But they were notoriously difficult to train. The two networks can fall out of balance (the generator collapses to producing one image, or the discriminator becomes unbeatable), and the training process is unstable compared to diffusion models.&lt;/p&gt;

&lt;p&gt;Diffusion models have largely replaced GANs for general-purpose image generation, but GANs remain useful in some niches — real-time applications where the single-pass generation is faster than iterative denoising, and super-resolution tasks where you’re enhancing an existing image rather than generating from scratch.&lt;/p&gt;

&lt;h4 id=&quot;state-space-models&quot;&gt;State-space models&lt;/h4&gt;

&lt;p&gt;Transformers aren’t the only game in town for text. State-space models (SSMs), most notably Mamba (&lt;a href=&quot;https://arxiv.org/abs/2312.00752&quot;&gt;Gu and Dao, 2023&lt;/a&gt;), are an alternative architecture that processes sequences without the quadratic attention cost.&lt;/p&gt;

&lt;p&gt;Instead of letting every token attend to every other token, SSMs maintain a compressed hidden state that evolves as each token is processed. Think of it as the difference between re-reading an entire book every time you want to recall something (attention) versus keeping a running set of notes that you update as you read (state-space). The notes are lossy — you can’t recall every detail — but updating them is fast and the cost scales linearly with sequence length, not quadratically.&lt;/p&gt;

&lt;p&gt;SSMs are still emerging. They show promising results on long sequences where the quadratic cost of attention is prohibitive, but transformers remain dominant for most tasks as of early 2026. The two approaches may converge — hybrid architectures that combine attention for local precision with state-space mechanisms for long-range efficiency are an active area of research.&lt;/p&gt;

&lt;h3 id=&quot;paradigms-patterns-built-on-top&quot;&gt;Paradigms: patterns built on top&lt;/h3&gt;

&lt;p&gt;Architectures are the engine. Paradigms are how you drive. These are patterns and techniques that sit on top of the fundamental architectures, often combining them in clever ways.&lt;/p&gt;

&lt;h4 id=&quot;reasoning-models&quot;&gt;Reasoning models&lt;/h4&gt;

&lt;p&gt;Standard LLMs generate text in a single pass — the model reads your prompt, then starts producing tokens immediately. Reasoning models add an explicit thinking phase before answering.&lt;/p&gt;

&lt;p&gt;OpenAI’s o1 and o3 models, and DeepSeek-R1, are the most prominent examples. When you ask a reasoning model a hard question, it generates a long internal chain of thought — sometimes thousands of tokens of deliberation — before producing the visible response. The model might consider multiple approaches, check its own reasoning, backtrack from dead ends, and work through intermediate steps.&lt;/p&gt;

&lt;p&gt;This isn’t just &lt;label for=&quot;sn-writing-to-llms-and-beyond-chain-of-thought&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-chain-of-thought-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;chain-of-thought&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-chain-of-thought&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-chain-of-thought-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Chain-of-thought&lt;/span&gt;Prompting the model to write out its intermediate reasoning before giving a final answer – which empirically makes hard problems get answered better.
&lt;/span&gt; prompting (which we covered in the &lt;a href=&quot;/writing/how-llms-actually-work/&quot;&gt;LLM post&lt;/a&gt;). Chain-of-thought prompting asks a standard model to show its working. Reasoning models are specifically trained — often using reinforcement learning — to use that thinking time productively. The training process rewards not just correct answers but effective reasoning strategies.&lt;/p&gt;

&lt;p&gt;The trade-off is straightforward: reasoning models are slower and more expensive, but substantially better at tasks that require genuine multi-step reasoning — mathematics, formal logic, complex code, and scientific analysis. For a simple question like “what’s the capital of France?”, a reasoning model is overkill. For “find the bug in this 500-line concurrent program,” the extra thinking time pays for itself.&lt;/p&gt;

&lt;h4 id=&quot;recursive-language-models-rlms&quot;&gt;Recursive Language Models (RLMs)&lt;/h4&gt;

&lt;p&gt;RLMs are a recent inference-time paradigm from MIT (&lt;a href=&quot;https://arxiv.org/abs/2512.24601&quot;&gt;Zhang, Kraska, and Khattab, 2026&lt;/a&gt;) that addresses one of the most stubborn limitations of LLMs: the context window.&lt;/p&gt;

&lt;p&gt;The insight is simple and surprisingly effective. Instead of cramming a massive prompt directly into the model’s context window, an RLM loads the prompt as a variable in a Python REPL and lets the model write code to examine, decompose, and process it. The model can peek at snippets, chunk the input, search through it, and call itself recursively on sub-sections.&lt;/p&gt;

&lt;p&gt;This means a model with a 272K token context window can effectively process inputs of 10 million tokens or more. The model never sees the whole input at once. Instead, it writes a program that strategically examines the parts it needs, delegates sub-questions to copies of itself, and assembles the results.&lt;/p&gt;

&lt;p&gt;It’s not a new architecture — the underlying model is still a standard transformer. It’s a scaffold, a way of using an existing model more effectively. But the results are striking: RLMs outperformed both the base model and existing long-context approaches (summarisation agents, &lt;label for=&quot;sn-writing-to-llms-and-beyond-rag&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-rag-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;retrieval-augmented generation&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-rag&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-rag-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RAG&lt;/span&gt;A pattern where you retrieve relevant documents at query time and stuff them into the prompt so the model can ground its answer on them.
&lt;/span&gt;) by large margins on four diverse benchmarks, while maintaining comparable cost.&lt;/p&gt;

&lt;p&gt;The pattern here is worth noting: some of the most impactful advances aren’t new architectures at all. They’re clever ways of using existing architectures differently.&lt;/p&gt;

&lt;h4 id=&quot;agents&quot;&gt;Agents&lt;/h4&gt;

&lt;p&gt;An &lt;label for=&quot;sn-writing-to-llms-and-beyond-ai-agent&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-ai-agent-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;agent&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-ai-agent&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-ai-agent-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Agent&lt;/span&gt;A system that wraps an LLM with tools, memory, and a loop, so it can take multi-step actions toward a goal rather than just answering one prompt.
&lt;/span&gt; is an AI system that can take actions in the world — not just generate text, but use tools, browse the web, execute code, call APIs, and make decisions about what to do next.&lt;/p&gt;

&lt;p&gt;The underlying model is typically an LLM, but instead of just producing a response, it produces a &lt;em&gt;plan&lt;/em&gt;: “I need to search for X, then read the result, then calculate Y, then write a file.” Each step generates a new prompt that includes the results of previous steps. To understand agents, you need a few pieces of vocabulary.&lt;/p&gt;

&lt;p&gt;Prompts are the instructions you give a model. You already know this — you type something, the model responds. But there’s a layer most people don’t see: the &lt;label for=&quot;sn-writing-to-llms-and-beyond-system-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-system-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;system prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-system-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-system-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;System prompt&lt;/span&gt;The instruction block that frames the model’s behaviour for a session, separate from the user’s messages.
&lt;/span&gt;. Before your message ever reaches the model, the application wraps it with hidden instructions that shape behaviour. “You are a helpful assistant. Answer concisely. Do not produce harmful content.” That’s a system prompt. When ChatGPT refuses to help you build a bomb, that’s not some deep moral reasoning — it’s following instructions in a system prompt, reinforced by RLHF training. When Claude writes code in a particular style, that’s partly system prompt too. The system prompt is the invisible hand that makes the same underlying model behave differently in different products.&lt;/p&gt;

&lt;p&gt;Tools are capabilities granted to an agent — things it can &lt;em&gt;do&lt;/em&gt; beyond generating text. A bare LLM can only produce words. Give it tools and it can read files, search the web, execute code, query databases, send messages, or call external APIs. The model doesn’t inherently have these abilities. They’re defined by the developer who builds the agent, and the model learns to invoke them by generating structured requests (“I want to call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read_file&lt;/code&gt; tool with the path &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/src/main.py&lt;/code&gt;”). The set of tools available to an agent defines what it can accomplish — and its limits.&lt;/p&gt;

&lt;p&gt;Sub-agents extend this further. A complex task might be too large or too varied for a single agent to handle efficiently. Instead, the agent can spawn sub-agents — smaller, focused agents that handle specific sub-tasks. An agent reviewing a large codebase might spawn one sub-agent to explore the directory structure, another to search for specific patterns, and a third to read and summarise relevant files — all working in parallel. Each sub-agent has its own context, its own tools, and returns its results to the parent. It’s delegation, the same way a manager breaks work into tasks for a team.&lt;/p&gt;

&lt;p&gt;Skills are pre-packaged workflows — reusable recipes that an agent can invoke rather than figuring out from scratch. Instead of reasoning through the twelve steps of “create a git commit with the correct message format,” an agent might invoke a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit&lt;/code&gt; skill that encapsulates that workflow. Skills trade flexibility for reliability: the agent doesn’t need to reinvent common procedures every time.&lt;/p&gt;

&lt;p&gt;Agents blur the line between “AI as a tool” and “AI as a collaborator.” A tool responds to a single prompt. An agent pursues a goal across multiple steps, adapting its approach based on what it discovers along the way.&lt;/p&gt;

&lt;h4 id=&quot;rag-retrieval-augmented-generation&quot;&gt;RAG (Retrieval-Augmented Generation)&lt;/h4&gt;

&lt;p&gt;RAG is a pattern that addresses a fundamental limitation: the model’s knowledge is frozen at training time. If you ask about something that happened after the training cutoff, or about your company’s internal documentation, the model can only hallucinate.&lt;/p&gt;

&lt;p&gt;RAG works by retrieving relevant documents before generating a response. Your question gets converted into an embedding (a numerical representation), that embedding is compared against a database of document embeddings, the most relevant documents are pulled in, and those documents are included in the prompt alongside your question. The model then generates a response grounded in the retrieved text, rather than relying solely on what it learned during training.&lt;/p&gt;

&lt;p&gt;This is how most enterprise AI deployments work in practice. The model might be Claude or GPT-4, but the knowledge comes from your documentation, your codebase, your internal wiki. RAG lets you get domain-specific answers from a general-purpose model without &lt;label for=&quot;sn-writing-to-llms-and-beyond-fine-tuning&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-fine-tuning-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;fine-tuning&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-fine-tuning&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-fine-tuning-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Fine-tuning&lt;/span&gt;Continuing to train an already-trained model on a smaller dataset to adapt its behaviour.
&lt;/span&gt; it.&lt;/p&gt;

&lt;h3 id=&quot;the-models-what-you-can-actually-use&quot;&gt;The models: what you can actually use&lt;/h3&gt;

&lt;p&gt;All of the above is theory. Here’s the practical bit: what models exist, who makes them, and what can you do with them?&lt;/p&gt;

&lt;h4 id=&quot;gpt-is-not-a-generic-term&quot;&gt;GPT is not a generic term&lt;/h4&gt;

&lt;p&gt;Let’s start with the biggest source of confusion. GPT stands for Generative Pre-trained Transformer. It’s the name of OpenAI’s model family — GPT-3, GPT-4, GPT-4o, GPT-5. It is not a generic term for AI models, despite being used that way in roughly half of all conversations about AI.&lt;/p&gt;

&lt;p&gt;Calling all AI models “GPTs” is like calling all vacuum cleaners “Hoovers” or all search engines “Google.” Understandable, but imprecise. When someone says “we should use a GPT for this,” they might mean “we should use an LLM” — or they might specifically mean OpenAI’s product. It’s worth asking.&lt;/p&gt;

&lt;h4 id=&quot;the-major-llm-families&quot;&gt;The major LLM families&lt;/h4&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Model family&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Made by&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Open / closed&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Notable for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;GPT&lt;/strong&gt; (GPT-4o, o1, o3, GPT-5)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;OpenAI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Closed&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;First mover, reasoning models (o-series), broad multimodal support&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Claude&lt;/strong&gt; (Haiku, Sonnet, Opus)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Anthropic&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Closed&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Long context (1M tokens), strong at code and structured reasoning, Constitutional AI safety approach&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Gemini&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Google DeepMind&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Closed&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Natively multimodal (text, image, audio, video), integrated with Google services&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Llama&lt;/strong&gt; (Llama 3, 4)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Meta&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open-weight&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Largest open model ecosystem, strong community, commercially usable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Mistral / Mixtral&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Mistral AI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open-weight&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;European, efficient MoE architecture, strong multilingual&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Qwen&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Alibaba&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open-weight&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Strong multilingual (especially CJK), good code models, range of sizes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;DeepSeek&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;DeepSeek AI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open-weight&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Reasoning focus (DeepSeek-R1), competitive with frontier closed models at lower cost&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Grok&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;xAI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Partially open&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Integrated with X (Twitter) data, less filtered&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h4 id=&quot;open-weight-vs-closed-why-it-matters&quot;&gt;Open-weight vs closed: why it matters&lt;/h4&gt;

&lt;p&gt;This distinction is one of the most important practical decisions you’ll make.&lt;/p&gt;

&lt;p&gt;Closed models (GPT, Claude, Gemini) are accessible only through an API. You send your prompt to someone else’s servers and get a response back. You can’t see the model’s weights, can’t run it on your own hardware, and can’t modify it. The provider controls the model’s behaviour, pricing, and availability.&lt;/p&gt;

&lt;p&gt;Open-weight models (Llama, Mistral, Qwen, DeepSeek) publish their model weights. You can download them, run them on your own hardware, fine-tune them for your specific use case, and inspect them. “Open-weight” rather than “open-source” because many of these models have restrictive licences — you can use the weights but the training code, data, and full methodology are often proprietary.&lt;/p&gt;

&lt;p&gt;When does this matter?&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fine-tuning: If you want to train a model on your own data (say, a dataset of space game sprites), you need open weights. You cannot fine-tune GPT-4 or Claude from scratch. OpenAI and others offer limited fine-tuning APIs, but the level of customisation is constrained.&lt;/li&gt;
  &lt;li&gt;Privacy: If your data can’t leave your infrastructure (medical, legal, financial), you need a model you can run locally.&lt;/li&gt;
  &lt;li&gt;Cost at scale: API calls add up. If you’re making millions of inference calls, running your own model on your own GPUs can be cheaper — though the upfront hardware cost is significant.&lt;/li&gt;
  &lt;li&gt;Control: Closed models can change behaviour between versions, add or remove capabilities, or adjust content policies in ways that break your workflow. Open-weight models are a snapshot — the version you downloaded today will behave the same way tomorrow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most individuals and small teams experimenting with AI, the closed model APIs are the pragmatic starting point. They’re the most capable, the easiest to use, and the per-query cost is manageable at small scale. Open-weight models become compelling when you need customisation, privacy, or cost control at volume.&lt;/p&gt;

&lt;h4 id=&quot;image-generation-models&quot;&gt;Image generation models&lt;/h4&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Model&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Made by&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Architecture&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Open / closed&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Notable for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;DALL-E 3&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;OpenAI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Diffusion&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Closed&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Integrated with ChatGPT, good prompt adherence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Midjourney&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Midjourney&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Diffusion (proprietary)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Closed&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Aesthetically striking defaults, strong at artistic styles&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Stable Diffusion / SDXL&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Stability AI&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Latent diffusion&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open-weight&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Enormous community, fine-tunable, runs locally&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Flux&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Black Forest Labs&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Flow matching&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Open-weight&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Founded by original Stable Diffusion researchers, strong prompt adherence, efficient&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;&lt;strong&gt;Imagen&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Google DeepMind&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Diffusion&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Closed&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Integrated with Google products&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;The open-weight image models — particularly Stable Diffusion and Flux — have spawned an enormous ecosystem of community-trained variants, style adaptations, and fine-tuning techniques. This is where &lt;label for=&quot;sn-writing-to-llms-and-beyond-lora&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-lora-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LoRA&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-lora&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-lora-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LoRA&lt;/span&gt;A fine-tuning technique that trains a small low-rank matrix on top of the frozen base model, instead of updating every parameter.
&lt;/span&gt; (Low-Rank Adaptation) and Dreambooth come in: techniques for teaching an existing model a new style or concept with relatively little data and compute. Want a model that generates pixel art sprites in a specific style? Fine-tune Stable Diffusion or Flux with LoRA on a few hundred examples. We’ll dig deeper into this in a future post.&lt;/p&gt;

&lt;h4 id=&quot;video-audio-and-beyond&quot;&gt;Video, audio, and beyond&lt;/h4&gt;

&lt;p&gt;The landscape for non-text, non-image modalities is moving fast but less mature:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Video generation: Sora (OpenAI), Runway Gen-3, Kling (Kuaishou), Veo (Google). These typically extend diffusion models to generate sequences of frames. Quality has improved dramatically but consistency across long videos (characters changing appearance, physics breaking) remains challenging.&lt;/li&gt;
  &lt;li&gt;Music and audio: Suno and Udio generate full songs from text descriptions. Whisper (OpenAI) is the standard for speech-to-text. Text-to-speech models (ElevenLabs, XTTS) produce increasingly natural-sounding voices.&lt;/li&gt;
  &lt;li&gt;3D generation: Still early. Point-E (OpenAI), various NeRF-based approaches. Generating 3D assets from text or images is an active research area but not yet reliable enough for production use in most cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;mixture-of-experts-an-architecture-trick-worth-knowing&quot;&gt;Mixture of Experts: an architecture trick worth knowing&lt;/h3&gt;

&lt;p&gt;You’ll encounter the term Mixture of Experts (MoE) and it’s worth understanding because it explains how some models can be very large without being very expensive to run.&lt;/p&gt;

&lt;p&gt;A standard transformer activates all of its parameters for every token. A 70-billion-parameter model does 70 billion parameters’ worth of computation for every single token it processes.&lt;/p&gt;

&lt;p&gt;A Mixture of Experts model has many more total parameters, but only activates a subset of them for each token. The model contains multiple “expert” sub-networks, and a learned routing mechanism decides which experts to use for each token. Mixtral 8x7B, for example, has 8 expert networks of 7 billion parameters each (about 47 billion total), but only activates 2 experts per token — so the effective compute per token is closer to a 14-billion-parameter model, while having access to a much larger knowledge base.&lt;/p&gt;

&lt;p&gt;This is how some models can be “bigger” without being proportionally slower or more expensive. The total parameter count (which gets the headlines) is much larger than the active parameter count per token (which determines the actual cost).&lt;/p&gt;

&lt;h3 id=&quot;embeddings-the-hidden-infrastructure&quot;&gt;Embeddings: the hidden infrastructure&lt;/h3&gt;

&lt;p&gt;Embeddings deserve special mention because they’re everywhere and rarely explained.&lt;/p&gt;

&lt;p&gt;An embedding is a &lt;label for=&quot;sn-writing-to-llms-and-beyond-vector&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-to-llms-and-beyond-vector-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;vector&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-to-llms-and-beyond-vector&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-to-llms-and-beyond-vector-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Vector&lt;/span&gt;An ordered list of numbers – in AI usage, almost always an embedding – and by extension the databases that index them for nearest-neighbour search.
&lt;/span&gt; that represents the meaning of a piece of text (or an image, or an audio clip) in a high-dimensional space that captures semantic similarity. Two texts that mean similar things will have similar embeddings, even if they use completely different words.&lt;/p&gt;

&lt;p&gt;“The cat sat on the mat” and “A feline rested on the rug” would have very similar embeddings. “The stock market crashed” would have a very different one.&lt;/p&gt;

&lt;p&gt;This matters because embeddings are the glue behind:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Semantic search: Instead of keyword matching (“does this document contain the word ‘cat’?”), you compare embeddings (“is this document about a similar concept?”).&lt;/li&gt;
  &lt;li&gt;RAG: The retrieval step in retrieval-augmented generation uses embeddings to find relevant documents.&lt;/li&gt;
  &lt;li&gt;Clustering and classification: Group similar items together without hand-written rules.&lt;/li&gt;
  &lt;li&gt;Recommendation systems: “You liked X, here are similar things.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Embedding models are typically smaller, faster, and cheaper than generative models. They don’t produce text — they produce vectors. OpenAI’s text-embedding-3, Cohere’s Embed, and various open-source options (e5, GTE, BGE) are the main choices.&lt;/p&gt;

&lt;h3 id=&quot;making-sense-of-it-all-a-decision-framework&quot;&gt;Making sense of it all: a decision framework&lt;/h3&gt;

&lt;p&gt;If you’ve read this far, you have the vocabulary. Now let’s make it practical. You have a task. Which model type do you need?&lt;/p&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;I want to…&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;You need&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Start here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Write or edit text, summarise documents, answer questions&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An LLM&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Claude or GPT-4o via API&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Solve hard maths, logic, or coding problems&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A reasoning model&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Claude (extended thinking), o3, DeepSeek-R1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Generate images from text descriptions&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A diffusion model&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Midjourney (quality), Stable Diffusion / Flux (open, fine-tunable)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Generate images in a &lt;em&gt;specific style&lt;/em&gt;&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A fine-tuned diffusion model&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Stable Diffusion or Flux + LoRA fine-tuning&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Generate video&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A video generation model&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Sora, Runway, Kling&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Transcribe speech to text&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A speech recognition model&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Whisper&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Generate music&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;A music generation model&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Suno, Udio&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Search my own documents using meaning, not keywords&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An embedding model + vector database&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;text-embedding-3 + Pinecone/Chroma/pgvector&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Build an AI that uses tools, browses the web, writes code&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An agent framework around an LLM&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Claude Code, LangChain, or build your own&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Answer questions using my company&apos;s internal knowledge&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;RAG (embedding model + LLM)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Embed your docs, retrieve relevant ones, pass to Claude/GPT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Process inputs far beyond any model&apos;s context window&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An RLM scaffold or chunking strategy&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;RLM framework, or manual chunking with an LLM&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Run AI locally, on my own hardware, with full privacy&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;An open-weight model&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Llama or Mistral via Ollama&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-pace-of-change&quot;&gt;The pace of change&lt;/h3&gt;

&lt;p&gt;One thing this post can’t give you is a stable picture. It won’t last.&lt;/p&gt;

&lt;p&gt;The landscape described here is accurate as of mid-2026. Six months ago, some of these models didn’t exist. Six months from now, some of them will have been superseded. The pace is genuinely unprecedented in software engineering — not just incremental improvements, but new categories of capability appearing every few months.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;will&lt;/em&gt; last is the framework. Modalities, architectures, paradigms, and models. New things will appear, but they’ll slot into this structure. A new model will operate on specific modalities, use a specific architecture (or a hybrid), employ specific paradigms, and be open or closed. If you understand the categories, you can evaluate new developments without starting from scratch every time.&lt;/p&gt;

&lt;h3 id=&quot;where-to-from-here&quot;&gt;Where to from here?&lt;/h3&gt;

&lt;p&gt;This post gave you the map. Future posts in this series will zoom into specific squares on it — picking a real problem, choosing the correct model type, and walking through the process end to end, including what it actually costs.&lt;/p&gt;

&lt;p&gt;Because the real test of understanding a landscape isn’t being able to name everything in it. It’s being able to pick the correct path through it for where you’re trying to go.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Example Mapping: Making Stories Concrete</title>
    <link href="/writing/example-mapping-making-stories-concrete/"/>
    <updated>2026-03-31T06:00:00+08:00</updated>
    <id>/writing/example-mapping-making-stories-concrete/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/from-chaos-to-clarity/&quot;&gt;From Chaos to Clarity&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;The Greenbox team has done good work. They &lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Stormed&lt;/a&gt; and got the whole domain out of Maya’s head. The hotspots on the wall made it clear: subscriptions are the critical path, nothing else works without them.&lt;/p&gt;

&lt;p&gt;Now they need to build something. And the first story on the board is: “Subscribe to a produce box.”&lt;/p&gt;

&lt;p&gt;Sounds clear enough, right? That’s what they thought four weeks ago too, and it didn’t go well.&lt;/p&gt;

&lt;p&gt;The story is too vague to build from. What does “subscribe” actually mean? What has to happen? What could go wrong? What does the customer see? Tom could start coding right now, but he’d be guessing, again, and the team knows where that leads.&lt;/p&gt;

&lt;p&gt;They need a way to turn that vague story into something concrete before anyone opens an IDE.&lt;/p&gt;

&lt;h3 id=&quot;what-is-example-mapping&quot;&gt;What is Example Mapping?&lt;/h3&gt;

&lt;p&gt;Example Mapping is a structured conversation technique created by Matt Wynne. The idea is simple: get a small group together for a short, focused session, take a single user story, and break it apart until everyone agrees on what “done” looks like. What are the rules? What are the concrete examples? What can’t we answer yet?&lt;/p&gt;

&lt;p&gt;By the end of the session, you know one of three things: the story is well-understood and ready to build, the story is too big and needs splitting, or there are too many unknowns and it needs more research first. All three are useful outcomes. The worst thing you can do with a vague story is ship it unexplored, with unknown assumptions and no validation. You may choose to build part of it to learn what’s missing, then revisit and possibly rewrite the story before you commit to the delivery.&lt;/p&gt;

&lt;p&gt;The technique uses four colours of index card (or sticky note, or virtual equivalent) to keep the conversation structured.&lt;/p&gt;

&lt;h3 id=&quot;four-colours-of-card&quot;&gt;Four colours of card&lt;/h3&gt;

&lt;p&gt;Yellow is the story. One card. The thing you’re discussing.&lt;/p&gt;

&lt;p&gt;Blue is for rules. These are the business rules, constraints, and acceptance criteria that govern how the story works. Each rule gets its own card.&lt;/p&gt;

&lt;p&gt;Green is for examples. Concrete, specific instances that illustrate a rule. “If X happens, then Y.” These are the things that tell you what “done” looks like.&lt;/p&gt;

&lt;p&gt;Red is for questions. Anything you can’t answer in the room. Unknowns, disagreements, things that need research or a decision from someone who isn’t here.&lt;/p&gt;

&lt;p&gt;That’s it. Four colours, four purposes.&lt;/p&gt;

&lt;h3 id=&quot;how-to-run-one&quot;&gt;How to run one&lt;/h3&gt;

&lt;p&gt;The format is deliberately tight:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Keep it short. Twenty-five minutes. Long enough to explore a story properly, short enough to stay focused. Set a timer. If you haven’t finished, the story is too big or too unclear. That’s useful information.&lt;/li&gt;
  &lt;li&gt;Small group. Someone who understands the business, someone who’ll build it, someone who’ll challenge the assumptions. Three to five people is ideal.&lt;/li&gt;
  &lt;li&gt;One story at a time. Don’t try to batch these. One story, one session.&lt;/li&gt;
  &lt;li&gt;Write as you go. Someone states a rule, write it on a blue card. Someone gives an example, write it on a green card under that rule. Someone asks a question nobody can answer, write it on a red card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conversation flows naturally. Someone proposes a rule. Someone else challenges it with an example. Edge cases surface. Assumptions get exposed. The map grows organically.&lt;/p&gt;

&lt;h3 id=&quot;the-first-session-subscribe-to-a-produce-box&quot;&gt;The first session: “Subscribe to a produce box”&lt;/h3&gt;

&lt;p&gt;The Greenbox team gathers round a table. Maya, Tom, Priya, Jas, and Sam. Lee facilitates. Twenty-five minutes on the clock.&lt;/p&gt;

&lt;p&gt;Lee places a yellow card in the middle of the table and writes on it: Subscribe to a produce box.&lt;/p&gt;

&lt;p&gt;“Don’t try to define it,” Lee says. “Start with a concrete scenario. A real person doing a real thing. Tell me about a real person subscribing to a produce box.”&lt;/p&gt;

&lt;h4 id=&quot;starting-with-examples&quot;&gt;Starting with examples&lt;/h4&gt;

&lt;p&gt;Jas goes first: “Someone visits the site, picks a box, enters their card details, and they’re subscribed.”&lt;/p&gt;

&lt;p&gt;Lee pushes back gently. “Who? Which box? What price? What happens so they know they’re subscribed? The more concrete the example, the more useful it is. Abstract examples hide assumptions.”&lt;/p&gt;

&lt;p&gt;Jas tries again: “OK. Sarah visits the site, picks a small box at $25 a week, enters her Visa ending in 4242, and gets a confirmation with a delivery date of Thursday 2nd April.”&lt;/p&gt;

&lt;p&gt;Lee writes it on a green card: &lt;em&gt;Sarah chooses small box ($25/week), pays with Visa 4242 → subscription confirmed, first delivery Thursday 2 April.&lt;/em&gt; “See the difference? The first version could mean almost anything. Everyone in the room would picture something slightly different. This one leaves much less room for ambiguity, and ambiguity is where assumptions hide, and assumptions are where the bugs, the waste, and the rework come from.”&lt;/p&gt;

&lt;p&gt;“Give me another one. What else could happen?”&lt;/p&gt;

&lt;p&gt;Tom: “The card gets declined. Say Sarah enters an expired card.”&lt;/p&gt;

&lt;p&gt;Green card: &lt;em&gt;Sarah tries to subscribe with expired Visa → no subscription, asked to retry with a different card.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;“What happens then?” Lee asks. “Does she lose her box choice? Start over from scratch?”&lt;/p&gt;

&lt;p&gt;Maya: “No, she just re-enters payment details. The box choice stays.”&lt;/p&gt;

&lt;p&gt;Lee writes that detail on the green card. “Good, that’s exactly the kind of detail that would have been a surprise in code review if nobody asked.”&lt;/p&gt;

&lt;p&gt;Maya: “We deliver on Thursdays. If someone subscribes on Monday, they should get a box this Thursday. If they subscribe on Friday, it’s next Thursday.”&lt;/p&gt;

&lt;p&gt;Jas: “Should we ask about dietary preferences when they subscribe? Allergies, things they don’t want?”&lt;/p&gt;

&lt;p&gt;Maya nods. “Mrs Patterson hates beetroot. We should probably –”&lt;/p&gt;

&lt;p&gt;Lee reaches for a red card. “That’s worth solving, but is it part of subscribing, or is it its own thing?” He writes: Dietary preferences and allergies during subscription? and moves it to the parked area. “We’ll come back to it. For now, let’s finish the shape of this one.”&lt;/p&gt;

&lt;p&gt;Lee pushes for dates: “Which Monday? Which Friday?”&lt;/p&gt;

&lt;p&gt;Maya: “If Sarah subscribes on Monday 30th March, she gets a box Thursday 2nd April. If she subscribes on Friday 3rd April, she gets a box Thursday 9th April.”&lt;/p&gt;

&lt;p&gt;Two more green cards:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Sarah subscribes Monday 30 March → first delivery Thursday 2 April&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Sarah subscribes Friday 3 April → first delivery Thursday 9 April&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;context-action-outcome&quot;&gt;Context, action, outcome&lt;/h4&gt;

&lt;p&gt;Lee looks at the cards on the table. “Every solid example has three parts: the context, what’s true before anything happens, the action, what someone does, and the outcome, what should be true afterwards.”&lt;/p&gt;

&lt;p&gt;He picks up the delivery date card. “&lt;em&gt;Sarah subscribes Monday 30 March, first delivery Thursday 2 April.&lt;/em&gt; What’s the context?”&lt;/p&gt;

&lt;p&gt;Tom: “Delivery day is Thursday.”&lt;/p&gt;

&lt;p&gt;Maya: “And the minimum lead time is three days.”&lt;/p&gt;

&lt;p&gt;Priya: “And there’s no public holiday that week.”&lt;/p&gt;

&lt;p&gt;“Right. None of that is on the card.” He rewrites it:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Context: delivery day is Thursday, minimum lead time is 3 days, no public holiday this week. Sarah subscribes Monday 30 March. → First delivery Thursday 2 April.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;“Now it’s self-contained. Anyone can pick up this card and understand not just &lt;em&gt;what&lt;/em&gt; happens but &lt;em&gt;why&lt;/em&gt;. And Priya’s point about public holidays, that’s on the card now. If someone reads this example in two weeks, they won’t have to guess whether we considered holidays. We did. It’s right there.”&lt;/p&gt;

&lt;p&gt;Priya starts rewriting some of the earlier cards without being asked. This is Priya at her best, she sees structure where others see conversation, and she can’t leave a sloppy card on the table. The payment one becomes: &lt;em&gt;Context: Sarah has selected a small box ($25/week). She enters an expired Visa. → No subscription created, asked to retry. Box choice is preserved.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not every example needs three paragraphs of context. But the discipline of asking “what’s the context?” catches the assumptions that aren’t obvious, and those are the ones that cause problems in production.&lt;/p&gt;

&lt;p&gt;“What about Wednesday?” Tom asks. “If someone subscribes at 11pm on Wednesday, do they make the cutoff? And whose 11pm, ours or the customer’s?”&lt;/p&gt;

&lt;p&gt;Maya hesitates on the cutoff. “I think so… but the farms need to have confirmed supply by then.” The timezone question she can answer: “We’re Perth only. Everything is AWST.”&lt;/p&gt;

&lt;p&gt;“For now,” Tom says.&lt;/p&gt;

&lt;p&gt;“For now,” Maya agrees. “When we hit Melbourne we’ll need to revisit. They’re on a different timezone and they have daylight saving. Perth doesn’t.”&lt;/p&gt;

&lt;p&gt;Lee writes a blue card: All times are AWST (Perth). Then a red card: Exact cutoff time for same-week delivery? He places the red card off to the side.&lt;/p&gt;

&lt;p&gt;“Red cards are good. They’re unknowns we’ve caught before they became expensive surprises.”&lt;/p&gt;

&lt;h4 id=&quot;questions-and-assumptions&quot;&gt;Questions and assumptions&lt;/h4&gt;

&lt;p&gt;Sam asks: “Can someone have two subscriptions? Like a small box to their place and a large one to their mum’s house?”&lt;/p&gt;

&lt;p&gt;The room goes quiet. Maya hadn’t considered it.&lt;/p&gt;

&lt;p&gt;Lee writes a red card: Multiple subscriptions per customer? Then he asks, “Is that something we need for the first version?”&lt;/p&gt;

&lt;p&gt;Maya: “No. Definitely not for version one.”&lt;/p&gt;

&lt;p&gt;“Good. Park it.” He moves the red card to a separate area of the table. “Anything that isn’t part of &lt;em&gt;this&lt;/em&gt; story goes over here. We’re not losing it, we’re recognising it belongs somewhere else.”&lt;/p&gt;

&lt;p&gt;Jas: “What about cancellation? Can they cancel any time?”&lt;/p&gt;

&lt;p&gt;Another red card: What’s the cancellation policy? Parked.&lt;/p&gt;

&lt;p&gt;“What about 3D Secure?” Priya asks. “Some cards need that extra authentication step.”&lt;/p&gt;

&lt;p&gt;Red card: How do we handle 3D Secure? This one stays with the story, it’s a technical detail that affects the subscription flow directly. Tom volunteers to research it.&lt;/p&gt;

&lt;h4 id=&quot;generalising-to-rules&quot;&gt;Generalising to rules&lt;/h4&gt;

&lt;p&gt;“OK,” Lee says. “We’ve got a good set of examples and questions. Now let’s look at what they have in common. If you look across several examples, you’ll start to see patterns, things that are always true, constraints that apply every time. Those patterns are rules. A rule is a general statement that a set of examples all obey. ‘Payment must succeed before a subscription is created’, that’s a rule. Every example we’ve written either follows it or tests what happens when it breaks.”&lt;/p&gt;

&lt;p&gt;The team looks at the green cards spread across the table.&lt;/p&gt;

&lt;p&gt;Maya sees it first: “There’s a box size choice. Small or large. That’s it for now.”&lt;/p&gt;

&lt;p&gt;Blue card: Customer must choose a box size. He arranges the size-related green cards underneath it.&lt;/p&gt;

&lt;p&gt;“Does the rule spark new examples? What could go wrong with box size selection?”&lt;/p&gt;

&lt;p&gt;Priya: “What if they don’t choose? What if they hit ‘subscribe’ without selecting a size?”&lt;/p&gt;

&lt;p&gt;Green card: &lt;em&gt;Sarah clicks subscribe without choosing a size → error, asked to choose.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tom: “And can they change their mind later? Switch from small to large?”&lt;/p&gt;

&lt;p&gt;Maya: “Yes, but not mid-week. It takes effect from the next delivery.”&lt;/p&gt;

&lt;p&gt;Green card: &lt;em&gt;Sarah switches from small ($25) to large ($45) on Monday → change takes effect Thursday.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lee nods. “The rule generates new examples, and the examples constrain the rule. It’s not just ‘choose a size’, it’s ‘must choose before subscribing, and can change with notice.’”&lt;/p&gt;

&lt;p&gt;Tom: “Payment has to work too. No valid payment, no subscription.”&lt;/p&gt;

&lt;p&gt;Blue card: Payment must succeed before subscription is created.&lt;/p&gt;

&lt;p&gt;“What else can go wrong with payment?”&lt;/p&gt;

&lt;p&gt;Sam: “What about when the weekly charge fails three weeks in? Card expired, insufficient funds?”&lt;/p&gt;

&lt;p&gt;Maya: “First failed charge, we retry after 24 hours. Second failure, we email them. Third, we pause the subscription.”&lt;/p&gt;

&lt;p&gt;Three new green cards:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Weekly charge fails once → retry after 24 hours&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Two failures → email customer to update payment&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Three failures → subscription paused automatically&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tom whistles. “That’s a lot more than ‘payment must succeed.’” Something he assumed would be straightforward, payment works or it doesn’t, just turned into a state machine with five transitions. Twenty-five minutes ago, he would have built it wrong.&lt;/p&gt;

&lt;p&gt;Jas: “And they need to know when their first box arrives.”&lt;/p&gt;

&lt;p&gt;Blue card: Customer sees their first delivery date after subscribing.&lt;/p&gt;

&lt;p&gt;Sam: “Public holidays. What if Thursday is a public holiday?”&lt;/p&gt;

&lt;p&gt;Maya: “We’d deliver Wednesday instead. Or Friday. Depends on the courier.”&lt;/p&gt;

&lt;p&gt;Red card: How do public holidays affect delivery dates?&lt;/p&gt;

&lt;p&gt;“Notice what happened,” Lee says. “We started with examples, and the rules emerged naturally. Then the rules generated &lt;em&gt;more&lt;/em&gt; examples, and those examples tightened the rules. If you start with rules, you tend to stay abstract. If you start with examples, you stay grounded.”&lt;/p&gt;

&lt;h4 id=&quot;the-map-so-far&quot;&gt;The map so far&lt;/h4&gt;

&lt;p&gt;The timer hasn’t gone off yet, but the team feels like they’ve covered the core shape. Here’s what the table looks like:&lt;/p&gt;

&lt;div style=&quot;border: 2px solid var(--color-rule); border-radius: 4px; padding: var(--space-md); margin: var(--space-md) 0;&quot;&gt;
  &lt;div style=&quot;background: rgba(201,168,0,0.10); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-sm); font-weight: bold; margin-bottom: var(--space-sm);&quot;&gt;Subscribe to a produce box&lt;/div&gt;
  &lt;!-- Rule 1 --&gt;
  &lt;div style=&quot;padding-left: var(--space-md); border-left: 3px solid rgba(51,153,255,0.4); margin-bottom: var(--space-sm);&quot;&gt;
    &lt;div style=&quot;background: rgba(51,153,255,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); font-weight: bold; margin-bottom: var(--space-xs); font-size: 0.88rem;&quot;&gt;Customer must choose a box size&lt;/div&gt;
    &lt;div style=&quot;padding-left: var(--space-md); font-size: 0.85rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Small box: $25/week&lt;/div&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Large box: $45/week&lt;/div&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;No size selected &amp;rarr; error&lt;/div&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm);&quot;&gt;Switch small&amp;rarr;large Monday &amp;rarr; change from Thursday&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- Rule 2 --&gt;
  &lt;div style=&quot;padding-left: var(--space-md); border-left: 3px solid rgba(51,153,255,0.4); margin-bottom: var(--space-sm);&quot;&gt;
    &lt;div style=&quot;background: rgba(51,153,255,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); font-weight: bold; margin-bottom: var(--space-xs); font-size: 0.88rem;&quot;&gt;Payment must succeed&lt;/div&gt;
    &lt;div style=&quot;padding-left: var(--space-md); font-size: 0.85rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Valid card &amp;rarr; confirmed&lt;/div&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Declined card &amp;rarr; retry&lt;/div&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Weekly charge fails &amp;rarr; retry after 24hrs&lt;/div&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Two failures &amp;rarr; email customer&lt;/div&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Three failures &amp;rarr; auto-pause&lt;/div&gt;
      &lt;div style=&quot;background: rgba(204,51,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); color: var(--color-ink-secondary); font-style: italic;&quot;&gt;How do we handle 3D Secure?&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- Rule 3 --&gt;
  &lt;div style=&quot;padding-left: var(--space-md); border-left: 3px solid rgba(51,153,255,0.4); margin-bottom: var(--space-sm);&quot;&gt;
    &lt;div style=&quot;background: rgba(51,153,255,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); font-weight: bold; margin-bottom: var(--space-xs); font-size: 0.88rem;&quot;&gt;Customer sees first delivery date&lt;/div&gt;
    &lt;div style=&quot;padding-left: var(--space-md); font-size: 0.85rem;&quot;&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Monday sub &amp;rarr; this Thursday&lt;/div&gt;
      &lt;div style=&quot;background: rgba(51,170,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);&quot;&gt;Friday sub &amp;rarr; next Thursday&lt;/div&gt;
      &lt;div style=&quot;background: rgba(204,51,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs); color: var(--color-ink-secondary); font-style: italic;&quot;&gt;Exact cutoff for same-week delivery?&lt;/div&gt;
      &lt;div style=&quot;background: rgba(204,51,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); color: var(--color-ink-secondary); font-style: italic;&quot;&gt;Public holidays and delivery dates?&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- Rule 4 --&gt;
  &lt;div style=&quot;padding-left: var(--space-md); border-left: 3px solid rgba(51,153,255,0.4); margin-bottom: var(--space-sm);&quot;&gt;
    &lt;div style=&quot;background: rgba(51,153,255,0.08); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); font-weight: bold; font-size: 0.88rem;&quot;&gt;All times are AWST (Perth)&lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- Parked questions --&gt;
  &lt;div style=&quot;padding-left: var(--space-md); border-left: 3px solid rgba(204,51,51,0.3); font-size: 0.85rem;&quot;&gt;
    &lt;div style=&quot;color: var(--color-ink-secondary); font-weight: bold; margin-bottom: var(--space-xs); font-size: 0.82rem;&quot;&gt;Parked (other stories)&lt;/div&gt;
    &lt;div style=&quot;background: rgba(204,51,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs); color: var(--color-ink-secondary); font-style: italic;&quot;&gt;Multiple subscriptions per customer?&lt;/div&gt;
    &lt;div style=&quot;background: rgba(204,51,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs); color: var(--color-ink-secondary); font-style: italic;&quot;&gt;What&apos;s the cancellation policy?&lt;/div&gt;
    &lt;div style=&quot;background: rgba(204,51,51,0.06); border: 1px solid var(--color-rule); border-radius: 4px; padding: var(--space-xs) var(--space-sm); color: var(--color-ink-secondary); font-style: italic;&quot;&gt;Dietary preferences and allergies during subscription?&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Four rules, eleven examples, three questions still attached to the story, and three parked for other stories.&lt;/p&gt;

&lt;h3 id=&quot;the-too-many-red-cards-signal&quot;&gt;The “too many red cards” signal&lt;/h3&gt;

&lt;p&gt;If you have more red cards than green cards, the story isn’t ready to build.&lt;/p&gt;

&lt;p&gt;Three red cards against eleven green cards is fine, and those are just the ones attached to this story, not the parked ones. The Greenbox team decides to resolve the cutoff and 3D Secure questions before starting work, and to treat multiple subscriptions, cancellation, and dietary preferences as separate stories.&lt;/p&gt;

&lt;p&gt;If they’d had eight red cards and three green cards, that would be a clear signal: go away, answer the questions, and come back for another session.&lt;/p&gt;

&lt;p&gt;This is one of the best things about Example Mapping. It doesn’t just help you understand a story, it tells you when you &lt;em&gt;don’t&lt;/em&gt; understand it. A readiness check disguised as a planning session.&lt;/p&gt;

&lt;h3 id=&quot;second-session-pause-a-subscription&quot;&gt;Second session: “Pause a subscription”&lt;/h3&gt;

&lt;p&gt;The next story up is “Pause a subscription.” A customer is going on holiday and wants to skip a week or two.&lt;/p&gt;

&lt;p&gt;This time the session goes more smoothly. The team knows the domain better. Maya is in the groove of stating rules explicitly instead of assuming everyone already knows them.&lt;/p&gt;

&lt;p&gt;Three rules emerge quickly: customers can pause for one or more weeks, they’re not charged for paused weeks, and they must pause at least three days before the next delivery.&lt;/p&gt;

&lt;p&gt;The edge cases are where it gets interesting. “What about Tuesday?” Priya asks. “If the delivery is Thursday and they pause on Tuesday, is that three days?”&lt;/p&gt;

&lt;p&gt;Maya hesitates. “I don’t think so… Monday to Thursday is three days. Tuesday to Thursday is two.”&lt;/p&gt;

&lt;p&gt;“So Tuesday is too late,” Tom says. “But what does ‘three days before’ actually mean? Before midnight on Monday? Or 72 hours before the delivery window starts?”&lt;/p&gt;

&lt;p&gt;Maya: “Before the end of Monday. If you pause any time on Monday, you’re fine. Tuesday, you’re not.”&lt;/p&gt;

&lt;p&gt;They update the rule to be precise: pause must be requested before midnight AWST on the day three days before delivery. For Thursday deliveries, that’s end of Monday. Sam asks: “Does the same cutoff apply to unpausing? If I unpause on Wednesday, do I get a box Thursday?”&lt;/p&gt;

&lt;p&gt;Maya: “No, same rule. You’d need to unpause by end of Monday to get Thursday’s box. Otherwise it’s the following week.”&lt;/p&gt;

&lt;p&gt;One question comes up that nobody can answer: can a subscription stay paused indefinitely, or does something happen if a customer never resumes?&lt;/p&gt;

&lt;p&gt;Three rules, seven examples, one question. Much cleaner ratio than the first session. This story is nearly ready to build.&lt;/p&gt;

&lt;p&gt;Notice how much faster it went. The team is developing a shared language. When Maya says “three days before delivery,” everyone knows what delivery day means, how the weekly cycle works, what the constraints are. That shared understanding from Event Storming is paying off already.&lt;/p&gt;

&lt;h3 id=&quot;why-example-mapping-is-the-one-youll-use-most&quot;&gt;Why Example Mapping is the one you’ll use most&lt;/h3&gt;

&lt;p&gt;Event Storming is brilliant for understanding a whole domain. You might do it once at the start of a project, or when entering a new area.&lt;/p&gt;

&lt;p&gt;Example Mapping is different. You do it &lt;em&gt;before every story&lt;/em&gt;. Every single one.&lt;/p&gt;

&lt;p&gt;It’s a short conversation. It surfaces assumptions. It catches edge cases. It builds shared understanding. And it tells you when a story isn’t ready.&lt;/p&gt;

&lt;p&gt;The Greenbox team starts doing Example Maps before picking up each new story. Before Tom and Priya start building, they spend twenty-five minutes with Maya and Jas mapping it out. The red cards tell them what to resolve. The green cards tell them what to build. The blue cards tell them the rules to enforce.&lt;/p&gt;

&lt;p&gt;Three weeks in, they’ve stopped finding surprises in code review. The arguments about scope have disappeared. When Priya finishes a story, it matches what Maya expected, because they agreed on concrete examples before anyone wrote a line of code.&lt;/p&gt;

&lt;p&gt;If you only adopt one technique from this series, make it Example Mapping. Twenty-five minutes. Four colours of card. Every assumption surfaced before it becomes a bug.&lt;/p&gt;

&lt;p&gt;Tom sits in his car after the session and texts Sarah: “I just spent 25 minutes doing something I thought was pointless and it saved me a week of work.” Sarah replies: “You sound surprised that something other than coding was useful.” He puts the phone down without responding. But he’s smiling.&lt;/p&gt;

&lt;h3 id=&quot;now-what&quot;&gt;Now what?&lt;/h3&gt;

&lt;p&gt;The team has cards on a table and a shared understanding of what “subscribe to a produce box” means, concrete, unambiguous, agreed upon by everyone in the room.&lt;/p&gt;

&lt;p&gt;But cards on a table aren’t software. Tom picks up his bag. “Right. I’m going to build this.”&lt;/p&gt;

&lt;p&gt;“Which part first?” Lee asks. “You’ve got red cards to resolve, Maya needs the subscription system live and hitting 200 subscribers before the seed money runs out, and some of these stories reduce more risk than others. What order gives you the most confidence that you’ll ship something useful by then?”&lt;/p&gt;

&lt;p&gt;Tom looks at the cards. He knows what to build. He doesn’t know what to build &lt;em&gt;first&lt;/em&gt;, or how to make the building predictable. None of them do. Not yet.&lt;/p&gt;

&lt;p&gt;That’s where &lt;a href=&quot;/writing/sprint-planning-turning-sticky-notes-into-delivery/&quot;&gt;the first sprints&lt;/a&gt; come in, turning sticky notes into delivery, one fortnight at a time.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>How LLMs Actually Work</title>
    <link href="/writing/how-llms-actually-work/"/>
    <updated>2026-03-26T06:00:00+08:00</updated>
    <id>/writing/how-llms-actually-work/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part  of &lt;a href=&quot;/writing/the-ai-field-guide/&quot;&gt;the The AI Field Guide series&lt;/a&gt; · &lt;a href=&quot;/writing/under-the-hood/&quot;&gt;Under the Hood&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;You type a question. A few seconds later, coherent, fluent text appears on your screen, text that seems to understand what you asked, that follows instructions, that writes code and poetry and legal briefs. It’s natural to wonder: what is actually happening in there?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In 1980, the philosopher &lt;a href=&quot;https://doi.org/10.1017/S0140525X00005756&quot;&gt;John Searle&lt;/a&gt; posed a thought experiment. Imagine you’re locked in a room. People slide Chinese characters under the door. You don’t speak Chinese, but you have an enormous book of rules: “When you see this pattern, write that pattern and slide it back.” You follow the rules perfectly. To the people outside, it looks like the room understands Chinese. But you, the person in the room, understand nothing. You’re just matching patterns.&lt;/p&gt;

&lt;p&gt;Large language models are the most sophisticated Chinese Room ever built. They don’t “understand” language in the way humans do. They don’t have beliefs, memories, or intentions. What they do, and they do it extraordinarily well, is predict the next &lt;label for=&quot;sn-writing-how-llms-actually-work-token&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-token-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;token&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-token&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-token-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Token&lt;/span&gt;The unit of text an LLM actually sees – usually a short character sequence, not a whole word.
&lt;/span&gt; in a sequence. One token at a time, over and over, until the response is complete.&lt;/p&gt;

&lt;p&gt;But here’s where Searle’s analogy breaks down, or at least gets interesting. “Just predicting the next token” turns out to be a surprisingly rich activity. To predict well, the model has to capture something about syntax, semantics, logic, world knowledge, coding conventions, social norms, and the structure of arguments. Not because anyone told it to. Because all of those things are reflected in the patterns of text that humans produce, and the model learned those patterns by reading a significant fraction of the internet.&lt;/p&gt;

&lt;p&gt;Is that understanding? Or just very good pattern matching? We’ll come back to that question; it’s more slippery than it sounds. But first, let’s open up the room and look at the machinery inside. It starts with tokens.&lt;/p&gt;

&lt;h3 id=&quot;tokens-the-atoms-of-text&quot;&gt;Tokens: the atoms of text&lt;/h3&gt;

&lt;p&gt;LLMs don’t read characters. They don’t read words, either. They read tokens: chunks of text that sit somewhere between characters and words in size.&lt;/p&gt;

&lt;p&gt;The word “understanding” might be a single token. The word “tokenisation” might be split into “token” + “isation”. A common word like “the” is almost certainly a single token in any major tokeniser. An uncommon word like “antidisestablishmentarianism” would be split into several. Numbers are tokenised digit by digit or in small groups. Code tokens include things like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;def&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;return&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;()&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\n&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why tokens instead of characters or words? Characters are too granular; a model working character by character would need enormous context windows to see meaningful patterns. Words are too coarse, with hundreds of thousands of distinct words in English alone, and the model would need a separate entry for every inflection, tense, and compound. Tokens hit a practical sweet spot.&lt;/p&gt;

&lt;p&gt;The process of breaking text into tokens is called tokenisation, and the dominant method is Byte Pair Encoding (BPE), originally described by &lt;a href=&quot;https://dl.acm.org/doi/10.5555/177910.177914&quot;&gt;Philip Gage in 1994&lt;/a&gt; as a data compression algorithm and later adapted for neural language models by &lt;a href=&quot;https://aclanthology.org/P16-1162/&quot;&gt;Sennrich, Haddow, and Birch in 2016&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;BPE works by starting with individual bytes (or characters) and iteratively merging the most frequent pair. Here’s a simplified example:&lt;/p&gt;

&lt;p&gt;Suppose your &lt;label for=&quot;sn-writing-how-llms-actually-work-training&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-training-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;training&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-training&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-training-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Training&lt;/span&gt;The process of fitting a model’s weights to data by minimising a loss function.
&lt;/span&gt; text contains the sequence &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;low lower lowest&lt;/code&gt; repeatedly. BPE starts with individual characters: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;l&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;w&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t&lt;/code&gt;, and so on. It counts every adjacent pair. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;l&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o&lt;/code&gt; appears most frequently, it merges them into a new token &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lo&lt;/code&gt;. Now it counts again. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lo&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;w&lt;/code&gt; is the most frequent pair, it merges them into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;low&lt;/code&gt;. Then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;low&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e&lt;/code&gt; might merge into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lowe&lt;/code&gt;, and so on. The process continues for a fixed number of merge operations (typically 30,000 to 100,000), producing a vocabulary of that many tokens.&lt;/p&gt;

&lt;p&gt;The result is a vocabulary where common words are single tokens, common subwords are single tokens, and rare or novel words get split into known pieces. This is crucial for handling words the model has never seen before: it can still process them, just broken into familiar subword units.&lt;/p&gt;

&lt;p&gt;Most modern LLMs use vocabularies of 30,000 to 100,000 tokens. GPT-4 uses around 100,000. Claude uses a similar order of magnitude. The exact vocabulary depends on the training data and the number of BPE merges performed.&lt;/p&gt;

&lt;p&gt;A practical consequence: LLMs “see” text differently from humans. The sentence “I saw a dog” might be four tokens. The sentence “I saw a Labradoodle” might be five or six, because “Labradoodle” gets split into subwords. The model doesn’t see characters. It sees a sequence of integer IDs, each mapping to a token in its vocabulary. Token 1547 might be “the”. Token 28903 might be “ function” (with a leading space; spaces are part of tokens in most schemes). Token 85 might be a newline character.&lt;/p&gt;

&lt;p&gt;This tokenisation step is entirely mechanical. It happens before the model sees anything. The model never operates on raw text, only on sequences of token IDs.&lt;/p&gt;

&lt;h3 id=&quot;embeddings-giving-tokens-meaning&quot;&gt;Embeddings: giving tokens meaning&lt;/h3&gt;

&lt;p&gt;A token ID is just a number. The model needs something richer: a representation that captures the &lt;em&gt;meaning&lt;/em&gt; of each token and its relationship to other tokens.&lt;/p&gt;

&lt;p&gt;This is where &lt;label for=&quot;sn-writing-how-llms-actually-work-embedding&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-embedding-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;embeddings&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-embedding&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-embedding-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Embedding&lt;/span&gt;A fixed-length vector of floats that represents a piece of text (or image, or other thing) in a space where similar meanings sit close together.
&lt;/span&gt; come in. Each token in the vocabulary is assigned a high-dimensional vector, a list of numbers, typically 4,096 to 12,288 of them in modern LLMs. These vectors are learned during training, not hand-crafted. At the start of training, they’re initialised randomly. By the end, tokens with similar meanings have vectors that point in similar directions in this high-dimensional space.&lt;/p&gt;

&lt;p&gt;The classic example, from &lt;a href=&quot;https://arxiv.org/abs/1301.3781&quot;&gt;Mikolov et al.’s 2013 word2vec paper&lt;/a&gt;, is that the vector for “king” minus the vector for “man” plus the vector for “woman” gives a vector very close to “queen”. This isn’t a trick; it falls out naturally from training on large amounts of text, because the contexts in which these words appear encode their relationships.&lt;/p&gt;

&lt;p&gt;In an &lt;label for=&quot;sn-writing-how-llms-actually-work-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt;, the embedding layer is the first thing that happens. The input sequence of token IDs gets converted into a sequence of embedding vectors. If your input is 500 tokens and each token maps to a vector of 8,192 dimensions, you now have a 500 x 8,192 matrix of floating-point numbers. This matrix is what flows into the rest of the model.&lt;/p&gt;

&lt;p&gt;But there’s a problem: the embedding for a token is the same regardless of where it appears in the sequence. The word “bank” has one embedding, whether it means a river bank, a financial bank, or a shot in snooker. The model needs to know not just what each token is, but where it sits in the sequence.&lt;/p&gt;

&lt;p&gt;Positional encoding solves this. The original &lt;label for=&quot;sn-writing-how-llms-actually-work-transformer&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-transformer-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;transformer&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-transformer&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-transformer-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Transformer&lt;/span&gt;The neural network architecture that underpins modern LLMs – stacks of self-attention layers that let every token look at every other token in the context.
&lt;/span&gt; paper (&lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;Vaswani et al., 2017&lt;/a&gt;) used sinusoidal functions to generate position-dependent vectors that are added to the token embeddings. More recent models use Rotary Position Embeddings (RoPE, &lt;a href=&quot;https://arxiv.org/abs/2104.09864&quot;&gt;Su et al., 2021&lt;/a&gt;), which encode relative positions by rotating the embedding vectors. The details vary, but the purpose is the same: after positional encoding, the model can distinguish between “The dog bit the man” and “The man bit the dog”.&lt;/p&gt;

&lt;h3 id=&quot;the-transformer-the-architecture-underneath&quot;&gt;The transformer: the architecture underneath&lt;/h3&gt;

&lt;p&gt;Every major LLM (GPT, Claude, Llama, Gemini) is built on the transformer architecture, introduced in a 2017 paper by researchers at Google with the quietly confident title &lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;“Attention Is All You Need”&lt;/a&gt;. Before transformers, language models used recurrent neural networks (RNNs) that processed text one word at a time, left to right, like reading a sentence with a finger. This worked, but it was slow and struggled with long-range dependencies; by the time the model reached the end of a paragraph, it had largely forgotten the beginning.&lt;/p&gt;

&lt;p&gt;Transformers threw that away. Instead of processing text sequentially, a transformer looks at the entire input at once and figures out which parts relate to which other parts. It’s the difference between reading a sentence word by word and seeing the whole sentence on a page. This parallelism made transformers dramatically faster to train, and the ability to attend to any part of the input regardless of distance made them dramatically better at capturing meaning.&lt;/p&gt;

&lt;p&gt;The transformer is built from a stack of identical blocks, each containing two key components: an &lt;label for=&quot;sn-writing-how-llms-actually-work-attention&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-attention-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;attention&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-attention&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-attention-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Attention&lt;/span&gt;The mechanism inside a transformer that lets each token weigh how much every other token in the context matters to it.
&lt;/span&gt; mechanism (which figures out what to pay attention to) and a feed-forward network (which processes the result). We’ll look at both, starting with attention, the mechanism that made the whole thing work.&lt;/p&gt;

&lt;h3 id=&quot;attention-the-mechanism-that-changed-everything&quot;&gt;Attention: the mechanism that changed everything&lt;/h3&gt;

&lt;p&gt;The core innovation is the attention mechanism. It’s what allows the model to relate different parts of the input to each other, regardless of distance.&lt;/p&gt;

&lt;p&gt;Here’s the intuition. Consider the sentence: “The cat sat on the mat because it was tired.” What does “it” refer to? The cat, obviously. But how does the model figure that out? It needs to look back at every previous token and determine which ones are relevant to interpreting “it” in this context.&lt;/p&gt;

&lt;p&gt;Attention lets the model do exactly this. For each token in the sequence, the model computes three things from its embedding:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A query vector: “What am I looking for?”&lt;/li&gt;
  &lt;li&gt;A key vector: “What do I contain?”&lt;/li&gt;
  &lt;li&gt;A value vector: “What information should I provide if I’m relevant?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are computed by multiplying the token’s embedding by three learned weight matrices (Q, K, and V). Then, for each token, the model computes the dot product of its query with every other token’s key. This produces a set of attention scores: numbers indicating how relevant each other token is to the current one.&lt;/p&gt;

&lt;p&gt;These scores are passed through a softmax function (which converts them into probabilities that sum to 1), and then used to compute a weighted average of the value vectors. The result is a new representation of the current token that incorporates information from every other token in the sequence, weighted by relevance.&lt;/p&gt;

&lt;p&gt;In the “it was tired” example, the attention mechanism would assign a high score to the pairing of “it” (query) with “cat” (key), because the model has learned from training data that pronouns attend to their antecedents.&lt;/p&gt;

&lt;p&gt;The mathematical formulation, from the original transformer paper, is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sqrt(d_k)&lt;/code&gt; term is a scaling factor (d_k is the dimension of the key vectors) that prevents the dot products from becoming too large, which would push the softmax into regions where the gradients are tiny and learning stalls.&lt;/p&gt;

&lt;h3 id=&quot;multi-head-attention-parallel-perspectives&quot;&gt;Multi-head attention: parallel perspectives&lt;/h3&gt;

&lt;p&gt;A single attention computation captures one kind of relationship between tokens. But language is rich. A single token might simultaneously need to attend to its syntactic subject, the verb it modifies, the topic of the paragraph, and the format of the document.&lt;/p&gt;

&lt;p&gt;Multi-head attention runs multiple attention computations in parallel, each with its own Q, K, and V weight matrices. A model with 32 attention heads computes 32 different sets of attention patterns simultaneously. The results are concatenated and projected back to the model’s dimension through another learned weight matrix.&lt;/p&gt;

&lt;p&gt;Different heads learn to capture different kinds of relationships. Research by &lt;a href=&quot;https://aclanthology.org/P19-1580/&quot;&gt;Clark et al. (2019)&lt;/a&gt; and others has found that in trained models, some attention heads specialise in syntactic dependencies (subject-verb agreement), some in positional relationships (attending to the previous token), some in semantic relationships, and some in patterns that are difficult for humans to interpret.&lt;/p&gt;

&lt;p&gt;Nobody tells the heads what to specialise in. The specialisation emerges from training. The model discovers that attending to different kinds of information in parallel produces better predictions.&lt;/p&gt;

&lt;h3 id=&quot;the-transformer-block&quot;&gt;The transformer block&lt;/h3&gt;

&lt;p&gt;An attention layer is part of a larger unit called a transformer block (or transformer layer). Each block consists of:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Multi-head self-attention: the attention mechanism described above&lt;/li&gt;
  &lt;li&gt;Layer normalisation: scaling the outputs to have zero mean and unit variance, which stabilises training&lt;/li&gt;
  &lt;li&gt;Feed-forward network: two linear transformations with a non-linear activation function (typically &lt;a href=&quot;https://arxiv.org/abs/1606.08415&quot;&gt;GeLU&lt;/a&gt; or &lt;a href=&quot;https://arxiv.org/abs/2002.05202&quot;&gt;SwiGLU&lt;/a&gt;) in between&lt;/li&gt;
  &lt;li&gt;Residual connections: adding the input of each sub-layer to its output, so information can flow through the network without being forced through every transformation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The feed-forward network is where much of the model’s “knowledge” is believed to be stored. While attention handles the relationships between tokens, the feed-forward layers act as a kind of lookup table: a massive, compressed, approximate memory of facts and patterns learned during training. &lt;a href=&quot;https://aclanthology.org/2021.emnlp-main.446/&quot;&gt;Research by Geva et al. (2021)&lt;/a&gt; characterised feed-forward layers as “key-value memories” where the first linear transformation acts as keys and the second acts as values.&lt;/p&gt;

&lt;p&gt;A modern LLM stacks many transformer blocks on top of each other. GPT-4 is believed to have around 120 layers. Claude’s architecture isn’t public, but models of this class typically have 80 to 120 layers. The input embeddings flow through every block, being progressively refined. Early layers tend to capture surface-level patterns (syntax, local word relationships). Middle layers capture more abstract features (semantic roles, entity relationships). Late layers produce the representations that directly inform the prediction of the next token.&lt;/p&gt;

&lt;h3 id=&quot;context-windows-how-much-the-model-can-see&quot;&gt;Context windows: how much the model can see&lt;/h3&gt;

&lt;p&gt;The &lt;label for=&quot;sn-writing-how-llms-actually-work-context-window&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-context-window-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;context window&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-context-window&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-context-window-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Context window&lt;/span&gt;The maximum number of tokens an LLM can attend to in a single call – prompt plus output combined.
&lt;/span&gt; is the maximum number of tokens the model can process in a single forward pass. It’s a hard limit: the model literally cannot see tokens outside this window.&lt;/p&gt;

&lt;p&gt;Early transformer models had modest context windows: GPT-2 (2019) had 1,024 tokens, roughly 750 words. GPT-3 (2020) had 2,048 tokens. As of 2025, context windows have expanded dramatically. Claude’s context window is 1,000,000 tokens, roughly 750,000 words, or about ten novels.&lt;/p&gt;

&lt;p&gt;The expansion is non-trivial because the standard attention mechanism has a computational cost that scales quadratically with sequence length. If you double the context window, the attention computation costs four times as much. For a 200,000-token context window with naive attention, the cost would be staggering.&lt;/p&gt;

&lt;p&gt;Modern models address this through various efficiency techniques. FlashAttention (&lt;a href=&quot;https://arxiv.org/abs/2205.14135&quot;&gt;Dao et al., 2022&lt;/a&gt;) restructures the attention computation to be more cache-efficient without changing the mathematical result. Grouped-query attention (GQA) shares key and value projections across multiple query heads, reducing memory requirements. Some models use sparse attention patterns that allow each token to attend to only a subset of other tokens.&lt;/p&gt;

&lt;p&gt;The context window matters because everything the model “knows” about your specific conversation comes from the context window. The model has no persistent memory between conversations. If you had a conversation yesterday, the model doesn’t remember it. If you mentioned your name 50,000 tokens ago, the model can (in principle) still attend to that information, but the practical effectiveness of attention over very long ranges depends on the model and the training.&lt;/p&gt;

&lt;h3 id=&quot;generating-text-one-token-at-a-time&quot;&gt;Generating text: one token at a time&lt;/h3&gt;

&lt;p&gt;Here’s where things get concrete. When you send a &lt;label for=&quot;sn-writing-how-llms-actually-work-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; to an LLM, the model processes the entire input through all its layers and produces, at the final layer, a probability distribution over the entire vocabulary for the next token.&lt;/p&gt;

&lt;p&gt;Not the next sentence. Not the next word. The next token.&lt;/p&gt;

&lt;p&gt;The model might assign a 15% probability to “the”, 8% to “a”, 4% to “\n”, 3% to “this”, and so on across all 100,000 tokens in its vocabulary. These probabilities sum to 1.&lt;/p&gt;

&lt;p&gt;Then the model selects one token from this distribution, appends it to the sequence, and runs the whole process again to predict the token after that. This is called autoregressive generation: each output becomes part of the input for the next prediction.&lt;/p&gt;

&lt;p&gt;A 500-token response requires 500 forward passes through the entire model. This is why generation is slower than processing the input. Each new token requires a full pass through all layers (though in practice, the computation is optimised using a &lt;label for=&quot;sn-writing-how-llms-actually-work-kv-cache&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-kv-cache-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;KV cache&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-kv-cache&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-kv-cache-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;KV cache&lt;/span&gt;A reuseable cache of the model’s attention computations for tokens it’s already seen, so generating the next token doesn’t redo work.
&lt;/span&gt; that stores the key and value vectors from previous tokens so they don’t need to be recomputed).&lt;/p&gt;

&lt;h3 id=&quot;temperature-and-top-p-controlling-randomness&quot;&gt;Temperature and top-p: controlling randomness&lt;/h3&gt;

&lt;p&gt;How does the model choose which token to select from the probability distribution? This is where &lt;label for=&quot;sn-writing-how-llms-actually-work-temperature&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-temperature-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;temperature&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-temperature&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-temperature-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Temperature&lt;/span&gt;A knob (usually 0 to 2) that controls how much the model deviates from its highest-probability next token.
&lt;/span&gt; and top-p (&lt;label for=&quot;sn-writing-how-llms-actually-work-top-p&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-top-p-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;nucleus sampling&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-top-p&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-top-p-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Top-p / Top-k&lt;/span&gt;Two ways to truncate the model’s probability distribution before sampling – top-k keeps the K most likely tokens, top-p (nucleus) keeps the smallest set whose cumulative probability reaches P.
&lt;/span&gt;) come in.&lt;/p&gt;

&lt;p&gt;Temperature scales the logits (the raw, pre-softmax scores) before converting them to probabilities. A temperature of 1.0 uses the distribution as-is. A temperature below 1.0 (say, 0.3) makes the distribution “sharper”: the most likely tokens become even more likely, and unlikely tokens become even less likely. A temperature of 0 is deterministic: always pick the highest-probability token. A temperature above 1.0 “flattens” the distribution, making unlikely tokens more likely to be selected.&lt;/p&gt;

&lt;p&gt;Low temperature produces more predictable, focused text. High temperature produces more varied, creative (and sometimes nonsensical) text.&lt;/p&gt;

&lt;p&gt;Top-p (nucleus sampling, introduced by &lt;a href=&quot;https://arxiv.org/abs/1904.09751&quot;&gt;Holtzman et al., 2020&lt;/a&gt;) takes a different approach: instead of scaling all probabilities, it considers only the smallest set of tokens whose cumulative probability exceeds a threshold p. If p = 0.9, the model considers only the top tokens that together account for 90% of the probability mass, and samples from among those. Everything else is excluded.&lt;/p&gt;

&lt;p&gt;Top-p is adaptive. When the model is confident (one token dominates the distribution), the nucleus is small. When the model is uncertain (many tokens are roughly equally likely), the nucleus is large. This tends to produce better results than temperature alone, because it naturally adjusts the diversity of outputs to the model’s confidence.&lt;/p&gt;

&lt;p&gt;In practice, APIs expose both parameters, and they interact. Most production uses keep temperature relatively low (0.0 to 0.7) for factual tasks and higher (0.7 to 1.0) for creative tasks.&lt;/p&gt;

&lt;h3 id=&quot;the-training-pipeline&quot;&gt;The training pipeline&lt;/h3&gt;

&lt;p&gt;How does a model learn to predict the next token? The training process has three major phases, each building on the last.&lt;/p&gt;

&lt;h4 id=&quot;phase-1-pretraining&quot;&gt;Phase 1: Pretraining&lt;/h4&gt;

&lt;p&gt;Pretraining is where the model learns language. The training data is a massive corpus of text: web pages, books, code repositories, academic papers, forums, documentation. For frontier models, the term the industry uses for the most capable models from the leading labs, like Claude, GPT-4, and Gemini, this corpus is measured in trillions of tokens. The exact composition is typically proprietary, but it includes a broad cross-section of human-written text.&lt;/p&gt;

&lt;p&gt;The training objective is straightforward: given a sequence of tokens, predict the next one. The model processes the training data in batches, makes predictions, computes how wrong it was (using cross-entropy loss, which measures the difference between the predicted probability distribution and the actual next token), and adjusts its weights to be slightly less wrong next time.&lt;/p&gt;

&lt;p&gt;This adjustment happens through backpropagation and gradient descent, the same optimisation procedure used in virtually all deep learning. The loss function tells you how wrong the model was. Backpropagation computes how each weight in the model contributed to that error. Gradient descent adjusts each weight by a small amount in the direction that reduces the error. Repeat this billions of times, across trillions of tokens, and the weights gradually converge on values that produce good predictions.&lt;/p&gt;

&lt;p&gt;Modern pretraining uses the Adam optimiser (&lt;a href=&quot;https://arxiv.org/abs/1412.6980&quot;&gt;Kingma and Ba, 2015&lt;/a&gt;) or variants of it, with learning rate schedules that warm up the learning rate gradually and then decay it. The training runs on thousands of GPUs (or TPUs) for weeks or months. The compute cost for frontier models is measured in tens of millions of dollars.&lt;/p&gt;

&lt;p&gt;The remarkable thing about pretraining is how much emerges from such a simple objective. The model isn’t told about grammar, logic, programming languages, history, or mathematics. It just learns to predict the next token. But to predict well across such a diverse corpus, it must implicitly capture an enormous amount about the structure of language and the world it describes.&lt;/p&gt;

&lt;h4 id=&quot;phase-2-fine-tuning-supervised&quot;&gt;Phase 2: Fine-tuning (supervised)&lt;/h4&gt;

&lt;p&gt;A pretrained model is good at predicting text, but it’s not yet useful as an assistant. If you prompt it with “What is the capital of Australia?”, a purely pretrained model might continue with “The answer is Canberra”, but it might also continue with “This question appears on the geography quiz for Year 7 students” or “A. Canberra B. Sydney C. Melbourne D. Brisbane”. It’s predicting what text is likely to follow, and there are many plausible continuations.&lt;/p&gt;

&lt;p&gt;&lt;label for=&quot;sn-writing-how-llms-actually-work-fine-tuning&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-fine-tuning-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Supervised fine-tuning&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-fine-tuning&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-fine-tuning-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Fine-tuning&lt;/span&gt;Continuing to train an already-trained model on a smaller dataset to adapt its behaviour.
&lt;/span&gt; (SFT) narrows the model’s behaviour by training it on examples of the desired interaction pattern. Human annotators write thousands of example prompt-response pairs demonstrating the kind of helpful, accurate, structured responses the model should produce. The model is fine-tuned on these examples using the same next-token prediction objective, but with a much smaller, curated dataset.&lt;/p&gt;

&lt;p&gt;SFT teaches the model the &lt;em&gt;format&lt;/em&gt; of being an assistant: that it should answer questions directly, structure its responses clearly, acknowledge uncertainty, and follow instructions.&lt;/p&gt;

&lt;h4 id=&quot;phase-3-rlhf-reinforcement-learning-from-human-feedback&quot;&gt;Phase 3: RLHF (Reinforcement Learning from Human Feedback)&lt;/h4&gt;

&lt;p&gt;SFT gets the model most of the way there, but human preferences are subtle. Is it better to give a concise answer or a thorough one? How should the model handle ambiguous instructions? When should it refuse a request?&lt;/p&gt;

&lt;p&gt;&lt;label for=&quot;sn-writing-how-llms-actually-work-rlhf&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-rlhf-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Reinforcement Learning from Human Feedback&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-rlhf&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-rlhf-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RLHF&lt;/span&gt;Training a model to prefer outputs humans rank highly, on top of standard supervised training.
&lt;/span&gt; (RLHF, described by &lt;a href=&quot;https://arxiv.org/abs/2203.02155&quot;&gt;Ouyang et al., 2022&lt;/a&gt; for the InstructGPT work) addresses this by training the model to optimise for human preferences.&lt;/p&gt;

&lt;p&gt;The process has two steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Train a reward model. Generate multiple responses to the same prompt. Human annotators rank them from best to worst. Train a separate neural network (the reward model) to predict which response a human would prefer. This reward model learns to score outputs on quality, helpfulness, safety, and adherence to instructions.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Optimise the language model against the reward model. Using a reinforcement learning algorithm (typically PPO, &lt;a href=&quot;https://arxiv.org/abs/1707.06347&quot;&gt;Proximal Policy Optimisation&lt;/a&gt;, Schulman et al., 2017, or more recently DPO, &lt;a href=&quot;https://arxiv.org/abs/2305.18290&quot;&gt;Direct Preference Optimisation&lt;/a&gt;), adjust the language model’s weights to produce outputs that the reward model scores highly. The key constraint is that the model shouldn’t deviate too far from the fine-tuned model. You don’t want optimising for the reward model to destroy the model’s general capabilities.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;RLHF is what makes the difference between a model that can predict text and a model that is genuinely useful to interact with. It’s also what makes models more cautious, more structured in their responses, and more inclined to refuse harmful requests.&lt;/p&gt;

&lt;p&gt;Some newer approaches, including Constitutional AI (&lt;a href=&quot;https://arxiv.org/abs/2212.08073&quot;&gt;Bai et al., 2022&lt;/a&gt;), use AI feedback in addition to (or instead of) human feedback in parts of the process, but the core idea remains: optimise the model’s outputs to align with human preferences.&lt;/p&gt;

&lt;h3 id=&quot;what-predicting-the-next-token-actually-means&quot;&gt;What “predicting the next token” actually means&lt;/h3&gt;

&lt;p&gt;There’s a common dismissal of LLMs: “It’s just predicting the next token.” This is technically accurate and deeply misleading.&lt;/p&gt;

&lt;p&gt;Consider what it takes to predict the next token well. If the context is a legal contract, the model must “know” contract structure, legal terminology, and the conventions of contract drafting. If the context is Python code, it must track variable scopes, function signatures, indentation, and the semantics of the language. If the context is a conversation about quantum physics, it must produce text that’s consistent with quantum mechanics.&lt;/p&gt;

&lt;p&gt;The model doesn’t “know” these things in the way a human expert does. It has no experiences, no intuitions, no understanding of why quantum mechanics is the way it is. But it has captured statistical patterns in text that are rich enough to produce outputs that look like they come from someone who does understand.&lt;/p&gt;

&lt;p&gt;This is genuinely remarkable, and it’s also the source of the most important failure modes. The model is optimising for “what would plausible-sounding text look like here?”, not for “what is true?” These are usually the same thing, because plausible text about well-covered topics tends to be accurate. But they diverge in exactly the cases where accuracy matters most: obscure facts, recent events, precise numerical claims, and reasoning chains that require strict logical validity.&lt;/p&gt;

&lt;h3 id=&quot;why-they-hallucinate&quot;&gt;Why they hallucinate&lt;/h3&gt;

&lt;p&gt;&lt;label for=&quot;sn-writing-how-llms-actually-work-hallucination&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-hallucination-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Hallucination&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-hallucination&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-hallucination-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Hallucination&lt;/span&gt;An LLM stating something false with the same confidence it states something true.
&lt;/span&gt;, the generation of confident, fluent, entirely fabricated information, is not a bug that can be fixed with more training data. It’s a structural consequence of how LLMs work.&lt;/p&gt;

&lt;p&gt;The model generates text by choosing high-probability tokens one at a time. It has no mechanism for checking whether its output is factually correct. It has no database of facts it can look up. It has no way to distinguish between “this is a pattern I learned from reliable sources” and “this is a plausible-sounding continuation that happens to be wrong.”&lt;/p&gt;

&lt;p&gt;When the model encounters a question about an obscure topic, it faces a choice: produce fluent text that matches the expected pattern (which might be wrong), or signal uncertainty (which requires overriding the strong pattern of producing confident text that it learned during training). The training process, especially RLHF, has pushed models toward expressing uncertainty more often, but the fundamental tension remains.&lt;/p&gt;

&lt;p&gt;Hallucination is especially likely when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The question asks for specific details (dates, numbers, names) about topics that appear infrequently in the training data&lt;/li&gt;
  &lt;li&gt;The model is asked to cite sources (it has learned the pattern of citations but doesn’t have access to a citation database)&lt;/li&gt;
  &lt;li&gt;The question requires reasoning that extends beyond the patterns in the training data&lt;/li&gt;
  &lt;li&gt;The prompt is ambiguous and the model guesses at intent rather than asking for clarification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;label for=&quot;sn-writing-how-llms-actually-work-rag&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-rag-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;Retrieval-augmented generation&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-rag&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-rag-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;RAG&lt;/span&gt;A pattern where you retrieve relevant documents at query time and stuff them into the prompt so the model can ground its answer on them.
&lt;/span&gt; (RAG), where the model is given relevant documents to reference, helps significantly, because it replaces “generate from patterns” with “summarise from provided text.” But the underlying architecture hasn’t changed. The model is still predicting tokens, not verifying facts.&lt;/p&gt;

&lt;h3 id=&quot;why-theyre-good-at-code&quot;&gt;Why they’re good at code&lt;/h3&gt;

&lt;p&gt;LLMs are disproportionately good at writing code, and the reasons are illuminating.&lt;/p&gt;

&lt;p&gt;First, code is heavily represented in training data. GitHub alone contains billions of files of source code, all publicly available. Stack Overflow has millions of answered questions with code examples. Documentation, tutorials, blog posts, textbooks: the volume of well-structured code in the training corpus is enormous.&lt;/p&gt;

&lt;p&gt;Second, code is less ambiguous than natural language. A function either compiles or it doesn’t. A variable is either in scope or it isn’t. The syntax rules are strict and well-defined. This makes code easier for a statistical model to learn, because the patterns are more consistent. In natural language, “bank” can mean ten different things. In Python, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;def&lt;/code&gt; always means the same thing.&lt;/p&gt;

&lt;p&gt;Third, code is highly repetitive. Most code follows standard patterns: import libraries, define functions, handle errors, return results. Design patterns recur across millions of repositories. The model doesn’t need to invent novel algorithms (though it sometimes can); it needs to recognise which pattern applies and instantiate it correctly for the current context.&lt;/p&gt;

&lt;p&gt;Fourth, code comes with its own error-checking mechanism. When you run LLM-generated code and it fails, the error message is itself a prompt you can feed back to the model. This feedback loop, generate, run, fix, repeat, is enormously productive, because the model is good at understanding error messages and making targeted corrections.&lt;/p&gt;

&lt;p&gt;This is part of the shift described in &lt;a href=&quot;/writing/the-value-is-in-ideas-not-code/&quot;&gt;The Value Is in Ideas, Not Code&lt;/a&gt;: when code generation becomes cheap, the bottleneck moves to knowing what to ask for. The teams that get the most from LLMs aren’t the ones with the best prompts; they’re the ones with the clearest understanding of their domain, the best-structured knowledge (decision records, test suites, observability), and the discipline to review what the model produces rather than trusting it blindly.&lt;/p&gt;

&lt;h3 id=&quot;the-gap-between-capability-and-understanding&quot;&gt;The gap between capability and understanding&lt;/h3&gt;

&lt;p&gt;Here’s the thing that I think is most important to understand about LLMs, and it’s the thing that most commentary gets wrong.&lt;/p&gt;

&lt;p&gt;LLMs are not “stochastic parrots” that merely recombine memorised text. Nor are they conscious beings that understand what they’re saying. They’re something new, something we don’t have a great word for yet.&lt;/p&gt;

&lt;p&gt;They can follow complex instructions. They can write functional code for problems that don’t appear in their training data. They can reason through multi-step problems (imperfectly, but measurably). They can transfer knowledge between domains in ways that look a lot like understanding. They can generate creative solutions that surprise even their creators.&lt;/p&gt;

&lt;p&gt;But they can also fail at basic arithmetic, get confused by negation, confidently assert falsehoods, struggle with spatial reasoning, and produce outputs that are syntactically perfect but semantically absurd. These failures are not random; they reflect the boundaries of what can be learned from the statistical patterns of text.&lt;/p&gt;

&lt;p&gt;A useful analogy: an LLM is like someone who has read everything ever written but has never been outside. They can describe a sunset beautifully because they’ve read thousands of descriptions. They can explain the physics of light scattering. They can write a character who watches a sunset and feels moved. But they’ve never actually seen one. Their knowledge is real, and it produces genuinely useful outputs, but it’s mediated entirely through text.&lt;/p&gt;

&lt;p&gt;This gap matters practically. LLMs are extraordinary tools for generation, summarisation, translation, code writing, brainstorming, and pattern matching. They are poor tools for factual verification, mathematical proof, real-time information, and any task where correctness must be guaranteed rather than probable.&lt;/p&gt;

&lt;h3 id=&quot;the-transformer-architecture-at-a-glance&quot;&gt;The transformer architecture at a glance&lt;/h3&gt;

&lt;p&gt;Here’s a summary of how the pieces fit together, from input to output.&lt;/p&gt;

&lt;div style=&quot;overflow-x: auto;&quot;&gt;
&lt;table style=&quot;font-size: 0.88rem; width: 100%; border-collapse: collapse;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom: 2px solid var(--color-rule, #ccc);&quot;&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Stage&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;What happens&lt;/th&gt;
&lt;th style=&quot;text-align: left; padding: 0.4em 0.8em;&quot;&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Tokenisation&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Raw text is split into tokens using BPE&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Sequence of token IDs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Embedding&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Token IDs are mapped to high-dimensional vectors&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Matrix of embedding vectors&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Positional encoding&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Position information is added to embeddings&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Position-aware embeddings&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Transformer blocks (x80-120)&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Multi-head attention + feed-forward, repeated&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Refined representations at each layer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Output projection&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Final layer representations projected to vocabulary size&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Logits (scores) for every token in vocabulary&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Softmax + sampling&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;Logits converted to probabilities, one token selected&lt;/td&gt;&lt;td style=&quot;padding: 0.3em 0.8em;&quot;&gt;The next token&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;Then the selected token is appended to the sequence and the process repeats from the transformer blocks onward (with the KV cache avoiding redundant computation for earlier tokens).&lt;/p&gt;

&lt;h3 id=&quot;scale-and-emergent-capabilities&quot;&gt;Scale and emergent capabilities&lt;/h3&gt;

&lt;p&gt;One of the most striking findings in LLM research is that capabilities emerge at scale. Smaller models can complete simple text. Larger models can follow instructions. Even larger models can perform multi-step reasoning, write complex code, and engage with nuanced arguments.&lt;/p&gt;

&lt;p&gt;These emergent abilities, capabilities that appear suddenly as models scale, rather than improving gradually, were characterised by &lt;a href=&quot;https://arxiv.org/abs/2206.07682&quot;&gt;Wei et al. (2022)&lt;/a&gt;. A model with 1 billion parameters might be unable to do basic arithmetic. A model with 10 billion might do simple addition. A model with 100 billion might do multi-digit multiplication. The capability doesn’t improve linearly with scale; it appears relatively abruptly.&lt;/p&gt;

&lt;p&gt;Whether “emergence” is a phase transition or an artefact of how we measure performance is debated (&lt;a href=&quot;https://arxiv.org/abs/2304.15004&quot;&gt;Schaeffer et al., 2023&lt;/a&gt; argue it’s partly the latter), but the practical observation is clear: larger models are not just slightly better; they’re qualitatively different in what they can do.&lt;/p&gt;

&lt;p&gt;The scaling laws described by &lt;a href=&quot;https://arxiv.org/abs/2001.08361&quot;&gt;Kaplan et al. (2020)&lt;/a&gt; and refined by &lt;a href=&quot;https://arxiv.org/abs/2203.15556&quot;&gt;Hoffmann et al. (2022)&lt;/a&gt; (the “Chinchilla” paper) established that model performance follows predictable power laws as a function of model size, dataset size, and compute. The Chinchilla paper’s key finding was that many models were trained on too little data relative to their size: a 70-billion-parameter model should be trained on roughly 1.4 trillion tokens, far more than was standard at the time.&lt;/p&gt;

&lt;h3 id=&quot;the-parameter-count&quot;&gt;The parameter count&lt;/h3&gt;

&lt;p&gt;When people talk about a “70B model” or a “400B model”, the B stands for billions of parameters: the learned weights in the model. These are the numbers that get adjusted during training. Every attention weight, every feed-forward weight, every embedding vector is a parameter.&lt;/p&gt;

&lt;p&gt;A 70-billion-parameter model stored in 16-bit floating point requires roughly 140 GB of memory just for the weights. And that’s before accounting for the memory needed when the model actually runs, what the industry calls &lt;label for=&quot;sn-writing-how-llms-actually-work-inference&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-inference-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;inference&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-inference&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-inference-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Inference&lt;/span&gt;Running a trained model to produce output – as opposed to training it.
&lt;/span&gt;, meaning the process of feeding in a prompt and generating a response. During inference, the model needs additional memory for the KV cache (a store of previously computed attention keys and values so it doesn’t have to recompute them for every new token), activations, and overhead. This is why running large models requires multiple GPUs.&lt;/p&gt;

&lt;p&gt;The cost of inference is substantial. Running a frontier model requires a cluster of high-end GPUs, typically NVIDIA A100s or H100s. A single H100 costs around US$30,000, and you need eight of them to run a 70B model (more for larger models). A cluster capable of serving a model like Claude or GPT-4 to millions of users costs tens of millions of dollars in hardware alone, before electricity, cooling, networking, and the engineering team to keep it running.&lt;/p&gt;

&lt;p&gt;This cost is what drives the per-token pricing you see from API providers. When Anthropic charges a fraction of a cent per token, that price reflects the amortised cost of the GPU cluster, the electricity to run it (a single H100 draws around 700 watts), the memory bandwidth consumed by the KV cache, and the engineering overhead. Input tokens are cheaper than output tokens because reading the prompt involves a single forward pass, while generating a response requires a separate forward pass for every token produced, each one computing attention across the full context. A long conversation with a frontier model might generate 2,000 output tokens. At each step, the model is attending to every previous token, which is why the cost scales with both the length of the input and the length of the output.&lt;/p&gt;

&lt;p&gt;For perspective: generating a 2,000-word response from a frontier model via API is typically &lt;em&gt;priced&lt;/em&gt; at between AU$0.05 and AU$0.50, depending on the model and the length of the input context. Note the word “priced”: what you pay and what it costs to serve are different things. The API price includes the provider’s margin, their amortised R&amp;amp;D costs (training a frontier model can cost US$100 million or more), and the overhead of running the platform. The actual compute cost of your individual request is a fraction of the price, but the infrastructure to serve millions of concurrent requests at low latency is what makes the price what it is. Providers are competing aggressively on pricing, and costs are falling, but the underlying economics remain a story about GPU memory, electricity, and how many tokens you can push through a chip per second.&lt;/p&gt;

&lt;p&gt;The parameters are where the model’s “knowledge” lives, encoded in the relationships between weights. A specific fact isn’t stored in a specific parameter; it’s distributed across millions of parameters in a way that makes it accessible when the correct pattern of activation occurs. This distributed representation is what makes it possible to store so much information in a relatively compact set of numbers, and it’s also what makes hallucination so difficult to prevent: you can’t just look up “is this fact correct?” in the model’s weights.&lt;/p&gt;

&lt;h3 id=&quot;chain-of-thought-and-reasoning&quot;&gt;Chain of thought and reasoning&lt;/h3&gt;

&lt;p&gt;A pure next-token predictor struggles with multi-step reasoning because each token is generated based on the full context but without any explicit “thinking” step. In 2022, &lt;a href=&quot;https://arxiv.org/abs/2201.11903&quot;&gt;Wei et al.&lt;/a&gt; showed that prompting models to “think step by step”, &lt;label for=&quot;sn-writing-how-llms-actually-work-chain-of-thought&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-how-llms-actually-work-chain-of-thought-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;chain-of-thought&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-how-llms-actually-work-chain-of-thought&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-how-llms-actually-work-chain-of-thought-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Chain-of-thought&lt;/span&gt;Prompting the model to write out its intermediate reasoning before giving a final answer – which empirically makes hard problems get answered better.
&lt;/span&gt; prompting, dramatically improves performance on reasoning tasks.&lt;/p&gt;

&lt;p&gt;This works because it gives the model more tokens in which to work through intermediate steps. Instead of jumping from question to answer in one step, the model generates its reasoning as text, and that text becomes part of the context for subsequent tokens. The model is using its own output as a scratchpad.&lt;/p&gt;

&lt;p&gt;This is less magical than it sounds. The model isn’t “thinking” in the way a human does. It’s producing text that follows the pattern of step-by-step reasoning, and each step constrains the next step in useful ways. But the practical effect is substantial: chain-of-thought prompting can improve accuracy on mathematical and logical reasoning tasks by 20-40 percentage points.&lt;/p&gt;

&lt;p&gt;More recent models have this behaviour built into their training. Claude, for instance, often works through problems step by step without being asked, because this pattern was reinforced during RLHF.&lt;/p&gt;

&lt;h3 id=&quot;what-about-the-future&quot;&gt;What about the future?&lt;/h3&gt;

&lt;p&gt;LLMs are improving fast. Context windows are expanding. Training data curation is becoming more sophisticated. New architectures (mixture-of-experts models, which activate only a subset of parameters for each token) are making larger models more efficient. Multimodal models that process text, images, and audio are becoming standard.&lt;/p&gt;

&lt;p&gt;But the fundamental architecture, transformers predicting the next token, has been remarkably stable since 2017. The improvements have come from scale, data quality, training techniques, and engineering, not from a radical rethinking of the approach.&lt;/p&gt;

&lt;p&gt;Whether this architecture has a ceiling (whether “predict the next token” can scale all the way to artificial general intelligence, or whether something fundamentally different is needed) is the most important open question in AI research. The optimists point to the steady improvement of scaling laws and the continued emergence of new capabilities. The sceptics point to the persistent failure modes (hallucination, poor arithmetic, brittleness to adversarial inputs) as evidence that statistical pattern matching has structural limits.&lt;/p&gt;

&lt;p&gt;Both sides might be right. LLMs might continue to improve dramatically while retaining certain categories of failure. They might become better at everything we need them for while still not “understanding” anything in the way humans do.&lt;/p&gt;

&lt;p&gt;For practical purposes, the answer to “how do LLMs work?” is: they read text as tokens, embed those tokens in high-dimensional space, use attention to relate tokens to each other across thousands of layers, and predict the next token from the resulting representation. The training process teaches them patterns that span syntax, semantics, logic, and world knowledge. The result is a system that can generate remarkably useful text while having no explicit model of truth, no persistent memory, and no understanding of why its outputs are correct when they are.&lt;/p&gt;

&lt;p&gt;That’s not a criticism; it’s a description. And understanding the description makes you better at using the tool: knowing when to trust it, when to verify, and when to reach for something else entirely.&lt;/p&gt;

&lt;h3 id=&quot;so-does-the-room-understand&quot;&gt;So does the room understand?&lt;/h3&gt;

&lt;p&gt;We opened this post with Searle’s Chinese Room: a person matching patterns without comprehension, producing outputs that look like understanding. Now you’ve seen the full machinery: tokens, embeddings, attention heads running in parallel, transformer blocks stacked a hundred layers deep, billions of parameters adjusted through gradient descent on trillions of tokens, reinforcement learning from human feedback, chain-of-thought reasoning, inference clusters burning megawatts of electricity. The room is vastly more complex than Searle imagined. But the question remains.&lt;/p&gt;

&lt;p&gt;The honest answer is: we don’t know. And the reason we don’t know exposes a deeper problem. We can’t define what “understanding” means precisely enough to test for it.&lt;/p&gt;

&lt;p&gt;When a child learns that fire is hot, is that understanding or pattern matching: touch fire, feel pain, don’t touch fire again? When a doctor diagnoses a rare disease from a cluster of symptoms, is that understanding or pattern matching against thousands of cases they’ve seen? When you catch a ball, are you solving differential equations or running a learned motor pattern? The boundary between “genuine understanding” and “very sophisticated pattern matching” is far blurrier than Searle’s thought experiment suggests.&lt;/p&gt;

&lt;p&gt;The question people really want answered, “is AI actually intelligent?”, runs into the same wall. We don’t have a rigorous definition. Alan Turing sidestepped it in 1950 with his &lt;a href=&quot;https://doi.org/10.1093/mind/LIX.236.433&quot;&gt;famous test&lt;/a&gt;: don’t ask whether the machine thinks, ask whether you can tell the difference. That’s pragmatic, not philosophical. The &lt;a href=&quot;https://plato.stanford.edu/entries/turing-test/&quot;&gt;Turing Test&lt;/a&gt; tells you about your ability to detect the difference, not about what’s happening inside.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.hup.harvard.edu/books/9780465025107&quot;&gt;Howard Gardner&lt;/a&gt; proposed that intelligence isn’t one thing; it’s at least eight (linguistic, logical-mathematical, spatial, musical, bodily-kinaesthetic, interpersonal, intrapersonal, naturalistic). LLMs are superhuman by some of those measures and non-functional by others. A system that writes better prose than most humans but can’t tell you whether a ball fits in a box is intelligent by one definition and not by another.&lt;/p&gt;

&lt;p&gt;The practical takeaway: stop asking “is it intelligent?” and start asking “is it useful for this specific task?” The Chinese Room might not understand Chinese, but if it answers your questions correctly, helps you write better code, and catches bugs you missed, does the philosophy matter? Searle would say yes. Your deploy pipeline doesn’t care.&lt;/p&gt;

&lt;p&gt;What I find most interesting is that the debate reveals more about the limits of our definitions than about the limits of the technology. We built something that defies our existing categories. It’s not intelligent the way humans are, and it’s not unintelligent the way a calculator is. It’s something else, and we’ll probably need new words before we can talk about it clearly.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Event Storming: Building Shared Understanding</title>
    <link href="/writing/event-storming-building-shared-understanding/"/>
    <updated>2026-03-24T06:00:00+08:00</updated>
    <id>/writing/event-storming-building-shared-understanding/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/from-chaos-to-clarity/&quot;&gt;From Chaos to Clarity&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;After four weeks of building the wrong thing, the Greenbox team knows they need a different approach. Lee’s advice was clear: get the domain out of Maya’s head and into shared understanding before anyone writes another line of code.&lt;/p&gt;

&lt;p&gt;The technique Lee recommends is Event Storming. It was created by Alberto Brandolini, and the premise is disarmingly simple: get everyone in a room, cover a wall in sticky notes, and map out how things actually work as a series of events.&lt;/p&gt;

&lt;p&gt;No code. No architecture diagrams. No user stories yet. Just: what happens, in what order, and where are the hard parts?&lt;/p&gt;

&lt;p&gt;It sounds almost too simple to be useful. That’s what Tom thinks when Maya suggests it. “We’re going to spend three hours sticking notes on a wall?” But the simplicity is the point. The sticky notes are a constraint that forces everyone to express ideas in small, concrete units. You can’t hide behind vague hand-waving when you have to write a specific event on a specific note.&lt;/p&gt;

&lt;h3 id=&quot;what-you-need&quot;&gt;What you need&lt;/h3&gt;

&lt;p&gt;Event Storming doesn’t require fancy tools or expensive facilitators. You need:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A long wall (or a very long roll of paper stuck to a wall)&lt;/li&gt;
  &lt;li&gt;Sticky notes in four colours (orange, blue, yellow, pink; Lee will explain what each means)&lt;/li&gt;
  &lt;li&gt;Markers, one per person, thick enough to read the notes from a distance&lt;/li&gt;
  &lt;li&gt;Everyone who matters in the room: developers, domain experts, product people, operations&lt;/li&gt;
  &lt;li&gt;Two to four hours of uninterrupted time&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;setting-up&quot;&gt;Setting up&lt;/h3&gt;

&lt;p&gt;Maya books the meeting room with the biggest wall. She grabs sticky notes from Officeworks: four packs, one of each colour. She invites the whole team: Tom, Priya, Jas, Sam. She also invites two of her farming contacts, Dave and Rachel, who she hopes will eventually supply Greenbox. They know the supply side in ways the team doesn’t.&lt;/p&gt;

&lt;p&gt;Dave Morrison arrives ten minutes early. He’s been to “workshops” before. The last one was run by a government agricultural adviser and produced a glossy brochure that nobody ever opened. He’s here because Maya asked personally, and because she grew up on a farm, and because that counts for something. He shakes Lee’s hand and eyes the wall of blank paper with the expression of a man who has seen a lot of fences built in the wrong paddock.&lt;/p&gt;

&lt;p&gt;Rachel, who runs a smaller mixed farm nearby, mentions her “dodgy broadband” when Lee hands her a marker. “Took me twenty minutes to load the map to get here,” she says. “Satellite internet. Works when it feels like it.” Nobody thinks much of it at the time.&lt;/p&gt;

&lt;p&gt;Seven people. One wall. Three hours blocked out on a Tuesday morning.&lt;/p&gt;

&lt;p&gt;Lee offered to facilitate, which helps enormously. The facilitator’s job isn’t to have domain knowledge; it’s to keep things moving, ask awkward questions, and make sure the quiet people get heard. You can run a session without a dedicated facilitator, but it’s harder. Someone inevitably gets sucked into the content and stops managing the process. If you can borrow someone who’s done it before, do.&lt;/p&gt;

&lt;h3 id=&quot;phase-one-chaos&quot;&gt;Phase one: chaos&lt;/h3&gt;

&lt;p&gt;Lee starts by explaining the format. He holds up the four colours of sticky note and runs through them quickly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Orange: things that happen, written in past tense. “Payment Submitted.” “Box Packed.” “Farm Confirmed Availability.” These are the backbone: the story of how the business works, told as a sequence of things that already happened. (Lee calls them “domain events,” but at this point nobody cares about the jargon. They’re just things that happen.)&lt;/li&gt;
  &lt;li&gt;Blue: decisions or actions that make those things happen. “Submit Payment.” “Pack Box.” Someone or something chose to do this. If orange is “what happened,” blue is “what triggered it.”&lt;/li&gt;
  &lt;li&gt;Yellow: who or what is involved. A customer clicking a button. A farmer calling with availability. A scheduled job that runs overnight. The people and systems in the story.&lt;/li&gt;
  &lt;li&gt;Pink: problems, questions, disagreements. Anything that makes someone say “wait, how does that work?” or “I thought it worked differently.” “These are the gold dust,” Lee says. “When you spot something that doesn’t make sense, or that two people disagree about, slap a pink note on it. Don’t try to resolve it now. Just mark it. We’ll get to pink notes later in the session; for now I just want you to know they exist.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“We’ll start with just orange,” Lee says. “Only events. Write each one in past tense on an orange note. Don’t worry about order. Don’t worry about getting it right. Just get everything out of your heads and onto the wall. Keep the pink notes in your pocket for now; we’ll come back to them.”&lt;/p&gt;

&lt;p&gt;He gives one important instruction: no talking during this phase. Just write and stick. Conversation comes later.&lt;/p&gt;

&lt;p&gt;He sets a timer for twenty minutes and says go.&lt;/p&gt;

&lt;p&gt;What follows is beautifully chaotic. Everyone grabs orange sticky notes and starts writing. Maya is writing rapidly: “Farm Listed Produce,” “Box Packed,” “Subscription Created,” “Weekly Menu Decided.” Tom writes “Payment Processed,” “Account Created,” “Subscription Cancelled.” Priya writes “Inventory Updated” and “Farm Onboarded.” Jas writes “Customer Signed Up” and “Box Previewed.” Sam writes “Delivery Scheduled” and “Customer Complained” (Sam always thinks about the operational realities).&lt;/p&gt;

&lt;p&gt;Dave, one of the farmers, writes “Harvest Confirmed,” “Surplus Reported,” and “Growing Schedule Committed.” Rachel hesitates over her next note, then writes “Crop Failed” quickly and sticks it on the wall without looking at it. She’s thinking about the 2019 frost that wiped out Dave’s entire tomato crop. Dave sees it go up and his jaw tightens, but he says nothing. Rachel also writes “Delivery Window Missed” and “Price Renegotiated.” These are events the team hadn’t considered at all. Nobody on the Greenbox team had thought about what happens on the farm before produce arrives at the packing facility.&lt;/p&gt;

&lt;p&gt;Priya notices Rachel writing “Crop Failed” and reaches for a pink note; she has questions. Lee catches her eye and taps his watch. “Good instinct. Hold that thought for the pink notes phase. Right now, just orange.” Priya nods and puts the pink note back, but she doesn’t forget the question.&lt;/p&gt;

&lt;p&gt;Within twenty minutes, there are about sixty orange sticky notes scattered across the wall in no particular order. Some are duplicates. Some contradict each other. “Payment Processed” and “Payment Confirmed” might be the same event, or they might not. “Customer Signed Up” and “Account Created” look like duplicates. That’s fine. That’s the point. The goal of this phase is volume, not precision.&lt;/p&gt;

&lt;h3 id=&quot;phase-two-the-timeline&quot;&gt;Phase two: the timeline&lt;/h3&gt;

&lt;p&gt;Lee gets everyone to step back and look at the wall. “Now let’s put these in order. Left to right, earliest to latest. Talk to each other. If you disagree about where something goes, that’s interesting; stick a pink note on it and we’ll come back to it.”&lt;/p&gt;

&lt;p&gt;This is where the real conversations start.&lt;/p&gt;

&lt;p&gt;Maya picks up “Farm Listed Produce” and puts it early on the timeline. Tom picks up “Customer Signed Up” and puts it at the start. Priya asks, “Which comes first? Do we need farms onboarded before customers can sign up, or can customers sign up before we have supply?”&lt;/p&gt;

&lt;p&gt;Maya pauses. “Good question. We need to know we can fulfil before we take subscriptions. So farm onboarding is first.”&lt;/p&gt;

&lt;p&gt;Tom didn’t know that. He’d been building the subscription system in isolation, assuming customers came first. One sticky note conversation, and an assumption is surfaced and resolved.&lt;/p&gt;

&lt;p&gt;But Lee notices a pattern forming. Maya is the one placing notes with confidence. Everyone else is asking, deferring, moving on. The pink notes, the disagreements, aren’t appearing.&lt;/p&gt;

&lt;p&gt;This is exactly what Event Storming is supposed to prevent. If one person places all the notes and nobody disagrees, you haven’t built shared understanding. You’ve just transferred one person’s mental model onto a wall. The whole point of getting everyone in the room is to surface the places where people see the domain differently. No pink notes doesn’t mean there are no disagreements. It means the disagreements are hidden, buried under politeness, deference, or the assumption that the founder must be right. Those hidden disagreements don’t go away. They become bugs, missed requirements, and late-night arguments in sprint three.&lt;/p&gt;

&lt;p&gt;He tries a direct prompt. “Tom, challenge one of these. Is there a note that might be in the wrong place?”&lt;/p&gt;

&lt;p&gt;Tom glances at the wall. “Looks right to me. Maya knows the farming side better than I do.”&lt;/p&gt;

&lt;p&gt;Lee changes tactic. He walks over to Dave. “Walk the supply side of this timeline with Tom. Tell him what actually happens on a farm between committing produce and it arriving at the packing facility.”&lt;/p&gt;

&lt;p&gt;Dave pulls “Farm Listed Produce” off the wall and holds it at arm’s length. “This makes it sound like I sit down on a Monday and know what I’ve got. I don’t. I can tell you what I’ll &lt;em&gt;probably&lt;/em&gt; have. But the weather, the pests, the truck, anything changes it between now and Thursday.”&lt;/p&gt;

&lt;p&gt;Tom stares at the note. “So the data model can’t treat supply as definite. It’s more like a forecast?”&lt;/p&gt;

&lt;p&gt;“Now you’re talking,” Dave says. And now there are pink notes.&lt;/p&gt;

&lt;p&gt;There’s a brief tangent about whether “Payment Submitted” and “Payment Confirmed” are the same event. Tom explains they’re not: one is the customer clicking “pay,” the other is Stripe confirming the charge went through. A payment can be submitted and then fail. Maya hadn’t thought about that. Priya makes a note that they’ll need to handle failed payments, another pink note for the wall.&lt;/p&gt;

&lt;p&gt;The duplicates get merged. “Customer Signed Up” and “Account Created” collapse into a single event. “Growing Schedule Committed” gets moved to a parallel swim lane because it happens on a different timeline to the customer flow. The wall starts to take shape.&lt;/p&gt;

&lt;p&gt;The team works through the timeline together. After thirty minutes of shuffling, arguing, and clarifying, a rough sequence emerges:&lt;/p&gt;

&lt;link href=&quot;https://fonts.googleapis.com/css2?family=Kalam:wght@400;700&amp;amp;display=swap&quot; rel=&quot;stylesheet&quot; /&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1680 420&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Kalam&apos;, &apos;Segoe Print&apos;, &apos;Comic Sans MS&apos;, cursive;&quot; role=&quot;img&quot; aria-label=&quot;The Greenbox domain after Event Storming: eighteen orange events grouped into four clusters — Farm Onboarding, Subscription, Supply Matching, and Fulfilment — in rough chronological order from left to right.&quot;&gt;
  &lt;defs&gt;
    &lt;filter id=&quot;wobble-su&quot; x=&quot;-5%&quot; y=&quot;-5%&quot; width=&quot;110%&quot; height=&quot;110%&quot;&gt;
      &lt;feTurbulence type=&quot;fractalNoise&quot; baseFrequency=&quot;0.02&quot; numOctaves=&quot;2&quot; seed=&quot;13&quot; result=&quot;n&quot; /&gt;
      &lt;feDisplacementMap in=&quot;SourceGraphic&quot; in2=&quot;n&quot; scale=&quot;2&quot; /&gt;
    &lt;/filter&gt;
    &lt;style&gt;
      .su-sticky { stroke: #1a1a1a; stroke-width: 1.8; filter: url(#wobble-su); }
      .su-event { fill: #ffb84d; }
      .su-cluster-label { font-size: 14px; font-weight: 700; fill: #4a4540; letter-spacing: 0.04em; text-transform: uppercase; }
      .su-cluster-sub { font-size: 11px; fill: #6b6560; font-style: italic; }
      .su-event-text { font-size: 12px; fill: #1a1a1a; }
      .su-caption { font-size: 13px; fill: #4a4540; font-style: italic; }
    &lt;/style&gt;
  &lt;/defs&gt;

  &lt;text x=&quot;20&quot; y=&quot;70&quot; class=&quot;su-cluster-label&quot;&gt;Farm Onboarding&lt;/text&gt;
  &lt;text x=&quot;20&quot; y=&quot;86&quot; class=&quot;su-cluster-sub&quot;&gt;one-time per farm&lt;/text&gt;
  &lt;g transform=&quot;translate(200, 50)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Farm Onboarded&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(440, 50)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Growing Schedule Committed&lt;/text&gt;&lt;/g&gt;

  &lt;text x=&quot;20&quot; y=&quot;150&quot; class=&quot;su-cluster-label&quot;&gt;Subscription&lt;/text&gt;
  &lt;text x=&quot;20&quot; y=&quot;166&quot; class=&quot;su-cluster-sub&quot;&gt;one-time per customer&lt;/text&gt;
  &lt;g transform=&quot;translate(200, 130)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Landing Page Visited&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(440, 130)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Box Selected&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(680, 130)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Payment Submitted&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(920, 130)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Payment Confirmed&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1160, 130)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Subscription Created&lt;/text&gt;&lt;/g&gt;

  &lt;text x=&quot;20&quot; y=&quot;230&quot; class=&quot;su-cluster-label&quot;&gt;Supply Matching&lt;/text&gt;
  &lt;text x=&quot;20&quot; y=&quot;246&quot; class=&quot;su-cluster-sub&quot;&gt;weekly&lt;/text&gt;
  &lt;g transform=&quot;translate(200, 210)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Produce Listed&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(440, 210)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Supply Aggregated&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(680, 210)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Supply Matched to Demand&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(920, 210)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Shortfall Identified&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1160, 210)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Substitution Decided&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1400, 210)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Box Contents Finalised&lt;/text&gt;&lt;/g&gt;

  &lt;text x=&quot;20&quot; y=&quot;310&quot; class=&quot;su-cluster-label&quot;&gt;Fulfilment&lt;/text&gt;
  &lt;text x=&quot;20&quot; y=&quot;326&quot; class=&quot;su-cluster-sub&quot;&gt;weekly&lt;/text&gt;
  &lt;g transform=&quot;translate(200, 290)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Delivery Scheduled&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(440, 290)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Box Packed&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(680, 290)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Box Dispatched&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(920, 290)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Box Delivered&lt;/text&gt;&lt;/g&gt;
  &lt;g transform=&quot;translate(1160, 290)&quot;&gt;&lt;rect width=&quot;220&quot; height=&quot;40&quot; rx=&quot;3&quot; class=&quot;su-sticky su-event&quot; /&gt;&lt;text x=&quot;110&quot; y=&quot;26&quot; text-anchor=&quot;middle&quot; class=&quot;su-event-text&quot;&gt;Feedback Received&lt;/text&gt;&lt;/g&gt;

  &lt;text x=&quot;840&quot; y=&quot;380&quot; text-anchor=&quot;middle&quot; class=&quot;su-caption&quot;&gt;Eighteen orange events across four clusters, in rough chronological order from left to right.&lt;/text&gt;
  &lt;text x=&quot;840&quot; y=&quot;400&quot; text-anchor=&quot;middle&quot; class=&quot;su-caption&quot;&gt;The one-time clusters (top) set the conditions; the weekly clusters (bottom) are the business.&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;Eighteen events across four clusters. That’s the core of Greenbox, from farm onboarding to customer feedback. It took the group about an hour to get here, and already the room feels different. Everyone can see the same picture.&lt;/p&gt;

&lt;p&gt;Notice the structure that’s emerged. The one-time events (farm onboarding, customer signup, payment) are the scaffolding. They happen once and create the conditions for everything else. The recurring events (weekly supply matching, packing, delivery) are the business. They repeat every week for as long as farms supply and customers stay subscribed. Tom is already thinking about how this affects the data model.&lt;/p&gt;

&lt;h3 id=&quot;phase-three-commands-and-actors&quot;&gt;Phase three: commands and actors&lt;/h3&gt;

&lt;p&gt;Lee hands out blue and yellow sticky notes. “For each event, let’s figure out what triggers it. Write the command on a blue note, and who or what performs the command on a yellow note.”&lt;/p&gt;

&lt;p&gt;This phase goes faster because the timeline provides structure. But it surfaces new questions.&lt;/p&gt;

&lt;p&gt;“Who decides substitutions?” Jas asks, placing a blue “Decide Substitution” note next to “Substitution Decided.”&lt;/p&gt;

&lt;p&gt;“I do,” Maya says. “For now, anyway. Eventually maybe an algorithm, but right now it’s judgement. You need to know the produce; you can’t just swap beetroot for lettuce.”&lt;/p&gt;

&lt;p&gt;Tom had assumed substitutions would be automatic. He was planning a simple algorithm: if item A is unavailable, pick the next cheapest item in the same category. Maya is telling him that’s not how it works at all. The substitution logic is a core part of the value proposition, and it requires domain expertise.&lt;/p&gt;

&lt;p&gt;Pink sticky note goes on the wall: “Substitution policy: who decides, and how?”&lt;/p&gt;

&lt;p&gt;Sam asks another question: “Who dispatches the boxes? Us, or a courier?”&lt;/p&gt;

&lt;p&gt;Maya says, “We’ll use a local courier for now, but eventually I want our own drivers. The delivery experience matters.”&lt;/p&gt;

&lt;p&gt;Sam writes a pink note: “Delivery logistics: own drivers vs courier, and when do we switch?”&lt;/p&gt;

&lt;p&gt;The actor layer reveals something interesting about “Supply Aggregated.” Who does the aggregating? Right now it would be Maya, manually checking what each farm has submitted. But with ten farms, that’s manageable. With fifty, it’s a full-time job. The yellow note says “Maya” but really it should say “System,” eventually. Another pink note: “When does supply aggregation need to be automated?”&lt;/p&gt;

&lt;p&gt;Priya points to the bracket the team added during the ordering phase, the one marking where the weekly cycle starts. “Every actor from here onwards is doing something every week,” she says. “But the yellow notes don’t show that. Maya doesn’t aggregate supply once. She does it every Wednesday, for every box.” The actor layer makes the repeating workload visible in a way the event timeline alone didn’t, and with it, the bottlenecks. If one person’s name appears on five weekly events, that’s a scaling problem waiting to happen.&lt;/p&gt;

&lt;h3 id=&quot;phase-four-hotspots&quot;&gt;Phase four: hotspots&lt;/h3&gt;

&lt;p&gt;By now the wall is covered. Orange notes tracing what happens, in order. Blue notes beneath them showing what triggers each step. Yellow notes above showing who’s involved. And scattered across the whole thing, pink notes marking every question, disagreement, and “wait, how does that actually work?”&lt;/p&gt;

&lt;p&gt;Lee gathers everyone around the hotspots. “These pink notes are the most valuable thing on the wall. Every one of them is a misunderstanding you caught before it became a bug, a wrong assumption, or a wasted sprint.”&lt;/p&gt;

&lt;figure style=&quot;margin: var(--space-md) 0; text-align: center;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 1240 170&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Kalam&apos;, &apos;Segoe Print&apos;, &apos;Comic Sans MS&apos;, cursive;&quot; role=&quot;img&quot; aria-label=&quot;Four pink hotspot sticky notes capturing the biggest clusters of unresolved questions from the Greenbox session: supply shortfalls, substitution policy, delivery logistics, and seasonal availability.&quot;&gt;
  &lt;defs&gt;
    &lt;filter id=&quot;wobble-su2&quot; x=&quot;-5%&quot; y=&quot;-5%&quot; width=&quot;110%&quot; height=&quot;110%&quot;&gt;
      &lt;feTurbulence type=&quot;fractalNoise&quot; baseFrequency=&quot;0.02&quot; numOctaves=&quot;2&quot; seed=&quot;17&quot; result=&quot;n&quot; /&gt;
      &lt;feDisplacementMap in=&quot;SourceGraphic&quot; in2=&quot;n&quot; scale=&quot;2&quot; /&gt;
    &lt;/filter&gt;
    &lt;style&gt;
      .su2-sticky { stroke: #1a1a1a; stroke-width: 1.8; filter: url(#wobble-su2); }
      .su2-hotspot { fill: #f4a6c0; }
      .su2-title { font-size: 12px; font-weight: 700; fill: #1a1a1a; text-transform: uppercase; letter-spacing: 0.05em; }
      .su2-text { font-size: 12px; fill: #1a1a1a; font-style: italic; }
      .su2-caption { font-size: 12px; fill: #4a4540; font-style: italic; }
    &lt;/style&gt;
  &lt;/defs&gt;

  &lt;g transform=&quot;translate(20, 20)&quot;&gt;
    &lt;rect width=&quot;280&quot; height=&quot;90&quot; rx=&quot;3&quot; class=&quot;su2-sticky su2-hotspot&quot; /&gt;
    &lt;text x=&quot;140&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;su2-title&quot;&gt;Supply shortfalls&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;52&quot; text-anchor=&quot;middle&quot; class=&quot;su2-text&quot;&gt;What happens when farms&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot; class=&quot;su2-text&quot;&gt;can&apos;t deliver enough?&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(320, 20)&quot;&gt;
    &lt;rect width=&quot;280&quot; height=&quot;90&quot; rx=&quot;3&quot; class=&quot;su2-sticky su2-hotspot&quot; /&gt;
    &lt;text x=&quot;140&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;su2-title&quot;&gt;Substitution policy&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;52&quot; text-anchor=&quot;middle&quot; class=&quot;su2-text&quot;&gt;Who decides,&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot; class=&quot;su2-text&quot;&gt;using what criteria?&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(620, 20)&quot;&gt;
    &lt;rect width=&quot;280&quot; height=&quot;90&quot; rx=&quot;3&quot; class=&quot;su2-sticky su2-hotspot&quot; /&gt;
    &lt;text x=&quot;140&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;su2-title&quot;&gt;Delivery logistics&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;52&quot; text-anchor=&quot;middle&quot; class=&quot;su2-text&quot;&gt;Own drivers vs courier?&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot; class=&quot;su2-text&quot;&gt;When to switch?&lt;/text&gt;
  &lt;/g&gt;

  &lt;g transform=&quot;translate(920, 20)&quot;&gt;
    &lt;rect width=&quot;280&quot; height=&quot;90&quot; rx=&quot;3&quot; class=&quot;su2-sticky su2-hotspot&quot; /&gt;
    &lt;text x=&quot;140&quot; y=&quot;24&quot; text-anchor=&quot;middle&quot; class=&quot;su2-title&quot;&gt;Seasonal availability&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;52&quot; text-anchor=&quot;middle&quot; class=&quot;su2-text&quot;&gt;How do we handle gaps&lt;/text&gt;
    &lt;text x=&quot;140&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot; class=&quot;su2-text&quot;&gt;between growing seasons?&lt;/text&gt;
  &lt;/g&gt;

  &lt;text x=&quot;620&quot; y=&quot;150&quot; text-anchor=&quot;middle&quot; class=&quot;su2-caption&quot;&gt;The four biggest pink-hotspot clusters — each one a question the team caught before it became a sprint of wasted work.&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;The team counts twelve pink notes. The four biggest clusters:&lt;/p&gt;

&lt;p&gt;Supply shortfalls. What happens when total farm supply doesn’t cover subscriber demand for the week? Rachel explains that this is completely normal in farming. “You think you’ll have twenty crates of zucchini, then the slugs get in.” Dave adds that some farms will over-promise because they don’t want to lose the contract. “We’ve all done it,” he says. “You say yes and hope the crop comes through. Sometimes it doesn’t.”&lt;/p&gt;

&lt;p&gt;The team needs a process for handling shortfalls, and it needs to be baked into the weekly cycle, not treated as an exception. This is a design decision that affects everything: the commitment deadline for farms, the buffer stock policy, the customer communication if a box has fewer items than expected.&lt;/p&gt;

&lt;p&gt;&lt;span id=&quot;substitution-hotspot&quot;&gt;&lt;/span&gt;Substitution policy. This one sparks the longest argument of the session. Tom thought boxes had fixed contents: the same items every week, based on what the customer selected at signup. Jas thought customers picked individual items each week, like a supermarket order. Maya says neither is right. The box contents change weekly based on what’s available, and the &lt;em&gt;curation&lt;/em&gt; is Greenbox’s differentiator. The customer doesn’t choose. They trust Greenbox to choose well.&lt;/p&gt;

&lt;p&gt;Three people, three completely different mental models. If the team had kept building without this conversation, they’d have shipped three different products.&lt;/p&gt;

&lt;p&gt;Delivery logistics. Sam raises the practical questions nobody else had thought about. What’s the delivery window? What happens if nobody’s home? Who handles complaints about damaged produce? Can customers change their delivery day? Is there a minimum order density per area to make delivery economical? None of these have answers yet, and every one of them affects the software.&lt;/p&gt;

&lt;p&gt;&lt;span id=&quot;seasonal-hotspot&quot;&gt;&lt;/span&gt;Seasonal availability gaps. Rachel explains something the team hadn’t considered at all. In Western Australia, summer is abundant, but late winter is lean: fewer varieties, smaller yields, and some crops just don’t grow. What does Greenbox do during those weeks? Pause subscriptions? Source from further afield and compromise on the local promise? Offer a reduced box at a lower price? This is a business model question disguised as a supply chain problem.&lt;/p&gt;

&lt;p&gt;The remaining hotspots are smaller but still important: how do farms get paid, what happens when a customer wants to skip a week, how is feedback collected and acted on, what are the deadlines for each step in the weekly cycle. None of them are show-stoppers individually, but together they represent the operational complexity that nobody had mapped before today.&lt;/p&gt;

&lt;h3 id=&quot;the-arguments-are-the-point&quot;&gt;The arguments are the point&lt;/h3&gt;

&lt;p&gt;About ninety minutes into the session, Tom and Maya have a proper disagreement. Tom is placing the “Supply Matched to Demand” event and says, “So the system automatically allocates produce to boxes based on the subscription sizes?”&lt;/p&gt;

&lt;p&gt;Maya shakes her head. “No. I look at what’s come in from the farms, I think about what makes a good combination, and I decide what goes in each box size. It’s not just weight and price matching. A box needs to make sense as a meal plan for the week.”&lt;/p&gt;

&lt;p&gt;Tom looks frustrated. He’s been building things for twelve years. He can hear the problem being described, and his instinct is to solve it with code. That’s what he does, that’s who he is. Being told that the answer is “Maya decides” feels like being told the problem isn’t worth solving properly. “So there’s no algorithm? You just… decide?”&lt;/p&gt;

&lt;p&gt;“For now, yes. The algorithm is my brain.” Tom says nothing, but the thought flickers: the substitution logic “could be automated eventually.” Maya catches his expression. “Eventually,” she says, with a weight that closes the topic for now.&lt;/p&gt;

&lt;p&gt;Lee steps in. “This is great. Put a pink note on it. The question is: can this scale? And if not, what does the handover from Maya-decides to system-decides look like?”&lt;/p&gt;

&lt;p&gt;This is exactly the kind of conversation that Event Storming is designed to provoke. The argument isn’t a problem; it’s the discovery working. Tom now understands that the matching process is far more nuanced than he assumed. Maya now understands that if they want to scale, they’ll eventually need to codify her decision-making process. Both of those insights are worth the entire session.&lt;/p&gt;

&lt;p&gt;Priya has been staring at the timeline. She traces it with her finger: “Supply Matched to Demand” on Tuesday, then “Box Contents Decided,” then “Box Packed,” then… she stops. “Where does payment happen?”&lt;/p&gt;

&lt;p&gt;Tom points to the left end of the wall. “Payment Submitted” is near “Customer Signed Up,” right at the beginning. That’s how he built it, &lt;a href=&quot;/writing/retrospectives-catching-the-wrong-kind-of-fast/&quot;&gt;charge on signup&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Priya shakes her head slowly. “But the box contents change every week. The cost depends on what’s in the box, and we don’t know what’s in the box until Tuesday evening when Maya finishes the matching. If we charge at signup, we’re charging for a box whose contents aren’t known yet.”&lt;/p&gt;

&lt;p&gt;The room goes quiet. Tom stares at the wall. She’s right. The entire payment flow he built assumes a fixed price at a fixed time. But the timeline on the wall makes it obvious: billing can’t happen until after supply matching. The data model, the Stripe integration, the receipt emails: all of it is in the wrong place.&lt;/p&gt;

&lt;p&gt;&lt;span id=&quot;billing-timing&quot;&gt;&lt;/span&gt;Pink sticky note: “Billing point: must be after supply matching, not at signup.”&lt;/p&gt;

&lt;p&gt;It’s one of those moments where the wall shows you something that no amount of code review would have caught. The billing architecture isn’t a technical decision; it’s a domain decision, visible only when you see the full sequence of events.&lt;/p&gt;

&lt;p&gt;There’s a quieter but equally important moment when Jas admits she’d been designing the customer experience around item selection. “I thought the whole point was letting customers choose,” she says. “Like a farmers’ market online.” Maya gently corrects her: the point is the &lt;em&gt;opposite&lt;/em&gt; of choosing. Customers are busy. They don’t want to browse and pick. They want to open their door and find a box of good stuff.&lt;/p&gt;

&lt;p&gt;Jas pauses. She’s been designing the wrong product for four weeks and nobody told her. She can feel the heat rising in her face. Then something clicks. “That actually changes everything about the landing page. The value proposition isn’t choice; it’s trust.”&lt;/p&gt;

&lt;p&gt;Nobody told Jas she was wrong at any point during her first two weeks. She’d been designing in good faith based on an assumption that nobody thought to challenge. Event Storming created the space for that challenge to happen naturally, without blame.&lt;/p&gt;

&lt;h3 id=&quot;what-emerged&quot;&gt;What emerged&lt;/h3&gt;

&lt;p&gt;By the end of three hours, the wall tells a story that nobody in the room could have told alone. Maya knew the farming side but hadn’t thought through the software implications. Tom and Priya understood the technical constraints but had wrong assumptions about the domain. Jas had been designing for a product that doesn’t exist. Sam had operational questions that nobody else had considered.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Before the session&lt;/th&gt;
      &lt;th&gt;After three hours on the wall&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;The domain lived in Maya’s head, and nobody else could build without asking her&lt;/td&gt;
      &lt;td&gt;18 domain events on the wall, visible to everyone. The team shares one picture instead of five different guesses.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Tom assumed box contents were fixed. Jas assumed customers chose items. Neither knew they were wrong.&lt;/td&gt;
      &lt;td&gt;Three fundamental misunderstandings surfaced and resolved: box contents vary weekly, customers don’t choose, farm onboarding comes before subscriptions.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Priya had a list of unanswered questions about the farm portal and no way to get them answered&lt;/td&gt;
      &lt;td&gt;12 pink hotspot notes, each one a question the team caught before it became a bug or a wasted sprint. Priya’s questions are now on the wall where everyone can see them.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Jas had designed a customisation interface for a product that doesn’t work that way&lt;/td&gt;
      &lt;td&gt;The value proposition is trust, not choice. Jas now knows what to design for.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Sam’s operational concerns (delivery logistics, courier contracts, customer complaints) hadn’t been heard by the developers&lt;/td&gt;
      &lt;td&gt;Sam’s events are on the wall alongside Tom’s and Priya’s. Operations is part of the domain, not an afterthought.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;what-the-session-bought&quot;&gt;What the session bought&lt;/h3&gt;

&lt;p&gt;The session resolved in three hours what would have taken weeks to discover through code. Every one of those twelve hotspots was a potential sprint of wasted work. The fundamental disagreement about box contents alone would have caused a full rewrite if discovered in production.&lt;/p&gt;

&lt;p&gt;And it’s not just about avoiding waste. Tom now knows the subscription model needs variable weekly contents. Priya knows about commitment deadlines and shortfall reporting. Jas is designing for trust, not choice. Sam has a list of logistics questions that need answers. Everyone is building toward the same product because they all stood in front of the same wall.&lt;/p&gt;

&lt;h3 id=&quot;facilitation-matters&quot;&gt;Facilitation matters&lt;/h3&gt;

&lt;p&gt;A few things Lee did that made the session work:&lt;/p&gt;

&lt;p&gt;He enforced the no-talking rule in phase one. When people talk too early, the loudest voices dominate and the quieter participants defer. The silent writing phase gives everyone equal weight. Dave and Rachel, who might have felt like outsiders in a tech team’s meeting, produced some of the most important events because they were writing, not competing for airtime.&lt;/p&gt;

&lt;p&gt;He kept asking “what happens next?” and “what could go wrong?” These two questions drive the entire session. “What happens next?” extends the timeline. “What could go wrong?” generates hotspots. The second question is the more valuable one, because it forces the group to think about the unhappy paths, and that’s where most of the domain complexity lives.&lt;/p&gt;

&lt;p&gt;He didn’t let anyone open a laptop. The moment someone starts Googling or checking Slack, they’re mentally out of the room. Event Storming works because everyone is physically engaged with the wall, moving sticky notes, pointing, arguing. Screens kill that energy.&lt;/p&gt;

&lt;p&gt;He adjusted when his first approach didn’t work. When Lee tried to get Tom to challenge the timeline directly, Tom deferred to Maya. So Lee paired Dave with Tom instead, putting a domain expert next to a developer and asking them to find contradictions. The best facilitation is adaptive, not scripted.&lt;/p&gt;

&lt;p&gt;He time-boxed ruthlessly. Three hours is enough for a first pass. After three hours people are tired and the returns diminish. Better to photograph the wall, take a break, and come back for a deeper session on the hotspots if needed. The wall isn’t going anywhere.&lt;/p&gt;

&lt;h3 id=&quot;when-to-use-event-storming&quot;&gt;When to use Event Storming&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;You’re entering an unfamiliar domain. If your team doesn’t deeply understand the business process, you need to surface that understanding before you build. Most domains are more complex than they first appear, and the complexity hides in the parts you don’t think to ask about.&lt;/li&gt;
  &lt;li&gt;You’re kicking off a new project. Even if individual team members understand parts of the domain, they probably don’t share a mental model. Event Storming builds that shared model explicitly.&lt;/li&gt;
  &lt;li&gt;You have access to domain experts. The technique depends on having people in the room who know how things actually work. Dave and Rachel’s farming knowledge was essential to the Greenbox session.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;when-not-to-use-it&quot;&gt;When not to use it&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;The feature is small and well-understood. Adding a password reset flow doesn’t need a three-hour workshop.&lt;/li&gt;
  &lt;li&gt;You don’t have the right people available. Running Event Storming without domain experts is just developers guessing together. That’s worse than useless: it creates false confidence.&lt;/li&gt;
  &lt;li&gt;The team already shares a strong mental model. If everyone genuinely agrees on how things work, Event Storming will confirm what you know without adding much. The danger is that “we all understand it” is often an untested assumption, though sometimes it’s genuinely true.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-happens-next&quot;&gt;What happens next&lt;/h3&gt;

&lt;p&gt;Maya photographs the wall: five panoramic shots that she’ll have laminated the following week. Sam volunteers to transcribe the events and hotspots into a shared document. Tom, who was sceptical about spending three hours not coding, admits he’s glad they did it. “I would have spent a week building automated substitution logic. That would have been completely wrong.”&lt;/p&gt;

&lt;p&gt;Lee and Dave walk to the car park together. Dave pulls his keys out of a jacket that’s seen a decade of Margaret River weather. “That wasn’t as bad as I expected.”&lt;/p&gt;

&lt;p&gt;“High praise from a farmer.”&lt;/p&gt;

&lt;p&gt;Dave pauses by his ute. “The thing about crop failures. That’s not a what-if for us. That’s a Tuesday.”&lt;/p&gt;

&lt;p&gt;“I know,” Lee says. “That’s why you needed to be in the room.”&lt;/p&gt;

&lt;p&gt;Back inside, the team stands in front of the wall, arms folded. Twelve hotspots, eighteen core events, dozens of questions. Tom’s face says &lt;em&gt;where do we even start?&lt;/em&gt; Priya is reading the pink notes methodically. Sam is counting them.&lt;/p&gt;

&lt;p&gt;“Pick the one that scares you most,” Lee says.&lt;/p&gt;

&lt;p&gt;Maya doesn’t hesitate. She reaches for the pink note from the substitution cluster. &lt;em&gt;Substitution policy: who decides, and how?&lt;/em&gt; The question that cost Tom two rewrites in week one. The question that sits at the heart of what makes Greenbox different from a supermarket delivery.&lt;/p&gt;

&lt;p&gt;“Good,” Lee says. “Now let’s make it concrete. Rules. Examples. Edge cases. Twenty-five minutes and four colours of card.”&lt;/p&gt;

&lt;p&gt;Tom groans. “More sticky notes?”&lt;/p&gt;

&lt;p&gt;“Cards, actually.” Lee is already pulling a fresh pack from his bag.&lt;/p&gt;

&lt;p&gt;After the session, Jas catches Maya in the kitchen. She’s been on a two-day-a-week contract (Maya’s “we don’t need a designer yet” arrangement) and she’s supposed to be in again on Thursday and then gone until next week.&lt;/p&gt;

&lt;p&gt;“I don’t want to do two days anymore,” Jas says.&lt;/p&gt;

&lt;p&gt;Maya’s face falls. She’s already thinking about finding another designer, about the landing page redesign, about all the trust-not-choice work that just landed on the wall.&lt;/p&gt;

&lt;p&gt;“I want to do five,” Jas says.&lt;/p&gt;

&lt;p&gt;Maya is quiet for a moment. The seed money landed a few weeks ago (Angela’s first tranche, $75K) and it’s already stretched across Priya’s salary, Sam’s reduced-but-no-longer-catastrophic pay, and the cafe-office lease. Tom is on equity and a token wage. The budget doesn’t have a full-time designer in it. But it also didn’t have a two-day-a-week contractor in it until Sam talked her into it, and she found the money for that.&lt;/p&gt;

&lt;p&gt;“I can’t match your contract rate,” Maya says. “Not even close. I can do a salary, startup salary, which means it’ll be less per week than you’re making now for two days. But I can offer equity. A small stake, vesting over two years. If this works, it’s worth something. If it doesn’t, you’ve taken a pay cut for a startup that folded.”&lt;/p&gt;

&lt;p&gt;Jas has done the maths already. She was doing it during the session, while the sticky notes were going up and the arguments were flying. Two days a week at contractor rates is safe money. Five days a week at a startup salary is less money and more risk. But she’s twenty-six, her rent in Leederville is manageable, she doesn’t have a mortgage, and she’s just spent three hours in a room where she understood for the first time what she’d actually be designing.&lt;/p&gt;

&lt;p&gt;“I want the equity in writing,” Jas says. “And I want to be in the room for product decisions. Not briefed after.”&lt;/p&gt;

&lt;p&gt;“That’s fair,” Maya says. “I should have had you in the room from the start.”&lt;/p&gt;

&lt;p&gt;They shake on it in the kitchen of a cafe-office in Fremantle, next to a kettle that takes four minutes to boil and a jar of instant coffee that nobody likes but everybody drinks.&lt;/p&gt;

&lt;p&gt;Jas goes home to her Leederville flat that evening and opens her Moleskine to the page where she’d been sketching the customisation flow, the dead one, the one nobody told her about. She turns to a fresh page and writes &lt;em&gt;trust, not choice&lt;/em&gt; at the top. Underneath, she starts sketching a landing page that sells the feeling of opening your front door and finding dinner sorted. Her grandmother’s market garden in the Adelaide Hills. Grow what they actually want.&lt;/p&gt;

&lt;p&gt;She fills three pages before she looks up.&lt;/p&gt;

&lt;p&gt;Lee calls the technique &lt;a href=&quot;/writing/example-mapping-making-stories-concrete/&quot;&gt;Example Mapping&lt;/a&gt;. Twenty-five minutes, four colours of card, and a vague story becomes something you can actually build.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Retrospectives: Catching the Wrong Kind of Fast</title>
    <link href="/writing/retrospectives-catching-the-wrong-kind-of-fast/"/>
    <updated>2026-03-17T06:00:00+08:00</updated>
    <id>/writing/retrospectives-catching-the-wrong-kind-of-fast/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/from-chaos-to-clarity/&quot;&gt;From Chaos to Clarity&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;/writing/customer-discovery-before-the-first-line-of-code/&quot;&gt;seed round&lt;/a&gt; closed last week. They were officially a company now, with an office above a cafe in Fremantle and $75K in the bank. Angela’s $150K investment came in two tranches: half now, half when they hit 200 active subscribers. Miss the milestone and the second tranche doesn’t release, leaving them with whatever runway remains from the first. At their current burn rate, that meant about three months after the milestone deadline to either hit it late, find other funding, or wind down. The clock was real.&lt;/p&gt;

&lt;p&gt;The seed money bought two things the team needed badly. Priya (29, quiet, precise, recently moved to Perth from Melbourne, her first startup) joined as the second developer. And they finally had enough runway that Maya, Tom, and Sam could stop treating this as a side project and start treating it as a job.&lt;/p&gt;

&lt;p&gt;They didn’t have a designer yet. Maya handled the brand herself, badly. Tom built the UI, also badly. That was fine for now. Design could wait. The &lt;a href=&quot;/writing/minimum-viable-product-the-first-box/&quot;&gt;first boxes&lt;/a&gt; had gone out to 38 pilot subscribers and the feedback was good. The produce was excellent, the delivery logistics were shaky, and the sign-up process was held together with sticky tape and a Google Form. It worked. It wouldn’t scale.&lt;/p&gt;

&lt;p&gt;Maya’s pitch to the team on Monday morning was simple: “We’ve proved people want this. Now we need to build it properly. Farms list what they have each week. Customers subscribe to a box size. We match supply to demand, pack the boxes, and deliver. Let’s build the software that makes it real.”&lt;/p&gt;

&lt;p&gt;Sounds straightforward. The team gets to work.&lt;/p&gt;

&lt;h3 id=&quot;week-one&quot;&gt;Week one&lt;/h3&gt;

&lt;p&gt;The output is incredible. Tom has Claude open in one tab and his IDE in the other. He describes the subscription model he wants, and the &lt;label for=&quot;sn-writing-retrospectives-catching-the-wrong-kind-of-fast-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-retrospectives-catching-the-wrong-kind-of-fast-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-retrospectives-catching-the-wrong-kind-of-fast-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-retrospectives-catching-the-wrong-kind-of-fast-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; generates a complete Stripe integration, data models, signup flow, all in an afternoon. He’s shipping pull requests faster than he ever has in his career.&lt;/p&gt;

&lt;p&gt;Priya does the same with the farm portal. She prompts for an inventory management screen, gets a working prototype back, tweaks it, and asks Tom to push it live. By Wednesday she has a portal where farms can list their available produce: tomatoes, 50kg, $4/kg.&lt;/p&gt;

&lt;p&gt;Maya sketches a box customisation page on a whiteboard: subscribers pick which items they want each week. Customer delight angle. Tom builds a prototype from the sketch, &lt;label for=&quot;sn-writing-retrospectives-catching-the-wrong-kind-of-fast-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-retrospectives-catching-the-wrong-kind-of-fast-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompting&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-retrospectives-catching-the-wrong-kind-of-fast-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-retrospectives-catching-the-wrong-kind-of-fast-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; an LLM for the UI components. It looks rough but functional.&lt;/p&gt;

&lt;p&gt;Slack is buzzing with screenshots and pull requests. The team is shipping faster than any of them expected. Tom messages the group: “This is the most productive week I’ve ever had.” Everyone agrees. It feels like they’ve cracked it.&lt;/p&gt;

&lt;p&gt;When the code is ready, Tom deploys by SSH-ing into the server from his laptop and running a script he wrote on the first day. It takes about twelve minutes. Nobody else has the credentials or knows the steps. That’s fine, Tom thinks; he’s the only one writing code that matters right now.&lt;/p&gt;

&lt;h3 id=&quot;week-two&quot;&gt;Week two&lt;/h3&gt;

&lt;p&gt;Maya reviews what the team has built.&lt;/p&gt;

&lt;p&gt;The subscription system looks impressive: lots of code, clean UI, working payment integration. But Tom has assumed the box contents are fixed, the same items every week. “No,” Maya explains. “The whole point is that contents change based on what farms have available that week. Seasonal produce. That’s what makes it different from a supermarket delivery.”&lt;/p&gt;

&lt;p&gt;Tom’s subscription model doesn’t account for variable contents at all. The data model is wrong. The LLM generated exactly what he asked for, the problem is he asked for the wrong thing. That’s a substantial rewrite.&lt;/p&gt;

&lt;p&gt;Priya’s farm portal works, but she has questions nobody has answered. How far in advance do farms need to commit their availability? Can they update quantities after a deadline? What happens when total supply across all farms doesn’t cover all subscriber orders? She’d been guessing at the answers and feeding those guesses to the LLM, and some of those guesses are wrong.&lt;/p&gt;

&lt;p&gt;The customisation prototype is functional. Maya clicks through it: pick your tomatoes, swap out the zucchini, add extra basil. It works. And something about seeing it working makes her stomach drop.&lt;/p&gt;

&lt;p&gt;“This isn’t right,” she says, mostly to herself. Then, louder: “This isn’t what we’re selling. We curate the box. That’s the whole point, they trust us to give them good stuff. If customers are picking items themselves, we’re just a worse version of online grocery shopping.”&lt;/p&gt;

&lt;p&gt;Tom looks at her. “You sketched this. On the whiteboard. Last Tuesday.”&lt;/p&gt;

&lt;p&gt;“I know.” Maya stares at the screen. She had been thinking about delight, about customers feeling involved. But seeing it built, she can see what it actually is: a feature that undermines the thing that makes them different. She hadn’t thought it through. She’d had a half-formed idea, sketched it in the excitement of the moment, and Tom had built it before either of them stopped to ask whether it made sense.&lt;/p&gt;

&lt;p&gt;The whole customisation flow is wasted work. Tom spent a day and a half on it.&lt;/p&gt;

&lt;h3 id=&quot;week-three&quot;&gt;Week three&lt;/h3&gt;

&lt;p&gt;The team tries to course correct. Tom prompts Claude again: “Rebuild the subscription model to support variable weekly contents based on farm availability.” The code comes back in twenty minutes. It’s clean, well-structured, has tests. Tom is pleased.&lt;/p&gt;

&lt;p&gt;Then Maya asks: “What happens when a farm can’t supply what they promised?”&lt;/p&gt;

&lt;p&gt;Tom looks at the code. There’s no concept of supply shortfalls. He prompts again: “Add handling for when farm supply doesn’t meet subscriber demand.” Claude generates a substitution system that randomly swaps items. Maya shakes her head. “You can’t just swap randomly. Carrots for parsnips, sure. Carrots for lettuce? Nobody wants that.”&lt;/p&gt;

&lt;p&gt;Tom prompts again. And again. Each iteration gets closer, but each one surfaces a new question nobody had thought to ask. The LLM is extraordinarily helpful at generating code. It’s just that nobody can tell it what the code should &lt;em&gt;do&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Meanwhile, Priya has paused the farm portal entirely. She has a list of questions and nobody to answer them: How far in advance do farms commit? Can they change quantities after a deadline? What units do they report in: kilograms, crates, “enough for about forty boxes”? She asks Maya, but Maya is in back-to-back meetings with courier companies trying to figure out delivery logistics.&lt;/p&gt;

&lt;p&gt;Maya starts redesigning the customer experience without customisation, but she’s pulled in every direction, answering Priya’s farm portal questions, reviewing Tom’s code, talking to courier companies. Nobody is doing the design work full-time. It shows.&lt;/p&gt;

&lt;p&gt;Sam mentions a designer she met at a coworking space event in Leederville: Jas Kowalski, freelance, good portfolio, available. Maya hesitates. “We don’t need a designer yet. Not full-time.” Sam pushes back: “Two days a week. Just to sort out the customer-facing stuff. You’re doing three jobs and none of them are design.” Maya agrees to two days. Jas starts the following Monday. Nobody briefs her on the customisation decision. Her first task is tidying up a flow that the team has already decided to throw away.&lt;/p&gt;

&lt;h3 id=&quot;week-four&quot;&gt;Week four&lt;/h3&gt;

&lt;p&gt;Tom’s subscription model v2 is working, sort of. He demos it to Maya on Monday. She spots a problem immediately: “This charges customers on signup day. We need to charge them on delivery day, because we don’t know what’s in their box until the morning we pack it.”&lt;/p&gt;

&lt;p&gt;Tom stares at the screen. The entire payment flow assumes charge-on-signup. The data model, the Stripe integration, the receipt emails: all of it. He could ask the LLM to restructure, but the last three restructures have each introduced new assumptions that turned out to be wrong.&lt;/p&gt;

&lt;p&gt;“I’ll fix it,” he says, but the energy has gone out of his voice. He’s thinking about his brother Marco at the family Christmas, asking “how’s the little startup going?” in that tone that manages to be both supportive and pitying.&lt;/p&gt;

&lt;p&gt;Priya, still blocked on the farm portal, starts helping Tom with the subscription rewrite. Maya is supposed to be thinking about the customer experience but hasn’t found time. Sam redesigns the landing page instead; at least that’s something she can do without needing decisions from anyone.&lt;/p&gt;

&lt;p&gt;Sam sends a cheerful Slack message: “Customer #1 just emailed asking when their &lt;a href=&quot;/writing/minimum-viable-product-the-first-box/&quot;&gt;first box&lt;/a&gt; arrives! The pilot subscribers are getting restless.” Nobody knows the answer. Even Mrs Patterson on Stirling Highway has asked twice now.&lt;/p&gt;

&lt;p&gt;New questions keep surfacing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;What happens when a customer is allergic to something in this week’s box?&lt;/li&gt;
  &lt;li&gt;Do farms get paid per item, per box, or per week?&lt;/li&gt;
  &lt;li&gt;Who decides substitutions when a farm can’t deliver what they promised?&lt;/li&gt;
  &lt;li&gt;What about delivery logistics: own drivers or a courier?&lt;/li&gt;
  &lt;li&gt;What if a customer wants to skip a week on holiday?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM is still generating code fast. But each answer raises two more questions, and the team keeps building on assumptions that turn out to be wrong. The velocity is high. The progress is circular.&lt;/p&gt;

&lt;p&gt;Four weeks in, the team has a subscription system built on wrong assumptions (twice), a farm portal that nobody’s sure how to finish, a discarded customisation prototype, and a growing list of questions that should have been answered before anyone opened an IDE. Tom’s git log has more reverts than merges. Maya needs to talk to Dave Morrison (a third-generation farmer outside Margaret River whose produce she’s hoping to build the business around) about supply commitments, but she hasn’t found the time.&lt;/p&gt;

&lt;p&gt;They’re not lazy. They’re not bad at their jobs. The LLMs aren’t the problem either; they did exactly what they were asked, impressively fast. The problem is that nobody understood what to ask for. The team started building before they understood the problem, and the LLMs just helped them build the wrong thing faster.&lt;/p&gt;

&lt;h3 id=&quot;the-expensive-kind-of-learning&quot;&gt;The expensive kind of learning&lt;/h3&gt;

&lt;p&gt;Every one of those surprises was knowable. The team assumed they understood the domain because the concept sounded simple.&lt;/p&gt;

&lt;p&gt;LLMs made it worse, not better. The sheer speed of code generation disguises the lack of understanding. When it took two weeks to build something wrong, you noticed after two weeks. When the LLM builds it wrong in an afternoon, you might not notice until you’ve built three more things on top of the wrong foundation. The velocity feels incredible. The progress is an illusion.&lt;/p&gt;

&lt;p&gt;“It’s just a…” is one of the most expensive phrases in software development. And “the LLM can build that in an hour” is its dangerous new cousin.&lt;/p&gt;

&lt;p&gt;The cost isn’t just the wasted code. It’s the trust erosion. Tom is frustrated because his work got thrown away, twice. Jas is frustrated because nobody told her the customisation premise was wrong. Priya is blocked and going quiet about it. Maya is wondering if she hired the correct people. Everyone’s doing their best, but the team is pulling in different directions because they never built a shared understanding of what they’re actually building.&lt;/p&gt;

&lt;h3 id=&quot;the-retro-that-changed-everything&quot;&gt;The retro that changed everything&lt;/h3&gt;

&lt;p&gt;Maya’s friend Lee drops by the office on a Friday afternoon. They’d met at the Margaret River farmers’ market six months earlier: Maya buying produce for a dinner party, Lee buying coffee, and a twenty-minute conversation about supply chains that turned into a friendship. Lee spent twenty years in enterprise consulting before semi-retiring to the coast. He’s 52, surfs badly but persistently, and has the calm manner of someone who’s watched a lot of teams struggle with the same problems. He can feel the tension the moment he walks in. Tom is quiet. Priya is staring at a Jira board full of blocked tickets. Jas is redesigning the landing page for the third time because nobody will answer her questions about the customer experience.&lt;/p&gt;

&lt;p&gt;“When was the last time you all stopped and talked about how the work is going?” Lee asks.&lt;/p&gt;

&lt;p&gt;Maya looks blank. “We have standups.”&lt;/p&gt;

&lt;p&gt;“Not standups. A proper retrospective. Where you actually talk about what’s working and what isn’t.”&lt;/p&gt;

&lt;p&gt;Maya is sceptical; they’re burning runway and the last thing they need is another meeting. But Lee pushes gently: “Ninety minutes. I’ll facilitate. If it’s a waste of time, I’ll buy the team lunch.”&lt;/p&gt;

&lt;p&gt;They gather in the meeting room on Monday morning. Lee draws two columns on the whiteboard (“What went well” and “What didn’t go well”) and hands out two colours of sticky notes.&lt;/p&gt;

&lt;p&gt;“Five stages,” he says. “Let’s start.”&lt;/p&gt;

&lt;p&gt;Stage one: set the stage. Lee reads the &lt;a href=&quot;https://retrospectivewiki.org/index.php?title=The_Prime_Directive&quot;&gt;Retrospective Prime Directive&lt;/a&gt;: &lt;em&gt;“Regardless of what we discover, we understand and truly believe that everyone did the best job they could, given what they knew at the time.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;He lets it sit for a moment. “This isn’t a blame session. One word from each of you: how are you feeling right now?”&lt;/p&gt;

&lt;p&gt;Tom: “Frustrated.” Priya: “Stuck.” Jas: “Confused.” Sam: “Anxious.” She has 47 unread emails from pilot subscribers on her phone. She reads them before bed most nights, but she hasn’t told anyone that. Maya pauses. “Guilty.”&lt;/p&gt;

&lt;p&gt;Lee nods. “Good. That’s honest. Let’s work with that.”&lt;/p&gt;

&lt;p&gt;Stage two: gather data. “Green notes for what went well. Pink notes for what didn’t. One thing per note, as many as you want. No talking; just write.”&lt;/p&gt;

&lt;p&gt;The team writes for five minutes. Lee tells them to put the green notes on the left side of the board and the pink notes on the right, then read each one aloud as they place it.&lt;/p&gt;

&lt;p&gt;The green side is thinner than the pink side, but it’s not empty.&lt;/p&gt;

&lt;p&gt;Tom: &lt;em&gt;“LLM code generation is genuinely fast. I’ve never shipped this much code this quickly.”&lt;/em&gt; And: &lt;em&gt;“The Stripe integration works perfectly. Payment flow is solid.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Priya: &lt;em&gt;“I identified the farm portal questions early. The problem wasn’t spotting them, it was getting answers.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sam: &lt;em&gt;“We have pilot subscribers. People actually want this product. The landing page is working.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maya: &lt;em&gt;“The team is motivated and hardworking. Nobody’s coasting.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then the pink side.&lt;/p&gt;

&lt;p&gt;Tom: &lt;em&gt;“I’ve rebuilt the subscription model twice. Both times I asked the LLM to generate it, both times it was wrong, and both times I didn’t find out until Maya looked at it.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maya: &lt;em&gt;“I sketched a whole customisation flow that we’re not using. I should have checked the premise before Tom built it.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Priya: &lt;em&gt;“I’ve been blocked for two weeks waiting for answers about how farms work. I keep guessing and getting it wrong.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sam: &lt;em&gt;“Pilot subscribers are emailing me asking when their first box arrives. I don’t know the answer.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Jas: &lt;em&gt;“I spent my first three days redesigning a customisation flow that was already dead. Nobody told me.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maya, reading her own note back: &lt;em&gt;“Everyone is frustrated with me. I have the answers but I’m not sharing them fast enough.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Stage three: generate insights. Lee asks the team to stand up and look at the wall. “Group the pink notes that seem related.”&lt;/p&gt;

&lt;p&gt;Priya puts her “blocked waiting for answers” note next to Tom’s “didn’t find out until Maya looked at it.” Jas adds her customisation note to the same cluster. Sam’s note goes there too.&lt;/p&gt;

&lt;p&gt;One large cluster. A few stragglers.&lt;/p&gt;

&lt;p&gt;“What do you notice?” Lee asks.&lt;/p&gt;

&lt;p&gt;Tom sees it first. “They’re all the same problem. Maya understands the business. We don’t. And building stuff without that understanding isn’t working. I’m prompting an LLM to write code, but I’m describing the wrong thing because I don’t know what the correct thing is.”&lt;/p&gt;

&lt;p&gt;Priya nods. “The LLM does exactly what I ask. The problem is I’m asking the wrong questions.”&lt;/p&gt;

&lt;p&gt;Lee: “And the green side?”&lt;/p&gt;

&lt;p&gt;Jas reads them again. “We’re not bad at our jobs. The code quality is high. The speed is real. We have customers who want the product.”&lt;/p&gt;

&lt;p&gt;“Right,” Lee says. “The tools aren’t the problem. The people aren’t the problem. One person has the domain knowledge, and everyone else is guessing. The LLMs made that worse, not better, because the guesses turned into working code before anyone could catch them.”&lt;/p&gt;

&lt;p&gt;The room goes quiet.&lt;/p&gt;

&lt;p&gt;Stage four: decide what to do. “Actions,” Lee says. “What could this team do to fix the root cause? One idea per note, no filtering. Two minutes.”&lt;/p&gt;

&lt;p&gt;The notes come fast.&lt;/p&gt;

&lt;p&gt;Tom: “Daily check-ins with Maya.” And: “Maya reviews every PR before merge.” Priya: “Shared document of all business rules.” And: “Weekly domain Q&amp;amp;A session.” Sam: “Record Maya explaining the business on video.”&lt;/p&gt;

&lt;p&gt;Five ideas. Lee reads them back. “What do they all have in common?”&lt;/p&gt;

&lt;p&gt;Priya sees it. “They all depend on Maya. Every single one puts Maya at the centre.”&lt;/p&gt;

&lt;p&gt;“Right. Five ways to get knowledge out of Maya’s head, one conversation at a time. They’d work, slowly.” He writes a seventh note. “There’s a technique called Event Storming. Whole team in a room, farming contacts too if you can get them. A few hours mapping out how the business actually works, not architecture, not user stories. Just: what happens, in what order, and where are the hard parts. Sticky notes on a wall. The shared understanding these six ideas are reaching for? Event Storming builds it in an afternoon.”&lt;/p&gt;

&lt;p&gt;He sticks it on the board. “Dot vote. Two dots each. Pick whatever you think will make the biggest difference, even if it’s not mine.”&lt;/p&gt;

&lt;p&gt;Event Storming gets six dots out of eight. Daily check-ins get two.&lt;/p&gt;

&lt;p&gt;Lee nods. “That’s your call, not mine. If I’d walked in and said ‘do Event Storming,’ you’d be doing it because I told you to. Different thing entirely.”&lt;/p&gt;

&lt;p&gt;Maya looks unconvinced. “So the answer is… sticky notes.”&lt;/p&gt;

&lt;p&gt;“The misunderstandings that just cost you four weeks? They surface in the first hour, when they’re cheap to fix.” Lee pauses. “You’ll feel like you’re going slower. You’re not. You’re just putting the learning where it’s cheap: on a wall instead of in production.”&lt;/p&gt;

&lt;p&gt;Stage five: close. “One last thing,” Lee says. “One thing you appreciated about someone else these past four weeks.”&lt;/p&gt;

&lt;p&gt;Tom: “Priya spotted the farm portal questions before any of us even thought about them. That’s good instinct.”&lt;/p&gt;

&lt;p&gt;Priya: “Tom’s code is always clean. Even the stuff we threw away was well-written.”&lt;/p&gt;

&lt;p&gt;Priya: “Sam’s been handling angry pilot subscribers by herself and never complained.”&lt;/p&gt;

&lt;p&gt;Sam: “Maya’s always available when you can actually get hold of her. She never brushes you off.”&lt;/p&gt;

&lt;p&gt;Maya: “Everyone kept working even when they weren’t sure what they were building. That takes guts.”&lt;/p&gt;

&lt;p&gt;Lee smiles. “You’ve got a good team. You just need a shared picture of what you’re building. Let’s go get one.”&lt;/p&gt;

&lt;p&gt;“And the retros?”&lt;/p&gt;

&lt;p&gt;“Every two weeks. Non-negotiable.” Lee glances at the wall of pink notes. “When everyone’s prompting LLMs on their own, the thinking goes invisible. This is where it becomes visible again. But first. Event Storming.”&lt;/p&gt;

&lt;p&gt;The team files out. Lee steps outside by himself. His phone shows a missed call from his daughter Yuki. He looks at it for a moment, puts the phone back in his pocket, and goes to find his car.&lt;/p&gt;

&lt;p&gt;Inside, Maya stays in the meeting room alone. The wall of pink sticky notes stares back at her, every one of them a version of the same problem. She calls Nadia. “I think I’m the problem,” she says. Nadia listens for a long time.&lt;/p&gt;

&lt;p&gt;That evening, Tom sits on the couch while Sarah puts Ava and Leo to bed. Ava calls out from her room: “Did you make something today, Daddy?” Tom doesn’t answer. Sarah comes out and asks how the startup is going. “It’s fine,” he says. Sarah studies him. She knows it’s not fine, but she also knows that Tom processes things by building, not by talking. She lets it go. Tom opens his laptop and stares at his git log. More reverts than merges. He’d been thinking about other jobs all weekend. He’s not thinking about them now. Not quite.&lt;/p&gt;

&lt;p&gt;Priya goes home to her flat in North Perth, feeds her cat Refactor, and calls her mum in Melbourne. Her mum asks about work. “It’s fine,” Priya says. It’s not fine, but she doesn’t know how to explain what “blocked on domain questions” means to someone who runs a grocery shop in Dandenong.&lt;/p&gt;

&lt;p&gt;Jas walks back to her flat in Leederville. Her contract is two days a week and she’s already wondering if those two days are worth it. She spent her first week designing improvements to a customisation flow that was already dead. Nobody told her. She found out when Tom mentioned it in standup, casually, like everyone knew. She’d sat there with her Moleskine open and said nothing. She thinks about not renewing. It’s only two days. She could fill them easily. She calls her mum in Adelaide instead. Her mum listens, then tells her about her grandmother, who ran a market garden in the Adelaide Hills for thirty years. “She never grew what she thought people should eat. She grew what they actually wanted.” Her mum pauses. “The good ones figure that out. Give them a minute.”&lt;/p&gt;

&lt;p&gt;Jas doesn’t quit.&lt;/p&gt;

&lt;p&gt;The retro produced one action. One. And it changed everything that followed.&lt;/p&gt;

&lt;p&gt;Maya books the biggest meeting room she can find and calls Dave Morrison.&lt;/p&gt;

&lt;p&gt;“I need you to come to Perth,” she says. “You and Rachel. My team has been building for a month and half of what they’ve built is wrong because they don’t understand how any of this actually works. How the farms operate. What happens when a crop fails. What the substitution logic really looks like. They need to hear it from someone who lives it, not from me relaying it secondhand between meetings.”&lt;/p&gt;

&lt;p&gt;She takes a breath. “I need everyone in the same room: the developers, the designer, Sam, you, Rachel, Lee. I need us to map the whole thing out together. What happens, in what order, where it gets complicated. I need the team to see the problems you see. I need you to tell them about the deadlines that matter, the things that go wrong, the stuff I’ve been carrying around in my head that I should have put on a wall weeks ago.”&lt;/p&gt;

&lt;p&gt;Dave is quiet for a moment. “I’ve been to workshops before. They were rubbish.”&lt;/p&gt;

&lt;p&gt;“This one might be too. But we can’t keep building on guesses. I need you there so we can figure out what’s actually important, what we’re getting wrong, and what we haven’t even thought about yet.”&lt;/p&gt;

&lt;p&gt;“What time? I’ve got cows.”&lt;/p&gt;

&lt;p&gt;Dave agrees to come. The workshop is called &lt;a href=&quot;/writing/event-storming-building-shared-understanding/&quot;&gt;Event Storming&lt;/a&gt;, and it starts with a wall of sticky notes and everyone in the room.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Value Is in Ideas, Not Code</title>
    <link href="/writing/the-value-is-in-ideas-not-code/"/>
    <updated>2026-03-12T06:00:00+08:00</updated>
    <id>/writing/the-value-is-in-ideas-not-code/</id>
    <content type="html">&lt;p&gt;Writing code used to be the bottleneck. You’d have an idea, and then you’d spend days or weeks turning it into something you could actually try. Most ideas died in that gap, not because they were bad, but because the cost of finding out was too high.&lt;/p&gt;

&lt;p&gt;That’s changed. LLMs have made code implementation almost trivial for a huge class of problems. I don’t mean they write perfect production systems; they don’t (who does?). But they’re astonishingly good at producing “good enough”. The kind of thing you need to try an idea out, show it to someone, see if the shape of it works. A rough dashboard. A prototype API. A quick tool that does the one thing you need. An iOS app to manage substitutions on your kid’s sports team. What used to take a week or two takes an afternoon.&lt;/p&gt;

&lt;h3 id=&quot;the-value-has-moved&quot;&gt;The value has moved&lt;/h3&gt;

&lt;p&gt;If producing code is cheap, the bottleneck shifts. The scarce resource isn’t implementation any more; it’s knowing what to ask for. Two things feed that: curation and knowledge.&lt;/p&gt;

&lt;p&gt;Curation is the strategic bit. Which ideas are worth pulling together? What combination of things, each individually unremarkable, becomes something genuinely useful when you stack them up? An &lt;label for=&quot;sn-writing-the-value-is-in-ideas-not-code-llm&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-value-is-in-ideas-not-code-llm-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;LLM&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-value-is-in-ideas-not-code-llm&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-value-is-in-ideas-not-code-llm-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;LLM&lt;/span&gt;A neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for.
&lt;/span&gt; can build what you describe, but it can’t (yet…) tell you what’s worth building. That judgement (knowing which thread to pull, which experiment to run next, which of your twelve half-formed ideas deserves an afternoon) is where the leverage is now.&lt;/p&gt;

&lt;p&gt;Knowledge is the tactical bit. The more you know exists, the more you can build. LLMs are force multipliers, but they only multiply what you bring to the conversation.&lt;/p&gt;

&lt;p&gt;If you know that sparkline charts exist, you can say “put sparklines in the table cells” and get them in minutes. If you don’t know sparklines are a thing, you’ll never think to ask, and they are unlikely to crop up as the LLM explores for you.&lt;/p&gt;

&lt;p&gt;This pattern is everywhere:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Know what a dead letter queue is? You can ask for one by name instead of reinventing retry logic from scratch.&lt;/li&gt;
  &lt;li&gt;Seen an optimistic UI before? You can tell the LLM “update the UI before the server responds, roll back if it fails” and get a snappy interface in minutes.&lt;/li&gt;
  &lt;li&gt;Heard of feature flags? You can ask for a feature flag system in your prototype and suddenly you’re testing two versions of an idea at once.&lt;/li&gt;
  &lt;li&gt;Know what eventual consistency means? You can describe the tradeoff you want and skip the long detour where you accidentally build something that doesn’t scale.&lt;/li&gt;
  &lt;li&gt;Familiar with the concept of a circuit breaker? One sentence in your &lt;label for=&quot;sn-writing-the-value-is-in-ideas-not-code-prompt&quot; class=&quot;term&quot; aria-describedby=&quot;sn-writing-the-value-is-in-ideas-not-code-prompt-note&quot;&gt;&lt;span class=&quot;term__label&quot;&gt;prompt&lt;/span&gt;&lt;/label&gt;&lt;input type=&quot;checkbox&quot; id=&quot;sn-writing-the-value-is-in-ideas-not-code-prompt&quot; class=&quot;term-toggle&quot; aria-hidden=&quot;true&quot; /&gt;&lt;span class=&quot;sidenote&quot; id=&quot;sn-writing-the-value-is-in-ideas-not-code-prompt-note&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sidenote__term&quot;&gt;Prompt&lt;/span&gt;The input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot.
&lt;/span&gt; and your API client handles failures gracefully instead of hammering a dead service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every piece of knowledge you’ve accumulated over the years is a prompt waiting to happen. Broad technical knowledge has always been valuable, but now it converts directly into working software in a way it never did before. The person who’s seen a lot of things and roughly knows what’s possible will consistently out-build the person who’s deeper in one stack but doesn’t know what’s out there.&lt;/p&gt;

&lt;h3 id=&quot;deploy-learn-iterate&quot;&gt;Deploy, learn, iterate&lt;/h3&gt;

&lt;p&gt;When the cost of trying something drops this far, you can run experiments you’d never have justified before. Build the thing. Ship it. See if anyone cares. If they don’t, you’ve lost a few hours, not a sprint.&lt;/p&gt;

&lt;p&gt;We’ve talked about rapid prototyping (deploy, learn, iterate) for years, but the cost has finally dropped low enough that it’s genuinely practical for most ideas. Not just the ones that survive a prioritisation meeting. Instead of specifying, building, testing, deploying over weeks, you can have something in front of real users in hours, and that changes which ideas get a chance at all.&lt;/p&gt;

&lt;h3 id=&quot;so-what&quot;&gt;So what?&lt;/h3&gt;

&lt;p&gt;If you’re a builder: lean into breadth. Read widely. Collect patterns and concepts. Your library of “things I know exist” is your competitive advantage, because each one is a card you can play when the right problem shows up.&lt;/p&gt;

&lt;p&gt;And if you’re not a builder yet? The barrier just got a whole lot lower.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Minimum Viable Product: The First Box</title>
    <link href="/writing/minimum-viable-product-the-first-box/"/>
    <updated>2026-03-10T06:00:00+08:00</updated>
    <id>/writing/minimum-viable-product-the-first-box/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/from-chaos-to-clarity/&quot;&gt;From Chaos to Clarity&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;Tom built the landing page in a weekend. It wasn’t beautiful. Jas hadn’t joined yet, and Tom’s design instincts ran to “functional.” But it had a clear headline (&lt;em&gt;Fresh local produce, delivered weekly&lt;/em&gt;), a description of the two box sizes, a price, and a signup form that collected a name, email, address, and payment details.&lt;/p&gt;

&lt;p&gt;The payment integration worked. Tom had used Claude to generate a Stripe setup in an afternoon, and it was solid: one of the few things from those early weeks that didn’t need rebuilding. The confirmation email went out. The landing page loaded fast. The form submitted cleanly.&lt;/p&gt;

&lt;p&gt;What the form didn’t collect was a unit number. Or a delivery note. Or any indication of whether the customer lived in a house, a flat, or a unit complex. Tom’s data model had: name, email, street address, suburb, postcode. It seemed like enough at the time.&lt;/p&gt;

&lt;h3 id=&quot;the-flyer&quot;&gt;The flyer&lt;/h3&gt;

&lt;p&gt;Maya designed the flyer herself. Hand-drawn, because she couldn’t afford a designer and because she wanted it to feel personal. A sketch of a green crate overflowing with vegetables. The Greenbox name in her own handwriting. A QR code that Tom generated, linking to the landing page. And a line at the bottom: &lt;em&gt;Local farms. Weekly boxes. No thinking required.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;She printed fifty copies at Officeworks and drove down to the Margaret River farmers’ market on Saturday morning.&lt;/p&gt;

&lt;p&gt;The market was where Maya had grown up. Her parents had sold produce from a trestle table at the far end for fifteen years. She knew the rhythms: the early-morning setup, the rush between nine and eleven, the slow afternoon when the stallholders started packing up and the remaining customers got the best deals. She knew the regulars: the retired couples who came every week, the young families with kids running between the stalls, the restaurant owners doing their weekend sourcing.&lt;/p&gt;

&lt;p&gt;Maya walked the market with her flyers, talking to everyone who’d listen. Some of them remembered her parents. Most of them liked the idea. A few were sceptical.&lt;/p&gt;

&lt;p&gt;“Another subscription thing? I tried one of those meal kit services. Lasted three weeks.”&lt;/p&gt;

&lt;p&gt;“This is different. It’s not a meal kit. It’s actual produce from actual farms within fifty k’s.”&lt;/p&gt;

&lt;p&gt;“Which farms?”&lt;/p&gt;

&lt;p&gt;“Dave Morrison, for one. And Rachel’s place.”&lt;/p&gt;

&lt;p&gt;The mention of Dave’s name carried weight at the Margaret River market. People knew Dave. People trusted Dave. If Dave was involved, the vegetables would be good.&lt;/p&gt;

&lt;p&gt;By the end of Saturday, twenty-two people had scanned the QR code and signed up. Twenty-two. Maya sat in her car in the market car park and stared at her phone. Twenty-two real people had given her their credit card details and trusted her to send them a box of vegetables.&lt;/p&gt;

&lt;p&gt;She called Tom. “Twenty-two.”&lt;/p&gt;

&lt;p&gt;“Twenty-two what?”&lt;/p&gt;

&lt;p&gt;“Subscribers. We have twenty-two subscribers.”&lt;/p&gt;

&lt;p&gt;A pause. Then Tom’s voice, with the particular excitement of a builder who’s just learned that the thing he built has users: “That’s… that’s actual people.”&lt;/p&gt;

&lt;p&gt;“Actual people who expect a box of vegetables on Thursday.”&lt;/p&gt;

&lt;p&gt;“Right. Thursday. That’s… five days from now.”&lt;/p&gt;

&lt;p&gt;“Four, actually.”&lt;/p&gt;

&lt;h3 id=&quot;packing-day&quot;&gt;Packing day&lt;/h3&gt;

&lt;p&gt;Wednesday morning. Maya’s kitchen table.&lt;/p&gt;

&lt;p&gt;Dave arrived at 5am in his ute, the back loaded with green crates. Tomatoes, zucchini, spinach, carrots, beetroot, a few bunches of herbs. Everything picked the day before. He carried the crates in through the front door, set them on the kitchen floor, and surveyed the operation.&lt;/p&gt;

&lt;p&gt;“This is your packing facility?”&lt;/p&gt;

&lt;p&gt;“For now.”&lt;/p&gt;

&lt;p&gt;“It’s a kitchen table.”&lt;/p&gt;

&lt;p&gt;“It’s a large kitchen table.”&lt;/p&gt;

&lt;p&gt;Dave shook his head with the expression of a man who had seen many ambitious plans meet their first contact with reality. He left without saying much else, though he paused at the door and said, “The spinach won’t last if you leave it out. Get it in the boxes fast.”&lt;/p&gt;

&lt;p&gt;Rachel arrived an hour later in her own ute, with a smaller contribution: bunches of kale, some sweet potatoes, and a crate of capsicums. She helped carry them in and looked at the kitchen table.&lt;/p&gt;

&lt;p&gt;“You right?”&lt;/p&gt;

&lt;p&gt;“I think so.”&lt;/p&gt;

&lt;p&gt;Rachel studied the piles of produce. “You’ll want to pack the heavy stuff at the bottom. Sweet potatoes first, then the root veg, then the leafy stuff on top. If you put the spinach at the bottom it’ll be soup by the time it arrives.”&lt;/p&gt;

&lt;p&gt;Maya wrote this down. She hadn’t thought about packing order. The spreadsheet had columns for subscription size, produce allocation, and delivery address. It did not have a column for “which vegetables go on the bottom.”&lt;/p&gt;

&lt;p&gt;Sam arrived at seven with boxes. Actual cardboard boxes; she’d sourced them from a packaging company in Welshpool, the cheapest option that was still food-grade. They were plain brown, no branding, because branded boxes cost four times as much and Maya’s budget didn’t stretch that far. Sam had written “GREENBOX” on each one in green marker. It looked homemade, because it was.&lt;/p&gt;

&lt;p&gt;They packed twenty-two boxes on the kitchen table. Maya and Sam working side by side, consulting the spreadsheet on Maya’s laptop, weighing produce on a kitchen scale. Tom sat in the living room, monitoring the website and the payment system, feeling useless.&lt;/p&gt;

&lt;p&gt;“Can I help pack?” he asked.&lt;/p&gt;

&lt;p&gt;“Can you tell the difference between baby spinach and rocket?” Sam replied.&lt;/p&gt;

&lt;p&gt;“They’re both green.”&lt;/p&gt;

&lt;p&gt;“Stay in the living room.”&lt;/p&gt;

&lt;p&gt;The packing took four hours. Maya’s back ached. Sam had produce stains on her shirt. The kitchen looked like a greengrocer had exploded. Nadia, who had taken the day off to help, wrapped the last box in brown paper and taped the address label on with the precision of someone who had decided that if her living room was going to be a warehouse, at least the warehouse would be tidy.&lt;/p&gt;

&lt;p&gt;By midday, twenty-two boxes were stacked by the front door. They looked good. They smelled good. Maya took a photo and sent it to Dave. He replied with a single thumbs-up emoji, the most effusive communication she’d ever received from him.&lt;/p&gt;

&lt;h3 id=&quot;the-delivery&quot;&gt;The delivery&lt;/h3&gt;

&lt;p&gt;Sam had arranged delivery through her mate Callum, who drove a courier van in the southern suburbs. Callum was reliable, Sam said. He’d done deliveries for the trucking company and he knew the Perth metro area. He was also cheap. Sam had negotiated a rate per box that was barely above fuel costs, a favour that Callum would regret by the third week.&lt;/p&gt;

&lt;p&gt;The plan was Thursday delivery. Callum would pick up the boxes at midday and deliver them between 2pm and 6pm.&lt;/p&gt;

&lt;p&gt;Callum picked up the boxes on Wednesday.&lt;/p&gt;

&lt;p&gt;“Thursday,” Sam said, when he turned up a day early. “Thursday delivery. I said Thursday.”&lt;/p&gt;

&lt;p&gt;“You said this week. I’ve got a full run tomorrow. Today’s better.”&lt;/p&gt;

&lt;p&gt;Sam called Maya. Maya called Callum. Callum was already driving, with twenty-two boxes in the back of his van and the confidence of a man who had been doing deliveries for twelve years and didn’t see the problem.&lt;/p&gt;

&lt;p&gt;“Most of these people are at work,” Maya said. “They won’t be home until five or six.”&lt;/p&gt;

&lt;p&gt;“I’ll leave them on the doorstep.”&lt;/p&gt;

&lt;p&gt;“It’s fresh produce. In a cardboard box. In the sun.”&lt;/p&gt;

&lt;p&gt;“I’ll find shade.”&lt;/p&gt;

&lt;p&gt;Half the boxes were delivered to empty houses on a Wednesday afternoon. Six were left on doorsteps in full sun. Three were delivered to the wrong addresses because Tom’s address data didn’t include unit numbers. Two customers lived in unit complexes, and Callum had left the boxes at the front door of the building, not the individual unit. One box was never found. The spinach, which Dave had warned them about, had wilted in the boxes that sat in the sun for three hours.&lt;/p&gt;

&lt;h3 id=&quot;mayas-car&quot;&gt;Maya’s car&lt;/h3&gt;

&lt;p&gt;At 4pm on Wednesday, Maya was sitting in her car outside a house in Applecross. She’d driven out to intercept the last few deliveries, hoping to correct the addresses and apologise in person. The house belonged to a subscriber named Mrs Patterson, a woman in her sixties who lived alone on Stirling Highway and had signed up at the market because she liked the idea of someone else choosing her vegetables for the week.&lt;/p&gt;

&lt;p&gt;Mrs Patterson wasn’t home. The box was on her doorstep, in the shade at least, but it had been there for two hours. Maya picked it up and opened it. The spinach was limp. The herbs had started to wilt. The tomatoes were fine (tomatoes are forgiving), but the overall impression was not “premium local produce.” It was “vegetables that had been sitting in a box for too long.”&lt;/p&gt;

&lt;p&gt;Maya put the box back, sat in her car, and called Mrs Patterson.&lt;/p&gt;

&lt;p&gt;“Hello?”&lt;/p&gt;

&lt;p&gt;“Mrs Patterson, this is Maya from Greenbox. Your box was delivered today instead of tomorrow, and I’m afraid some of the produce might not be at its best. I’m so sorry. I’m outside your house now and I’d like to –”&lt;/p&gt;

&lt;p&gt;“Oh, that’s all right, love. I saw it when I came home for lunch. The tomatoes looked gorgeous. Don’t worry about it.”&lt;/p&gt;

&lt;p&gt;Mrs Patterson was kind. She was generous. She told Maya to stop worrying and come back next week with a better box. Maya thanked her, hung up, and sat in her car for five minutes with her hands on the steering wheel, staring at nothing.&lt;/p&gt;

&lt;p&gt;She wasn’t crying because Mrs Patterson was angry. She was crying because Mrs Patterson was kind, and Maya felt like she didn’t deserve it. Twenty-two people had trusted her with their dinner, and she’d delivered wilted spinach on the wrong day to the wrong addresses. The spreadsheet, the flyer, the 5am packing session: all of it had produced a result that was, by any honest assessment, a disaster.&lt;/p&gt;

&lt;p&gt;She wiped her face, started the car, and drove to the next address.&lt;/p&gt;

&lt;h3 id=&quot;the-recovery&quot;&gt;The recovery&lt;/h3&gt;

&lt;p&gt;That evening, Maya, Tom, and Sam sat at the kitchen table (the same table they’d packed boxes on that morning) and went through every problem.&lt;/p&gt;

&lt;p&gt;Tom opened his laptop and pulled up the customer data. “We need unit numbers. I’ll add a field to the signup form tonight.” He paused. “I should have thought of that.”&lt;/p&gt;

&lt;p&gt;“We all should have,” Maya said.&lt;/p&gt;

&lt;p&gt;Sam had a list on her phone. “The delivery window is non-negotiable. Thursday between 3pm and 7pm. Not Wednesday. Not whenever Callum feels like it. I’ll find a different courier if I have to.”&lt;/p&gt;

&lt;p&gt;“Can we afford a different courier?”&lt;/p&gt;

&lt;p&gt;“Can we afford to lose customers?”&lt;/p&gt;

&lt;p&gt;Maya conceded the point.&lt;/p&gt;

&lt;p&gt;They went through every failure. The spinach problem was timing; they’d packed too early. If they packed on Thursday morning and delivered Thursday afternoon, the produce would be hours old instead of a day old. Dave had told them this. They hadn’t listened, or rather, they’d listened and then let the logistics override what they’d heard.&lt;/p&gt;

&lt;p&gt;The address problem was data. Tom fixed the form that night, adding fields for unit number and delivery instructions. He also added a confirmation step that showed the customer their full address before they submitted, so they could catch errors.&lt;/p&gt;

&lt;p&gt;The delivery timing was Sam’s domain. She called three courier companies on Thursday morning and found one (a woman named Jen who ran a small delivery business in Fremantle) who could guarantee a Thursday afternoon window. Jen was more expensive than Callum, but she answered her phone, confirmed delivery times, and understood that fresh produce and hot doorsteps were a bad combination.&lt;/p&gt;

&lt;p&gt;By the following Thursday (week two), the process worked. Pack on Thursday morning. Deliver Thursday afternoon. Address data includes unit numbers. Jen delivers within the confirmed window. No boxes in the sun. No wrong-day deliveries. No wilted spinach.&lt;/p&gt;

&lt;p&gt;It wasn’t smooth. Sam spent Thursday afternoon texting Jen for updates. Maya called three customers to confirm their boxes had arrived. Tom refreshed the delivery tracker (a shared Google Sheet that was the entire “operations platform”) every fifteen minutes. But the boxes arrived. The produce was fresh. Nobody called to complain.&lt;/p&gt;

&lt;p&gt;Mrs Patterson emailed on Friday morning: “Much better this week! The carrots were beautiful.”&lt;/p&gt;

&lt;h3 id=&quot;week-three&quot;&gt;Week three&lt;/h3&gt;

&lt;p&gt;By week three, the process was routine. Pack at 6am Thursday. Jen picks up at 10am. Deliveries between 2pm and 5pm. Sam confirms each delivery by text. Tom monitors the payments. Maya handles the farm coordination: checking in with Dave and Rachel on Monday about what they’d have available, confirming quantities on Wednesday, adjusting the packing list if something fell short.&lt;/p&gt;

&lt;p&gt;The rhythm emerged not from a plan but from the accumulated learning of things that went wrong. Every mistake in week one became a process in week three. Unit numbers on the form. Packing order: heavy at the bottom, leafy on top. Delivery window confirmed 24 hours in advance. A shared spreadsheet tracking every box from packing to delivery.&lt;/p&gt;

&lt;p&gt;Sam started a simple feedback system: an email sent to every subscriber on Friday asking how their box was. Most people didn’t reply. The ones who did were either very happy or very specific about what they didn’t like. One subscriber requested no coriander. Another asked if they could get extra tomatoes. A third wanted to know which farm her carrots came from.&lt;/p&gt;

&lt;p&gt;Maya answered every email personally. She learned the subscribers’ names, their preferences, their quirks. Mrs Patterson didn’t like beetroot. A young couple in Northbridge were vegetarian and wanted more variety in leafy greens. A family in Cottesloe had three kids and needed quantity over variety. A retired teacher in Mosman Park wanted whatever Dave recommended, because she’d been buying from Dave at the market for years and trusted his judgement.&lt;/p&gt;

&lt;p&gt;These weren’t user personas on a whiteboard. They were real people with real kitchens and real opinions about coriander.&lt;/p&gt;

&lt;h3 id=&quot;the-email&quot;&gt;The email&lt;/h3&gt;

&lt;p&gt;On a Friday afternoon in the third week, an email arrived from a subscriber named Claire. It was three sentences long.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Hi Maya, just wanted to say thanks. I haven’t thought about what’s for dinner since I started getting the box. That’s worth more than the vegetables.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maya read it three times. She read it standing at the kitchen counter while Nadia made tea. She read it again before bed. She didn’t have the language for what Claire was describing, not yet. The phrase “job to be done” was months away. But she felt the shape of it. The box wasn’t about vegetables. It was about something else, something larger and harder to name, and Claire had just told her what it was.&lt;/p&gt;

&lt;p&gt;The box was about one fewer decision in a day full of decisions. Open the door, pick up the box, cook what’s inside. No planning, no shopping, no standing in a supermarket aisle at 6pm wondering what to have for dinner. Trust the box. Trust the farm. Trust Maya.&lt;/p&gt;

&lt;p&gt;She saved the email in a folder she named “Why We Do This.” It was the only email in the folder. Over the next year, it would have company.&lt;/p&gt;

&lt;h3 id=&quot;growth&quot;&gt;Growth&lt;/h3&gt;

&lt;p&gt;Twenty-two subscribers became twenty-six in week two. Word of mouth: the people who got the box told the people who didn’t. By week four, thirty-one. By week six, thirty-eight.&lt;/p&gt;

&lt;p&gt;Maya hadn’t run a single ad. The growth was entirely organic: market flyers, word of mouth, and a short piece in the local Fremantle newspaper that Sam had arranged by calling the editor and saying, “We’re three people packing vegetables on a kitchen table and delivering them to your neighbours. Want to write about it?”&lt;/p&gt;

&lt;p&gt;The editor did want to write about it. The article ran on a Wednesday and produced nine signups by Friday. Small numbers, but each one was a person who’d read about Greenbox and decided to trust a stranger with their weekly dinner.&lt;/p&gt;

&lt;p&gt;Thirty-eight subscribers was encouraging. It was also nowhere near enough. And the manual operation (Maya, Sam, and a kitchen table) couldn’t scale. Every Thursday was a full day of packing and coordinating. Maya was spending Monday on farm calls, Tuesday on the packing list, Wednesday on logistics, Thursday on packing and delivery, and Friday on customer emails. That left no time for anything else. Tom was building the platform as fast as he could, but the platform was for 200 subscribers and they needed to stop packing by hand long before then.&lt;/p&gt;

&lt;p&gt;The seed round would change that. Maya had been talking to investors since before the first box shipped. The terms were clear: reach 200 active subscribers within three months of funding, and the next round follows. Miss the target, and Greenbox is done.&lt;/p&gt;

&lt;p&gt;Two hundred. From thirty-eight. In twelve weeks.&lt;/p&gt;

&lt;p&gt;Once the money came in, they could hire a proper team, move out of the living room, and build the systems to replace the kitchen-table operation. The pilot subscribers (the thirty-eight people who’d trusted Maya with their Thursday dinners) would keep getting boxes through the manual process for now. But the platform Tom was building had to be ready before the numbers got any higher. You can hand-pack thirty-eight boxes on a kitchen table. You cannot hand-pack two hundred.&lt;/p&gt;

&lt;p&gt;Maya looked at the subscriber graph, a line on a spreadsheet that she checked every morning at 5am, before her run, before coffee, before anything else. The line was going up. Slowly. Steadily. But 200 was a long way from 38, and the gap between them was filled with packing days and delivery runs and emails about coriander and a team of three people who were already working as hard as they could.&lt;/p&gt;

&lt;p&gt;She needed more people. She needed an office. Nadia’s patience with the living room situation was genuine but not infinite. She needed a developer who wasn’t Tom, because Tom was one person and the codebase was growing faster than one person could manage. She needed money.&lt;/p&gt;

&lt;p&gt;The seed round had to close. And once it did, the clock started. Three months. Two hundred subscribers. Build the platform, grow the customer base, prove the model. Tom was already building as fast as he could, using LLMs to generate code at a pace that felt miraculous. They’d shipped a subscription system, a landing page, a basic farm portal, all in weeks.&lt;/p&gt;

&lt;p&gt;The question Maya couldn’t answer (the question that would define the next three months) was whether all that speed was pointed in the right direction.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Customer Discovery: Before the First Line of Code</title>
    <link href="/writing/customer-discovery-before-the-first-line-of-code/"/>
    <updated>2026-03-03T06:00:00+08:00</updated>
    <id>/writing/customer-discovery-before-the-first-line-of-code/</id>
    <content type="html">&lt;div class=&quot;series-banner&quot;&gt;
  &lt;span&gt;Part of &lt;a href=&quot;/writing/from-chaos-to-clarity/&quot;&gt;From Chaos to Clarity&lt;/a&gt; · &lt;a href=&quot;/writing/the-greenbox-story/&quot;&gt;The Greenbox Story&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;Maya’s earliest memories are of dirt under her fingernails and the sound of her father’s ute on the gravel road before dawn.&lt;/p&gt;

&lt;p&gt;The farm was sixty acres outside Margaret River: dairy originally, then mixed organic produce after her parents made the conversion. Her parents had emigrated from Taiwan in the early eighties, bought the cheapest land they could find in a place where nobody would tell them they didn’t belong, and built something with their hands. The conversion from dairy nearly bankrupted them. Two seasons of no income while they learned new skills. Her mother picking up extra shifts at the local school canteen. Her father up at four every morning, teaching himself soil chemistry from library books and a Mandarin agricultural manual he’d brought in his suitcase.&lt;/p&gt;

&lt;p&gt;By the time Maya was ten, the farm was producing vegetables that restaurants in Margaret River asked for by name. Her parents sold the rest at the Saturday farmers’ market: a trestle table, a hand-painted sign, and crates of whatever was in season. Maya worked the market from age twelve. She learned to make change, to explain what kohlrabi was, and to smile when tourists asked if the vegetables were “really organic” in a tone that meant they didn’t believe her.&lt;/p&gt;

&lt;p&gt;She also learned something about distribution that she wouldn’t have words for until much later: the gap between what the farm produced and what people could actually buy.&lt;/p&gt;

&lt;p&gt;Her parents grew beautiful produce. The restaurants took a small percentage. The market took a Saturday. The rest (the bulk of what they grew) went to a wholesaler who paid them barely enough to cover costs. The supermarkets took 40% margins and put their produce next to imported tomatoes from Queensland at half the price. The economics were brutal. The quality was irrelevant to anyone who wasn’t standing at the market stall on a Saturday morning, holding a bunch of carrots and tasting the difference.&lt;/p&gt;

&lt;p&gt;Maya left for Perth at eighteen. Computer science at UWA. She was good at it; the logical structure of code felt like a language she’d been waiting to learn. She graduated with honours, took a job at a consulting firm, and spent the next decade doing technical advisory work for companies that were always larger, richer, and less interesting than they appeared from the outside. She built systems for mining companies, insurance firms, a state government department that needed a new payroll system and took three years to get one. She learned to translate between business people and technical people. She learned that the hardest problems in software were never about software.&lt;/p&gt;

&lt;p&gt;She also learned to dress for offices, to present to boards, and to eat lunch at her desk without getting crumbs on client deliverables. She was good at consulting. She was not passionate about it. The difference matters less than you’d think in your twenties and more than you’d think in your thirties.&lt;/p&gt;

&lt;h3 id=&quot;the-idea&quot;&gt;The idea&lt;/h3&gt;

&lt;p&gt;The idea arrived the way most ideas arrive: not in a flash, but as a slow accumulation of irritation.&lt;/p&gt;

&lt;p&gt;Maya was thirty-one, living in a flat in Fremantle with her partner Nadia, buying vegetables from the supermarket on the way home from work. The tomatoes were pale and mealy. The lettuce was wrapped in plastic and had been picked four days ago in another state. She knew, because she’d grown up on a farm, because her hands remembered what a good tomato felt like, that there were farms within fifty kilometres growing produce that was better in every way. But she couldn’t get it. Not easily, not regularly, not without driving to a farmers’ market on a Saturday morning and hoping for the best.&lt;/p&gt;

&lt;p&gt;The farmers’ markets were good, but they were weekend-only. You had to plan your whole Saturday around them. And the farms themselves had no direct-to-consumer channel at all. Dave Morrison, a third-generation farmer near Margaret River whose family had been working the same soil since 1962, sold most of his crop to a wholesaler and whatever was left at the market. Rachel, who ran a smaller mixed farm nearby, did the same. Both of them produced food that was extraordinary. Neither of them had a way to get it to the people who would value it most.&lt;/p&gt;

&lt;p&gt;What if you could subscribe to a weekly box? Fresh seasonal vegetables, sourced from farms within fifty kilometres, delivered to your door every Thursday. Simple concept. The farms get a reliable buyer at a fair price. The customer gets produce they can trust without thinking about it. The middleman (the wholesaler, the supermarket) gets cut out.&lt;/p&gt;

&lt;p&gt;Maya wrote the idea on a napkin at a cafe in Fremantle. Then she wrote it again, more carefully, in a notebook. Then she opened a spreadsheet and started modelling costs. The spreadsheet grew over three months. She worked on it in the evenings after Nadia went to bed, sitting at the kitchen table with a cup of tea and the quiet focus of someone who knows they’re building something real.&lt;/p&gt;

&lt;h3 id=&quot;the-conversations&quot;&gt;The conversations&lt;/h3&gt;

&lt;p&gt;The first person she called was Dave Morrison.&lt;/p&gt;

&lt;p&gt;Maya had known Dave since childhood. Her parents and Dave’s family had sold produce at adjacent stalls at the Margaret River market for years. Dave was laconic, careful with words, and deeply sceptical of anything that came from the city. He was fifty-eight, had survived droughts, frosts, a global financial crisis, and two decades of supermarket price pressure. He’d seen the co-ops come and go. He’d watched startups promise to “disrupt” agriculture and then disappear when the venture capital ran out.&lt;/p&gt;

&lt;p&gt;“You’re not the first city kid with this idea,” he said, when Maya pitched it over the phone.&lt;/p&gt;

&lt;p&gt;“I’m not a city kid, Dave. You’ve known me since I was twelve.”&lt;/p&gt;

&lt;p&gt;A long pause. Dave’s pauses carried more information than most people’s paragraphs.&lt;/p&gt;

&lt;p&gt;“Fair point. But the idea’s still not new. I’ve watched three co-ops and two startups promise to fix farm distribution. All of them ran out of money.”&lt;/p&gt;

&lt;p&gt;“What was different about them?”&lt;/p&gt;

&lt;p&gt;Another pause. “They didn’t understand farming. They thought it was a supply chain problem. It’s not. It’s a relationship problem. Farms don’t produce on demand. We produce what the season gives us, and then we figure out who wants it.”&lt;/p&gt;

&lt;p&gt;“I know that.”&lt;/p&gt;

&lt;p&gt;“You know it because you grew up on a farm. They didn’t.”&lt;/p&gt;

&lt;p&gt;Dave didn’t say yes. He didn’t say no. He said: “Come down to the farm. Bring your spreadsheet. I’ll tell you what’s wrong with it.”&lt;/p&gt;

&lt;p&gt;Maya drove down on a Saturday. Dave walked her through the operation: the fields, the packing shed, the cold storage. He showed her the wholesale orders, the market prep, the waste. Produce that didn’t sell at market. Produce that was too small or too oddly shaped for the wholesaler. Produce that was perfect but had no buyer.&lt;/p&gt;

&lt;p&gt;“You see those crates?” Dave pointed to a stack of weathered green plastic crates by the packing shed door. The kind farms use everywhere: stackable, reusable, the colour of sun-faded gum leaves. “That’s what I send produce in. Twenty-odd years I’ve been using those crates. They go to market, they come home, they go out again.”&lt;/p&gt;

&lt;p&gt;Maya looked at the crates. Green, sturdy, practical. A farm thing. A real thing.&lt;/p&gt;

&lt;p&gt;“Greenbox,” she said.&lt;/p&gt;

&lt;p&gt;Dave raised an eyebrow.&lt;/p&gt;

&lt;p&gt;“That’s the name. Greenbox.”&lt;/p&gt;

&lt;p&gt;“It’s a crate.”&lt;/p&gt;

&lt;p&gt;“It’s a box. A green box. With produce in it, delivered to someone’s door.”&lt;/p&gt;

&lt;p&gt;Dave shook his head, but Maya saw the corner of his mouth twitch. That was as close to approval as Dave got.&lt;/p&gt;

&lt;h3 id=&quot;recruiting-tom&quot;&gt;Recruiting Tom&lt;/h3&gt;

&lt;p&gt;Tom Chen was Maya’s oldest friend from UWA. They’d met in a second-year algorithms tutorial. Maya was the only woman in the room and Tom was the only person who talked to her like a normal human being instead of either ignoring her or explaining things she already understood. They’d stayed friends through fifteen years of diverging careers: Maya into consulting, Tom into software development. He was thirty-eight now, married to Sarah, two kids (Ava and Leo), and the kind of programmer who built side projects after bedtime because making things was how he processed the world.&lt;/p&gt;

&lt;p&gt;Tom was between jobs. His last company had been acquired by a larger firm, the culture had rotted within six months, and he’d taken voluntary redundancy rather than spend another year in meetings about meetings. He was interviewing at two companies and felt lukewarm about both.&lt;/p&gt;

&lt;p&gt;Maya bought him coffee at a cafe in Leederville and pitched.&lt;/p&gt;

&lt;p&gt;Tom listened with the particular attentiveness of someone who builds systems for a living. He asked good questions. How many farms? What’s the delivery radius? How do you handle seasonal variation? What’s the tech stack?&lt;/p&gt;

&lt;p&gt;Maya answered what she could and was honest about what she couldn’t. “I don’t have all the answers. I’ve got a spreadsheet, a farming contact who hasn’t said no yet, and an idea that I can’t stop thinking about.”&lt;/p&gt;

&lt;p&gt;Tom stirred his coffee. “You know the success rate for food startups?”&lt;/p&gt;

&lt;p&gt;“I know it’s terrible.”&lt;/p&gt;

&lt;p&gt;“And you want me to leave a stable job market for this?”&lt;/p&gt;

&lt;p&gt;“You don’t have a stable job. You have two interviews you described as, what was the word, ‘uninspiring.’”&lt;/p&gt;

&lt;p&gt;Tom laughed. It was the first genuine laugh Maya had seen from him in months. “When do you need an answer?”&lt;/p&gt;

&lt;p&gt;“Yesterday.”&lt;/p&gt;

&lt;p&gt;He looked at his coffee. Then at Maya. Then at something in the middle distance that might have been the future or might have been the memory of all those side projects he’d built because the work that paid him wasn’t the work that interested him.&lt;/p&gt;

&lt;p&gt;“Yeah, all right. I’m in.”&lt;/p&gt;

&lt;h3 id=&quot;sam&quot;&gt;Sam&lt;/h3&gt;

&lt;p&gt;Sam Okafor was Maya’s cousin on her mother’s side; her mother’s sister had married a Nigerian engineer who’d moved to Perth in the nineties. Sam had grown up in Baldivis, studied business, and spent six years running logistics for a trucking company in Kewdale. She knew supply chains the way Tom knew code: from the inside, with the kind of practical knowledge that doesn’t come from textbooks.&lt;/p&gt;

&lt;p&gt;Sam was twenty-nine, competent, restless, and thoroughly bored. The trucking company moved the same cargo along the same routes on the same schedule, and the only variation was which driver called in sick. She’d been talking about leaving for a year. When Maya called, Sam didn’t need the full pitch.&lt;/p&gt;

&lt;p&gt;“What’s the job?”&lt;/p&gt;

&lt;p&gt;“Everything that isn’t code or farming. Marketing. Operations. Customer support. Logistics.”&lt;/p&gt;

&lt;p&gt;“That’s four jobs.”&lt;/p&gt;

&lt;p&gt;“It’s a startup. Everything is four jobs.”&lt;/p&gt;

&lt;p&gt;Sam was quiet for a moment. “What’s the pay?”&lt;/p&gt;

&lt;p&gt;Maya told her. Sam made a sound that was somewhere between a laugh and a cough.&lt;/p&gt;

&lt;p&gt;“That’s a 60% pay cut.”&lt;/p&gt;

&lt;p&gt;“I know. I’m asking a lot.”&lt;/p&gt;

&lt;p&gt;“You’re asking me to give up a salary to pack vegetables in your living room.”&lt;/p&gt;

&lt;p&gt;“I’m asking you to help me build something that matters. The salary comes later. If it works.”&lt;/p&gt;

&lt;p&gt;Sam thought about the trucking company. The same routes. The same cargo. The same conversations in the same break room. She thought about the spreadsheet Maya had shown her over family dinner last month: the one with the revenue projections and the subscriber targets and the note at the bottom that said &lt;em&gt;Break-even: Month 14 (optimistic)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;“When do I start?”&lt;/p&gt;

&lt;p&gt;“Monday.”&lt;/p&gt;

&lt;h3 id=&quot;the-living-room&quot;&gt;The living room&lt;/h3&gt;

&lt;p&gt;They started on a Monday in February. Three people, three laptops, Maya’s living room in Fremantle. The coffee table was their desk. The whiteboard was a sheet of butcher’s paper taped to the wall behind the couch. Nadia, who worked as a physiotherapist and kept sensible hours, came home that first evening to find her living room converted into an office.&lt;/p&gt;

&lt;p&gt;“How long is this going to last?” she asked, stepping over a power cable.&lt;/p&gt;

&lt;p&gt;“Not long. We’ll get an office soon.”&lt;/p&gt;

&lt;p&gt;Nadia looked at the three people hunched over laptops on her couch. “Define ‘soon.’”&lt;/p&gt;

&lt;p&gt;“A few weeks?”&lt;/p&gt;

&lt;p&gt;It was six weeks. Nadia never complained, though she did start leaving passive-aggressive notes on the fridge about the milk disappearing faster than usual. She also started making extra coffee in the mornings, enough for four, without being asked. That was Nadia. She expressed love in practical gestures and expected Maya to understand what they meant.&lt;/p&gt;

&lt;p&gt;The first week was all planning. Maya laid out the business model on the butcher’s paper in her neat handwriting (the same handwriting from the market flyer): farms commit weekly availability, customers subscribe to a box size, Greenbox matches supply to demand, packs the boxes, and delivers. Revenue comes from the subscription margin: the difference between what they pay the farms and what the customer pays. Simple, she said. Straightforward.&lt;/p&gt;

&lt;p&gt;Tom and Sam looked at each other. They’d both been around long enough to know that “simple” and “straightforward” were the words people used right before discovering that something was neither.&lt;/p&gt;

&lt;p&gt;Tom listened and started sketching a data model on the butcher’s paper. Subscription. Customer. Farm. Produce. Order. Box. The entities came easily. The relationships between them were where the complexity lived.&lt;/p&gt;

&lt;p&gt;Sam started on logistics. Delivery routes. Courier options. Packing materials. Cold chain timing: how long could produce sit in a box before it deteriorated? She called four courier companies and got quotes that ranged from expensive to absurd. One of them wanted a minimum of two hundred deliveries per week. Sam explained they’d be starting with about twenty. The line went quiet, then polite. She started a spreadsheet that would, over the next year, become the operational backbone of the company. It had twelve tabs by Friday.&lt;/p&gt;

&lt;p&gt;Maya called Dave. “We’re starting.”&lt;/p&gt;

&lt;p&gt;“Starting what?”&lt;/p&gt;

&lt;p&gt;“Building it. The app, the website, the operations. All of it.”&lt;/p&gt;

&lt;p&gt;A pause. “You haven’t got any customers yet.”&lt;/p&gt;

&lt;p&gt;“We will.”&lt;/p&gt;

&lt;p&gt;“Lot of confidence for someone with three laptops and no office.”&lt;/p&gt;

&lt;p&gt;“We’ve got a living room. It’s practically the same thing.”&lt;/p&gt;

&lt;p&gt;Dave’s silence was eloquent. Then: “I’ll have some produce ready when you need it. Don’t make me regret it.”&lt;/p&gt;

&lt;p&gt;Maya put the phone on the kitchen counter and looked at Tom and Sam. “He’s in.”&lt;/p&gt;

&lt;p&gt;“That didn’t sound like ‘in,’” Tom said.&lt;/p&gt;

&lt;p&gt;“For Dave, that was a standing ovation.”&lt;/p&gt;

&lt;p&gt;By Friday of the first week, Tom had a rough architecture sketched out. A web app for customer subscriptions. A portal for farms to submit their weekly availability. A matching engine to connect supply to demand. A basic admin panel for Maya to manage everything else. He’d been researching LLM-assisted development. The new code generation tools were getting impressive reviews, and he was itching to try them on a real project.&lt;/p&gt;

&lt;p&gt;“I reckon I can have a working prototype in two weeks,” he said.&lt;/p&gt;

&lt;p&gt;Sam raised her eyebrows. “Two weeks?”&lt;/p&gt;

&lt;p&gt;“The code generation tools are incredible. You describe what you want and they build it. I saw a demo where a guy built a full e-commerce site in an afternoon.”&lt;/p&gt;

&lt;p&gt;Maya looked at the butcher’s paper covered in entity relationships and arrows and questions. “That sounds fast.”&lt;/p&gt;

&lt;p&gt;“That’s the point.”&lt;/p&gt;

&lt;p&gt;The following Monday, Tom opened his laptop, fired up Claude in one browser tab and his IDE in the other, and started building.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Bash Pipes Execute in Subshells</title>
    <link href="/2014/10/02/bash-pipes-execute-in-subshells/"/>
    <updated>2014-10-02T00:00:00+08:00</updated>
    <id>/2014/10/02/bash-pipes-execute-in-subshells/</id>
    <content type="html">&lt;p&gt;Here’s a gotcha that caught me out this week.&lt;/p&gt;

&lt;p&gt;I had code like this, used to source settings from scripts stored in another directory:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;find /etc/application &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;*.sh&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f | &lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;read &lt;/span&gt;FILE&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$FILE&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; /path/to/application/run.sh&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/application/set_name.sh&lt;/code&gt; I’d have something like:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;SOME_VARIABLE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;some value&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;But when the application ran, it never saw the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SOME_VARIABLE&lt;/code&gt;. Puzzling.&lt;/p&gt;

&lt;p&gt;The reason: bash pipes run in subshells. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while read&lt;/code&gt; loop on the right side of the pipe runs in a subshell, so that’s where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; executes. And subshells can’t modify the environment of their parent process. The exported variables vanish the moment the subshell exits.&lt;/p&gt;

&lt;p&gt;The fix is to make sure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; runs in the main process. You can do this with process substitution and input redirection instead of a pipe:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;read &lt;/span&gt;FILE&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$FILE&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt; &amp;lt; &amp;lt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;find /etc/application &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;*.sh&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; /path/to/application/run.sh&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while read&lt;/code&gt; loop runs in the main shell, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; sets the variables in the right place, and the application sees everything it expects.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Pooling ActiveMQ Connections for Camel</title>
    <link href="/2012/09/30/pooling-activemq-connections-for-camel/"/>
    <updated>2012-09-30T00:00:00+08:00</updated>
    <id>/2012/09/30/pooling-activemq-connections-for-camel/</id>
    <content type="html">&lt;p&gt;In &lt;a href=&quot;/2012/09/10/a-basic-servicemix-install&quot;&gt;my previous camel.xml&lt;/a&gt; I used the following XML to set up the connection to ActiveMQ:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;    &lt;span class=&quot;nt&quot;&gt;&amp;lt;bean&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;activemq&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.apache.activemq.camel.component.ActiveMQComponent&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;connectionFactory&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;&amp;lt;bean&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.apache.activemq.ActiveMQConnectionFactory&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
	  &lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;brokerURL&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;vm://zuu:61613?create=false&amp;amp;amp;waitForStart=10000&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;&amp;lt;/bean&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/bean&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;While this works, every time a message is sent Camel opens a new connection to the broker. I know I’m going to be sending a lot of messages, and I’d rather not waste time opening and closing connections for each one. A connection pool is the obvious fix.&lt;/p&gt;

&lt;p&gt;By wrapping the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActiveMQConnectionFactory&lt;/code&gt; in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PooledConnectionFactory&lt;/code&gt;, I can maintain a pool of up to 8 connections that stay open and get returned to the pool (rather than closed) after each message is sent:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;    &lt;span class=&quot;nt&quot;&gt;&amp;lt;bean&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;activemq&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.apache.activemq.camel.component.ActiveMQComponent&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;connectionFactory&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;&amp;lt;bean&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;pooledConnectionFactory&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.apache.activemq.pool.PooledConnectionFactory&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
	  &lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;maxConnections&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;8&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
	  &lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;connectionFactory&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
	    &lt;span class=&quot;nt&quot;&gt;&amp;lt;bean&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.apache.activemq.ActiveMQConnectionFactory&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
	      &lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;brokerURL&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;vm://zuu:61613?create=false&amp;amp;amp;waitForStart=10000&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
	    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/bean&amp;gt;&lt;/span&gt;
	  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;&amp;lt;/bean&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/bean&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;A small change, but it makes a real difference under load.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>A Basic ServiceMix Install</title>
    <link href="/2012/09/10/a-basic-servicemix-install/"/>
    <updated>2012-09-10T00:00:00+08:00</updated>
    <id>/2012/09/10/a-basic-servicemix-install/</id>
    <content type="html">&lt;p&gt;Over the past several years I’ve frequently used ActiveMQ and Camel as a message broker and integration platform for my applications. They handle the glue and the message delivery so I can focus on what’s really interesting: solving business problems. Apache ServiceMix provides an OSGi container in which I can run, configure, and manage Camel and ActiveMQ instances, and I want to explore the other services it can provide.&lt;/p&gt;

&lt;p&gt;The full ServiceMix install is rather large. I don’t need most of it yet, and I don’t want to be running services I don’t understand, so I’m starting with a very minimal install and building from there.&lt;/p&gt;

&lt;p&gt;At the time of writing the most recent ServiceMix release is 4.4.2, so I’ll download, unpack, and run that:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ curl -L -O http://www.mirrorservice.org/sites/ftp.apache.org/servicemix/servicemix-4/4.4.2/apache-servicemix-minimal-4.4.2.tar.gz
$ tar -xzvf apache-servicemix-minimal-4.4.2.tar.gz
$ cd apache-servicemix-4.4.2/
$ ./bin/servicemix
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s verify what ships in the minimal install and make sure there are no surprises:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;karaf@root&amp;gt; features:list --installed
State         Version   Name            Repository  Description
[installed  ] [2.2.4  ] karaf-framework karaf-2.2.4
[installed  ] [2.2.4  ] config          karaf-2.2.4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Not much — just a basic Karaf install, pre-configured with the ServiceMix Maven repositories:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;karaf@root&amp;gt; features:listurl
 Loaded   URI
  true    mvn:org.apache.karaf.assemblies.features/standard/2.2.4/xml/features
  true    mvn:org.apache.servicemix/apache-servicemix/4.4.2/xml/features
  true    mvn:org.apache.activemq/activemq-karaf/5.5.1/xml/features
  true    mvn:org.apache.camel.karaf/apache-camel/2.8.5/xml/features
  true    mvn:org.apache.cxf.karaf/apache-cxf/2.4.6/xml/features
  true    mvn:org.apache.karaf.assemblies.features/enterprise/2.2.4/xml/features
  true    mvn:org.apache.servicemix.nmr/apache-servicemix-nmr/1.5.0/xml/features
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I can build on this by adding the features I need. To start, I definitely need Camel and ActiveMQ since those are the foundation of my integration layer. I’m used to configuring them with Spring, so I’ll use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*-spring&lt;/code&gt; variants rather than the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*-blueprint&lt;/code&gt; variants more commonly used in ServiceMix.&lt;/p&gt;

&lt;p&gt;First, I need to install some OSGi bundles that Camel depends on. I’m not yet sure how to configure ServiceMix to pull these in automatically — I suspect I need to add the correct feature URL, but I haven’t figured out which one. Please &lt;a href=&quot;mailto:craig@barkingiguana.com&quot;&gt;get in touch&lt;/a&gt; if you can explain.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;karaf@root&amp;gt; osgi:install -s mvn:org.apache.geronimo.specs/geronimo-activation_1.1_spec/1.0.2
karaf@root&amp;gt; osgi:install -s mvn:org.apache.servicemix.specs/org.apache.servicemix.specs.stax-api-1.0/1.1.0
karaf@root&amp;gt; osgi:install -s mvn:org.apache.servicemix.specs/org.apache.servicemix.specs.jaxb-api-2.1/1.1.0
karaf@root&amp;gt; osgi:install -s mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.jaxb-impl/2.1.6_1
karaf@root&amp;gt; osgi:install -s mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.xstream/1.3_4
karaf@root&amp;gt; osgi:install -s mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.joda-time/1.5.2_3
karaf@root&amp;gt; osgi:install -s mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.jdom/1.1_3
karaf@root&amp;gt; osgi:install -s mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.dom4j/1.6.1_3
karaf@root&amp;gt; osgi:install -s mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.xstream/1.3_4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now I can install ActiveMQ and Camel:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;karaf@root&amp;gt; features:install spring
karaf@root&amp;gt; features:install camel-core
karaf@root&amp;gt; features:install camel-spring
karaf@root&amp;gt; features:install activemq-spring
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I also need the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;camel-activemq&lt;/code&gt; component so Camel can talk to ActiveMQ:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;karaf@root&amp;gt; features:install camel-activemq
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With everything installed, I set up the broker. I’m telling it to use the name &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zuu&lt;/code&gt; (the name of my laptop, but it can be anything):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;karaf@root&amp;gt; activemq:create-broker --name zuu

Creating file: @|green /Users/craig/code/tmp/apache-servicemix-4.4.2/deploy/zuu-broker.xml|

Default ActiveMQ Broker (zuu) configuration file created at: /Users/craig/code/tmp/apache-servicemix-4.4.2/deploy/zuu-broker.xml
Please review the configuration and modify to suite your needs.

0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The default configuration sets up a Stomp transport on port 61613, which I’ll use from my Ruby (and other language) clients. No changes needed, although I could remove the OpenWire connector on port 61616 if I wanted.&lt;/p&gt;

&lt;p&gt;Configuring Camel is a touch more involved. I need to drop a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;camel.xml&lt;/code&gt; file into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy/&lt;/code&gt; subdirectory of the ServiceMix install:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;beans&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://www.springframework.org/schema/beans&quot;&lt;/span&gt;
 &lt;span class=&quot;na&quot;&gt;xmlns:xsi=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&lt;/span&gt;
 &lt;span class=&quot;na&quot;&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring-2.8.5.xsd&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;camelContext&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;camel&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://camel.apache.org/schema/spring&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;route&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;tick-tock&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;from&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uri=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;timer://tick-tock-timer?fixedRate=true&amp;amp;amp;period=5000&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;to&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uri=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;log:tick-tock-log&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;to&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uri=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;activemq:topic:tick-tock&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/route&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/camelContext&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;bean&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;activemq&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.apache.activemq.camel.component.ActiveMQComponent&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;connectionFactory&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;bean&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.apache.activemq.ActiveMQConnectionFactory&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;brokerURL&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;vm://zuu?create=false&amp;amp;amp;waitForStart=10000&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;userName&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;${activemq.username}&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
	&lt;span class=&quot;nt&quot;&gt;&amp;lt;property&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;${activemq.password}&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/bean&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/bean&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/beans&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;I can verify the route is running by checking the logs:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;karaf@root&amp;gt; log:tail
2012-09-10 22:39:19,603 | INFO  | tick-tock-timer  | tick-tock-log      | ? ? | 54 - org.apache.camel.camel-core - 2.8.5 | Exchange[ExchangePattern:InOnly, BodyType:null, Body:[Body is null]]
2012-09-10 22:39:19,603 | INFO  | tick-tock-timer  | TransportConnector | ? ? | 79 - org.apache.activemq.activemq-core - 5.5.1 | Connector vm://zuu Started
2012-09-10 22:39:19,606 | INFO  | tick-tock-timer  | TransportConnector | ? ? | 79 - org.apache.activemq.activemq-core - 5.5.1 | Connector vm://zuu Stopped
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I can also hook up a Ruby client to listen to the topic:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;rubygems&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;stomp&apos;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;STDOUT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sync&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Stomp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;stomp://127.0.0.1:61613&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;subscribe&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;/topic/tick-tock&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inspect&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Running that produces a steady stream of messages in the console:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ruby ./client.rb
{&quot;message-id&quot;=&amp;gt;&quot;ID:zuu.local-53690-1347226528097-2:42161:1:1:1&quot;, &quot;breadcrumbId&quot;=&amp;gt;&quot;ID-zuu-local-54049-1347228987046-12-310&quot;, &quot;destination&quot;=&amp;gt;&quot;/topic/tick-tock&quot;, &quot;timestamp&quot;=&amp;gt;&quot;1347313904606&quot;, &quot;expires&quot;=&amp;gt;&quot;0&quot;, &quot;subscription&quot;=&amp;gt;&quot;587e9bbe3714dfd10b3cfe9837a1fb7daac2d8b2&quot;, &quot;priority&quot;=&amp;gt;&quot;4&quot;, &quot;firedTime&quot;=&amp;gt;&quot;Mon Sep 10 22:51:44 BST 2012&quot;}
{&quot;message-id&quot;=&amp;gt;&quot;ID:zuu.local-53690-1347226528097-2:42162:1:1:1&quot;, &quot;breadcrumbId&quot;=&amp;gt;&quot;ID-zuu-local-54049-1347228987046-12-312&quot;, &quot;destination&quot;=&amp;gt;&quot;/topic/tick-tock&quot;, &quot;timestamp&quot;=&amp;gt;&quot;1347313909605&quot;, &quot;expires&quot;=&amp;gt;&quot;0&quot;, &quot;subscription&quot;=&amp;gt;&quot;587e9bbe3714dfd10b3cfe9837a1fb7daac2d8b2&quot;, &quot;priority&quot;=&amp;gt;&quot;4&quot;, &quot;firedTime&quot;=&amp;gt;&quot;Mon Sep 10 22:51:49 BST 2012&quot;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I can now tinker with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zuu-broker.xml&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;camel.xml&lt;/code&gt;, and every time I save, ServiceMix picks up the change and restarts the appropriate bundle.&lt;/p&gt;

&lt;p&gt;I now have a basic ServiceMix install providing what I’m used to. Time to explore.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Logging Considerations</title>
    <link href="/2011/12/14/logging-considerations/"/>
    <updated>2011-12-14T00:00:00+08:00</updated>
    <id>/2011/12/14/logging-considerations/</id>
    <content type="html">&lt;p&gt;From years of bitter experience staring at log files trying to work out what turned the servers into a pile of molten rubble, I’ve built up a list of what I really like to see when a process logs its activity. Prompted by some discussions at work, I’d like to share it — in the hope of raising the quality of logging in our software and saving someone a lot of stress at 3am when their project absolutely refuses to work and they can’t figure out why.&lt;/p&gt;

&lt;p&gt;This is explicitly not about logging frameworks. I don’t particularly care how logging is implemented in code, since that will necessarily differ by language and application architecture. I just care about the outcome.&lt;/p&gt;

&lt;h3 id=&quot;each-process-logs-to-one-log-file&quot;&gt;Each process logs to one log file&lt;/h3&gt;

&lt;p&gt;I don’t want to jump back and forth between log files, interleaving lines, trying to reconstruct what happened when. It’s hard enough to work out what’s going on at the best of times. Let’s not make it harder.&lt;/p&gt;

&lt;h3 id=&quot;each-log-file-has-one-process-one-thread-writing-to-it&quot;&gt;Each log file has one process, one thread writing to it&lt;/h3&gt;

&lt;p&gt;When two or more processes or threads write to the same file, it’s difficult to isolate what the process you care about actually did. You can work around this by adding a token to each log line — a process or thread ID, for instance — but there are deeper complications.&lt;/p&gt;

&lt;p&gt;Say thread A logs something at exactly the same time as thread B. Halfway through thread A writing its log entry, thread B becomes active and starts logging. Thread B eventually yields, and thread A resumes. What a mess. Thread A’s log entry now has thread B’s log entry spliced right through the middle. Who said what? Nightmare.&lt;/p&gt;

&lt;p&gt;When you can’t avoid having multiple threads or processes writing to a log file, they should talk to a logging arbitrator service that manages the file and ensures entries are written atomically.&lt;/p&gt;

&lt;h3 id=&quot;logging-is-not-buffered&quot;&gt;Logging is not buffered&lt;/h3&gt;

&lt;p&gt;When it comes to logging, I prefer completeness over speed. If the process dies or is killed, I don’t want the last few log entries sitting in a memory buffer somewhere — I want them on disk where I can read them. If my process does something, I should be able to see it immediately, not after waiting for a buffer to fill or a flush interval to expire.&lt;/p&gt;

&lt;h3 id=&quot;each-log-line-has-a-timestamp&quot;&gt;Each log line has a timestamp&lt;/h3&gt;

&lt;p&gt;This seems obvious, but I’m amazed by how often it doesn’t happen. A log file without timestamps is useless unless you happen to be watching it when something goes wrong.&lt;/p&gt;

&lt;h3 id=&quot;each-timestamp-is-at-sub-second-resolution&quot;&gt;Each timestamp is at sub-second resolution&lt;/h3&gt;

&lt;p&gt;Logging with timestamps accurate only to one second is maddening when you’re dealing with thousands of entries per second. Which of those caused the issue? Good luck.&lt;/p&gt;

&lt;p&gt;Given the choice, I’d prefer to let me configure the logger to print to STDOUT so I can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svlogd&lt;/code&gt; to add tai64n timestamps (and do much more besides). But just do something reasonably sane and I’ll be happy.&lt;/p&gt;

&lt;h3 id=&quot;each-log-file-has-a-guaranteed-maximum-size&quot;&gt;Each log file has a guaranteed maximum size&lt;/h3&gt;

&lt;p&gt;Ever seen what happens when a process tries to start and the disk is full of old logs? Generally, it doesn’t work. That’s annoying.&lt;/p&gt;

&lt;p&gt;Older logs should be archived elsewhere. Only recent logs should live on local disk for easy debugging. Your definition of “older” and “recent” will vary, but knowing you’re keeping, say, 1GB of logs on disk means you can ensure there’s always enough space.&lt;/p&gt;

&lt;p&gt;Ad hoc or interval-based log rotation doesn’t help here, because by the time the log is rotated the disk is already full. Processes like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svlogd&lt;/code&gt; and some syslog implementations handle this properly.&lt;/p&gt;

&lt;h3 id=&quot;theres-some-way-of-processing-a-log-file-once-it-reaches-a-known-size&quot;&gt;There’s some way of processing a log file once it reaches a known size&lt;/h3&gt;

&lt;p&gt;At some point I’ll want to analyse and archive log files. It’s annoying to miss a rotation trigger and discover I’ve lost several hours of entries. Please don’t make me track file rotation myself. I’ll do it badly, and that will make me sad.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Most of these preferences are legacies of working with (and being spoiled by) DaemonTools and Runit, coming from a Rails-centric, Mongrel-running world where there’s typically one process logging to one file. If I’ve missed something, please let me know — email address is below.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>How I Structure RubyGems</title>
    <link href="/2011/12/13/how-i-structure-rubygems/"/>
    <updated>2011-12-13T00:00:00+08:00</updated>
    <id>/2011/12/13/how-i-structure-rubygems/</id>
    <content type="html">&lt;p&gt;I haven’t been consistent in how I structure my RubyGems, and I want to be. Consistency means I know what to provide, and people who use my code know what to expect.&lt;/p&gt;

&lt;p&gt;These are guidelines for my future self.&lt;/p&gt;

&lt;h2 id=&quot;the-require-statement-follows-the-gem-name&quot;&gt;The require statement follows the gem name&lt;/h2&gt;

&lt;p&gt;You should be able to figure out the require path just by looking at the gem name:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A gem called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require &quot;nyan_cat&quot;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;A gem called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan-cat&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require &quot;nyan/cat&quot;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;A gem called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat-moar_cats&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require &quot;nyan_cat/moar_cats&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;file-structure&quot;&gt;File structure&lt;/h2&gt;

&lt;p&gt;A basic project layout should look like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;your-rubygem/
            |- bin/
            |- lib/
            |- tests/
            |- Gemfile
            |- Rakefile
            |- README
            |- LICENCE
            \- your-rubygem.gemspec
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The code that provides your gem’s functionality lives under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/&lt;/code&gt; in a directory named according to these rules:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A gem called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;A gem called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan-cat&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/nyan/&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;A gem called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat-moar_cats&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/nyan_cat/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The file required by the gem name rule above should sit directly under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/nyan_cat.rb&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan-cat&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/nyan/cat.rb&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat-moar_cats&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/nyan_cat/moar_cats.rb&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This file should &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require&lt;/code&gt; everything needed for the gem to work.&lt;/p&gt;

&lt;p&gt;The Gemfile should contain just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gemspec&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; should not be checked in for gems. Yehuda Katz has a good write-up on &lt;a href=&quot;http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/&quot;&gt;the roles of the gemspec and Gemfile&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;code-structure-and-namespace&quot;&gt;Code structure and namespace&lt;/h2&gt;

&lt;p&gt;Your gem should have a namespace that matches the directory structure:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NyanCat&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan-cat&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Nyan::Cat&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat-moar_cats&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NyanCat::MoarCats&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything should live under this namespace.&lt;/p&gt;

&lt;h2 id=&quot;versioning&quot;&gt;Versioning&lt;/h2&gt;

&lt;p&gt;Provide a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version.rb&lt;/code&gt; file containing the current version and nothing else. Be kind to the people who depend on your gem — stick to the &lt;a href=&quot;http://semver.org/&quot;&gt;Semantic Versioning&lt;/a&gt; scheme.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/nyan_cat/version.rb&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan-cat&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/nyan/cat/version.rb&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat-moar_cats&lt;/code&gt; -&amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/nyan_cat/moar_cats/version.rb&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An example &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version.rb&lt;/code&gt; for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nyan_cat-moar_cats&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;module NyanCat
  module MoarCats
    VERSION = &quot;0.0.1&quot;
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;tests&quot;&gt;Tests&lt;/h2&gt;

&lt;p&gt;Your code should be tested. You don’t need to distribute the tests in the gem file itself, though.&lt;/p&gt;

&lt;h2 id=&quot;logging&quot;&gt;Logging&lt;/h2&gt;

&lt;p&gt;Unless you’re providing a logger implementation, it’s not your job to configure logging. Logging is good and incredibly useful for debugging, so the answer isn’t to avoid it. What I want is to give your code a logger that &lt;em&gt;I’ve&lt;/em&gt; configured to my liking. It will support the standard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Logger&lt;/code&gt; interface. Please make the logger an option — let me pass mine to you — and stop worrying about logging configuration.&lt;/p&gt;

&lt;p&gt;You can do this easily by defaulting to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NullLogger&lt;/code&gt; when no logger is provided:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;require &quot;null_logger&quot;

class Foo
  attr_accessor :logger
  private :logger=, :logger

  def initialize bar, options = {}
    self.logger = options[:logger] || NullLogger.instance
  end

  def quux
    logger.info &quot;Called #quux&quot;
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Find out more about NullLogger at &lt;a href=&quot;http://github.com/craigw/null_logger&quot;&gt;http://github.com/craigw/null_logger&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;dependencies&quot;&gt;Dependencies&lt;/h2&gt;

&lt;p&gt;Read about your dependencies’ versioning schemes. If they use &lt;a href=&quot;http://semver.org/&quot;&gt;Semantic Versioning&lt;/a&gt; (and hopefully they do), depend on the appropriate version. Read about &lt;a href=&quot;http://blog.davidchelimsky.net/2011/05/28/rake-09-and-gem-version-constraints/&quot;&gt;using the pessimistic version constraint operator&lt;/a&gt; to depend on major, minor, or exact versions as appropriate.&lt;/p&gt;

&lt;h2 id=&quot;rake-tasks&quot;&gt;Rake tasks&lt;/h2&gt;

&lt;p&gt;Provide tasks to run your tests. The default rake task should run all tests.&lt;/p&gt;

&lt;h2 id=&quot;readme&quot;&gt;README&lt;/h2&gt;

&lt;p&gt;Include at minimum:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A brief description of the gem, ideally with an example of the problem it solves&lt;/li&gt;
  &lt;li&gt;Installation instructions, even if they’re just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gem install foo&lt;/code&gt; or “add this to your Gemfile”&lt;/li&gt;
  &lt;li&gt;A brief usage example, possibly with a link to more detailed documentation&lt;/li&gt;
  &lt;li&gt;Licensing info, even if it’s just “see the LICENCE file”&lt;/li&gt;
  &lt;li&gt;A “how to contribute” section explaining how to submit patches&lt;/li&gt;
  &lt;li&gt;A list of authors (it’s nice to see your name there)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;licensing&quot;&gt;Licensing&lt;/h2&gt;

&lt;p&gt;If you don’t provide a licence, I can’t use your project, because I don’t know the terms under which it’s available. I really want to use your project. Please provide a licence.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Principles of Service Design: Program to an Interface</title>
    <link href="/2011/12/05/principles-of-service-design-program-to-an-interface/"/>
    <updated>2011-12-05T00:00:00+08:00</updated>
    <id>/2011/12/05/principles-of-service-design-program-to-an-interface/</id>
    <content type="html">&lt;p&gt;I’ve been thinking a lot about service design recently, and one of the trickier problems is deciding how to implement and version a service so that supporting (or dropping) older versions is straightforward. It turns out that advice originally meant for writing code works brilliantly for writing services too: program to an interface.&lt;/p&gt;

&lt;h2 id=&quot;program-to-an-interface&quot;&gt;Program to an Interface&lt;/h2&gt;

&lt;p&gt;Borrowing from many blogs and books, I’ve come to believe that viewing a service interface the same way you’d view a programming interface is the correct move. Interfaces can be versioned. They isolate client code from the implementation behind them. And once published, a given version should be immutable.&lt;/p&gt;

&lt;p&gt;A service should hide its implementation details. If a database table changes inside the application providing the service, the clients of that service shouldn’t have to care.&lt;/p&gt;

&lt;p&gt;Just like interfaces in a programming language, by specifying a well-known interface to a service we free ourselves from worrying about how clients interact with it. When we need to change the implementation, we can do so without breaking anyone. And the reverse is also true: clients don’t need to worry about implementation changes as long as the interface stays consistent.&lt;/p&gt;

&lt;p&gt;Of course, interfaces sometimes have to change to support new functionality. When they do, we want to be confident we’re using the correct version. Just because v2 has been released doesn’t mean our clients automatically support it. We want to keep using v1 until we’re ready to update. Versioning gives us that choice.&lt;/p&gt;

&lt;h2 id=&quot;beyond-uris&quot;&gt;Beyond URIs&lt;/h2&gt;

&lt;p&gt;When we think of a service, we usually think of a web service. In these RESTful days the interface is generally thought to be the combination of URIs we interact with. But that’s not the full picture. Supporting an interface doesn’t just mean your URIs are stable between versions — it also means the content returned from service calls (i.e. HTTP responses) conforms to a defined structure.&lt;/p&gt;

&lt;p&gt;And of course, a web service is only one kind of service. Plenty of services don’t have a web interface at all, usually in situations where synchronous request-response messaging isn’t appropriate. Order processing, inventory management, fraud checks — these might take several seconds and are better handled asynchronously. The interfaces to these services should be versioned for the same reasons a web service’s should.&lt;/p&gt;

&lt;p&gt;The version of the interface should be detectable with each message passed or received, no matter the transport. We should &lt;em&gt;know&lt;/em&gt; that we’re dealing with version 3 of an API, not guess.&lt;/p&gt;

&lt;h2 id=&quot;mime-types-to-the-rescue&quot;&gt;Mime types to the rescue&lt;/h2&gt;

&lt;p&gt;Handily, versioning interfaces for these types of services is pretty much the ideal use case for a MIME type, and most message transports support custom MIME types:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17&quot;&gt;HTTP 1.1 supports a Content-Type header&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;As does &lt;a href=&quot;http://stomp.github.com/stomp-specification-1.1.html#Header_content-type&quot;&gt;Stomp 1.1&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;And &lt;a href=&quot;http://www.amqp.org/confluence/download/attachments/720900/amqp.pdf?version=1&amp;amp;modificationDate=1318011006000&quot;&gt;AMQP 1.0&lt;/a&gt; (search for “content-type”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIME types have a space reserved for vendor-specific types, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/vnd&lt;/code&gt;, inside which we’re free to define our own. There are a few conventions to follow to avoid name collisions: include your organisation name, a very short description of what you’re representing, a version number, and a base format.&lt;/p&gt;

&lt;h2 id=&quot;a-worked-example&quot;&gt;A worked example&lt;/h2&gt;

&lt;p&gt;Say you work at the Acme Toy Company. When your web service accepts an order via its RESTful interface, it puts a message on a queue with four fields — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customer_id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;purchase_order_id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;amount&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;description&lt;/code&gt; — in JSON:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;customer_id&quot;: 123,
  &quot;purchase_order_id&quot;: &quot;ASLA-001-2031&quot;,
  &quot;amount&quot;: 1000,
  &quot;description&quot;: &quot;100 x Acme Toy Dynamite&quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We coin a MIME type, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/vnd.acme.order-v1+json&lt;/code&gt;, and publish an interface specification saying any message claiming to be this type will have these four fields. Then in a consumer of the orders queue we use the &lt;a href=&quot;http://eaipatterns.com/MessageSelector.html&quot;&gt;Selective Consumer&lt;/a&gt; pattern to subscribe only to messages of this MIME type. Inside the consumer we can be confident that we’ll only receive orders in a format we understand and can process. Partners can POST with this MIME type in the Content-Type header so everyone knows what they’re talking about all the way through the system.&lt;/p&gt;

&lt;p&gt;A few months pass. Several partners are using the order API, but we want to automate our stock inventory, so instead of a plain text description we want item IDs. We don’t want to force this change on our partners, though — their development cycle is slow and they’re sending us plenty of orders. We like their cash.&lt;/p&gt;

&lt;p&gt;So we publish a second version, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/vnd.acme.order-v2+json&lt;/code&gt;, defining messages like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;customer_id&quot;: 123,
  &quot;purchase_order_id&quot;: &quot;ASLA-001-2031&quot;,
  &quot;amount&quot;: 1000,
  &quot;items&quot;: [
    { &quot;item_id&quot;: 1032, &quot;quantity&quot;: 100 }
  ]
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s now trivial to add a second &lt;a href=&quot;http://eaipatterns.com/MessageSelector.html&quot;&gt;Selective Consumer&lt;/a&gt; that handles only v2 messages and updates inventory accordingly. The v1 consumer keeps running happily with v1 orders. There’s a smooth, unhurried migration path for clients from v1 to v2. We can support both versions or drop older ones as we choose. We could even use a combination of &lt;a href=&quot;http://eaipatterns.com/Sequencer.html&quot;&gt;Splitter&lt;/a&gt;, &lt;a href=&quot;http://eaipatterns.com/MessageTranslator.html&quot;&gt;Translator&lt;/a&gt;, and &lt;a href=&quot;http://eaipatterns.com/DataEnricher.html&quot;&gt;Enricher&lt;/a&gt; to route v2 messages into the v1 consumer while splitting off inventory management messages to a separate, lightweight consumer. None of this matters to our partners, because they know they’re working to the interface we’ve defined.&lt;/p&gt;

&lt;h2 id=&quot;when-the-transport-changes&quot;&gt;When the transport changes&lt;/h2&gt;

&lt;p&gt;We might eventually decide that the RESTful order service isn’t appropriate for v3 — perhaps we’ve been won over by WebSockets. When we receive an order claiming to be v1 or v2 on the RESTful service, we can still happily accept it. If we receive anything else, we return an &lt;a href=&quot;http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.7&quot;&gt;HTTP 406&lt;/a&gt; to tell the client we can’t accept orders that way for v3.&lt;/p&gt;

&lt;p&gt;In contrast, if we’d used plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/json&lt;/code&gt; we’d have to guess based on message fields which version the client intended. That’s barely practical with the trivial example above, and once there are several versions of the interface it becomes a nightmare.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Painting with Constable</title>
    <link href="/2011/10/05/painting-with-constable/"/>
    <updated>2011-10-05T00:00:00+08:00</updated>
    <id>/2011/10/05/painting-with-constable/</id>
    <content type="html">&lt;p&gt;ImageMagick annoys me. Not because of what it does — functionally, it’s the bee’s knees — but because installing it is a pain. Like many Ruby developers, I tend to develop on a Mac, an operating system without much of an official package manager. Installing tools with complex dependencies like ImageMagick gets tedious fast. I long for the days when I can &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt-get install imagemagick&lt;/code&gt; while still enjoying all the lovely hardware a Mac provides.&lt;/p&gt;

&lt;p&gt;Over the years I’d built up some solid experience with virtualisation, and then Vagrant came along and made it trivially easy to run Ubuntu on my Mac. Suddenly I had access to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt&lt;/code&gt; and proper ImageMagick packages. Unfortunately I couldn’t use them natively on my Mac — they were only accessible from inside the VM. Better than nothing, so for command-line image manipulation I’d been getting by.&lt;/p&gt;

&lt;p&gt;During my more recent work I’d spent a fair amount of time with messaging, exposing services on a message bus for remote clients. There’s something really satisfying about not having to worry about the implementation of a service — just knowing that a message in a certain format sent to a certain destination will get the job done. So I tried exactly that for ImageMagick: exposing it on my VM as a service on the bus. It worked well enough that I threw up a &lt;a href=&quot;https://github.com/craigw/constable&quot;&gt;project on GitHub&lt;/a&gt; and &lt;a href=&quot;https://rubygems.org/gems/constable&quot;&gt;released a RubyGem&lt;/a&gt;. The project is called Constable — the &lt;a href=&quot;https://github.com/craigw/constable#readme&quot;&gt;README&lt;/a&gt; explains why.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/craigw/constable&quot;&gt;Constable&lt;/a&gt; is &lt;em&gt;very nearly&lt;/em&gt; a drop-in replacement for ImageMagick. After installing the gem and setting up the service, you can use the same ImageMagick commands to do a lot of the same stuff that a local install would let you do. There are some caveats, of course. Output must (at the moment, at least) be streamed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;STDOUT&lt;/code&gt;. ImageMagick supports this by letting your output filename take the form &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format:-&lt;/code&gt;, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jpg:-&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;png:-&lt;/code&gt;. There may be other shortcomings I haven’t hit, possibly because while I use ImageMagick a lot, I don’t use it in particularly complex ways. If you come across any problems, let me know. If you can submit a patch, even better.&lt;/p&gt;

&lt;p&gt;Setting up an example service is covered in the “Up and running fast” section of the &lt;a href=&quot;https://github.com/craigw/constable#readme&quot;&gt;README&lt;/a&gt;, so I’ll skip that and run through a quick demo: creating a couple of JPEGs with text in them, compositing one on top of the other, and producing a PNG at 50% of the original dimensions.&lt;/p&gt;

&lt;p&gt;First, make sure the service is up as described in the &lt;a href=&quot;https://github.com/craigw/constable#readme&quot;&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Second — a bit of an undocumented easter egg at the moment — install the binstubs for the ImageMagick services:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo constable-install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note that this will overwrite the following files if they exist:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/usr/bin/identify
/usr/bin/convert
/usr/bin/compare
/usr/bin/composite
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This step is optional but gives you the same command names that ImageMagick uses. If you’d rather skip it, just prefix each command with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;constable-&lt;/code&gt; and add a double dash immediately after the command name, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;convert foo.jpg png:-&lt;/code&gt; becomes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;constable-convert -- foo.jpg png:-&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, on to actually using the service. Creating text-based images is straightforward with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;convert&lt;/code&gt; command. Here I create two JPEGs — one in blue tones with the text “Anthony”, and another in pink tones with the text “Cleopatra”:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;convert -background lightblue -fill blue -font Candice -pointsize 72 \
  label:Anthony   jpg:- &amp;gt; anthony.jpg
convert -background pink      -fill red  -font Candice -pointsize 72 \
  label:Cleopatra jpg:- &amp;gt; cleopatra.jpg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;They won’t win any design awards, but they’re good enough for a demo:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;img src=&quot;/images/anthony.jpg&quot; alt=&quot;Anthony&quot; /&gt;&lt;/li&gt;
  &lt;li&gt;&lt;img src=&quot;/images/cleopatra.jpg&quot; alt=&quot;Cleopatra&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To combine them, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;convert&lt;/code&gt; in a different invocation:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;convert anthony.jpg cleopatra.jpg +append jpg:- \
  &amp;gt; anthony_and_cleopatra.jpg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Resulting in this magnificent creation:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;img src=&quot;/images/anthony_and_cleopatra.jpg&quot; alt=&quot;Anthony and Cleopatra&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, we can convert the combined image to a PNG at 50% of its original size:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;convert anthony_and_cleopatra.jpg -resize 50% png:- \
  &amp;gt; anthony_and_cleopatra.png
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Which outputs the smaller PNG:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;img src=&quot;/images/anthony_and_cleopatra.png&quot; alt=&quot;Smaller PNG&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This demo works identically whether you’re running the service in a VM or using a local ImageMagick install. I’m rather happy with that. But there’s no reason to stop here — why not expose this as a proper remote service and write a plugin for AttachmentFu or Paperclip that uses it to offload image processing entirely? Get that heavy lifting out of the request-response cycle!&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/craigw/constable&quot;&gt;code is out there&lt;/a&gt;. It’s rough around the edges but it works. Let me know if you find it useful, and if you’d like it to do something it can’t yet, patches and suggestions are very welcome.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>A Simple SOCKS Proxy Using SSH</title>
    <link href="/2011/08/17/a-simple-socks-proxy-using-ssh/"/>
    <updated>2011-08-17T00:00:00+08:00</updated>
    <id>/2011/08/17/a-simple-socks-proxy-using-ssh/</id>
    <content type="html">&lt;p&gt;Ever forget to add a firewall rule so people can reach an internal staging server from outside the network? I needed to verify that a server was accessible from the outside world, but I wanted to do it right now, from the machine sitting inside the network.&lt;/p&gt;

&lt;p&gt;Turns out this is trivially easy with a tool pretty much every developer already has installed: &lt;a href=&quot;https://www.openssh.com/&quot;&gt;SSH&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Three steps and you’re done:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Have a server somewhere that you can SSH into.
&lt;a href=&quot;https://aws.amazon.com/ec2/&quot;&gt;EC2&lt;/a&gt; is perfect for this sort of thing.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Open an SSH connection to it with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-D&lt;/code&gt; flag, which tells SSH to act as a SOCKS proxy. The other flags enable compression and keep things quiet:&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh -C2qTnN -D 8080 your-server-name-here.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Configure your browser to use a SOCKS proxy on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt;, port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080&lt;/code&gt;.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once that’s in place, head over to &lt;a href=&quot;https://www.whatismyip.com/&quot;&gt;whatismyip.com&lt;/a&gt; and you should see the public IP address of the remote server rather than your own. All your browser traffic is now tunnelled through that box.&lt;/p&gt;

&lt;p&gt;Quick, easy, and no VPN software required.&lt;/p&gt;

</content>
  </entry>
  
  
  
  
  <entry>
    <title>Reposted: Ten Steps for Attending a Keysigning Party</title>
    <link href="/2011/07/10/reposted-ten-steps-for-attending-a-keysigning-party/"/>
    <updated>2011-07-10T00:00:00+08:00</updated>
    <id>/2011/07/10/reposted-ten-steps-for-attending-a-keysigning-party/</id>
    <content type="html">&lt;div class=&quot;foreword&quot;&gt;
  &lt;p&gt;This is a copy of the post originally found at &lt;a href=&quot;http://commandline.org.uk/command-line/2007/sep/7/ten-steps-for-attending-a-keysigning-party/&quot;&gt;http://commandline.org.uk/command-line/2007/sep/7/ten-steps-for-attending-a-keysigning-party/&lt;/a&gt;. The original appears to have vanished and the URL now returns a 404. This work is not mine and I&apos;m not trying to claim it as such — I linked to it in a few places and wanted a permanent archive. Thanks to &lt;a href=&quot;http://vic.demuzere.be/&quot;&gt;Vic Demuzere&lt;/a&gt; who let me know the link had gone dead.&lt;/p&gt;

  &lt;p&gt;Update: the original post appears to be archived at &lt;a href=&quot;http://old.commandline.org.uk/command-line/ten-steps-for-attending-a-keysigning-party/&quot;&gt;http://old.commandline.org.uk/command-line/ten-steps-for-attending-a-keysigning-party/&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;p class=&quot;introduction&quot;&gt;A key signing party can be an event of its own, or it might happen at a user group meeting, a conference, or a workplace. The idea is to grow the &quot;web of trust&quot; and strengthen the system as a whole, while also making your own key more trusted. Alex Willmer explains what you need to do to participate in a key signing party using GNU Privacy Guard.&lt;/p&gt;

&lt;p&gt;You can use either the command line &lt;code&gt;gpg&lt;/code&gt; tool or a GUI front end such as Seahorse. The command line approach goes as follows:&lt;/p&gt;

&lt;h2&gt;0. Generate a key&lt;/h2&gt;

&lt;p&gt;If you haven&apos;t already done so, generate a key pair:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ gpg --gen-key&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;1. Get your key ID&lt;/h2&gt;

&lt;p&gt;Find your public key:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ gpg --list-keys&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This gives results like the below. The uid should match your name and chosen email address. Note the id on the line labelled &quot;pub&quot;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; /home/alex/.gnupg/pubring.gpg
-----------------------------
pub 1024D/5A6F95BE 2007-02-08
uid Alex Willmer &amp;lt;alex at moreati.org.uk&amp;gt;
sub 2048g/63329941 2007-02-08&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;2. Upload your key&lt;/h2&gt;

&lt;p&gt;Publish your public key to a keyserver:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ gpg --keyserver ldap://keyserver.pgp.com --send-keys 5A6F95BE&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Which should respond:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; gpg: sending key 5A6F95BE to ldap server keyserver.pgp.com&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;3. Print your key fingerprint&lt;/h2&gt;

&lt;p&gt;Using the id from step 1:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ gpg --fingerprint 5A6F95BE&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The result is the fingerprint of your public key:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; pub 1024D/5A6F95BE 2007-02-08
Key fingerprint = C9CD 3335 C138 7291 2022 F30D 2E51 C57B 5A6F 95BE
uid Alex Willmer &amp;lt;alex at moreati.org.uk&amp;gt;
sub 2048g/63329941 2007-02-08&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Print your fingerprint onto paper — you should be able to fit quite a few on a page, which you can then cut into slips. You can also generate these with the command &lt;code&gt;gpg-key2ps&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;4. Go to the party!&lt;/h2&gt;

&lt;p&gt;Bring the slips and credentials that prove your identity. Normally parties require photo ID (e.g. your passport or driving licence).&lt;/p&gt;

&lt;h2&gt;5. Give out slips&lt;/h2&gt;

&lt;p&gt;Give a fingerprint slip to anybody you&apos;d like to sign your key, and allow them to verify your identity using your credentials.&lt;/p&gt;

&lt;h2&gt;6. Take slips&lt;/h2&gt;

&lt;p&gt;Verify in person the identity of anybody you accept a slip from. Make sure the slip has a uid matching their name.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: it&apos;s anti-social to take slips and then throw them away or forget about them. If you take a slip from someone, it&apos;s polite to actually follow through with steps 7 and 8.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;7. Verify the key fingerprints of your acquaintances&lt;/h2&gt;

&lt;p&gt;Once you&apos;re home, use the id from each slip to download and verify each person&apos;s key fingerprint:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ gpg --keyserver ldap://keyserver.pgp.com --recv-keys [key_id]&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;$ gpg --fingerprint [key_id]&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;8. Sign and upload your acquaintances&apos; keys&lt;/h2&gt;

&lt;p&gt;Sign each verified key and upload it to a keyserver:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ gpg --sign-key [key_id]&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;$ gpg --keyserver ldap://keyserver.pgp.com --send-key [key_id]&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;9. Use GPG!&lt;/h2&gt;

&lt;p&gt;You can now sign emails, and anybody who signed your key can verify that the email was sent by you and hasn&apos;t been modified. You can also encrypt anything you send to a person whose key you&apos;ve signed.&lt;/p&gt;

&lt;h2&gt;10. Advanced usage&lt;/h2&gt;

&lt;p&gt;There are optional additional steps, such as encrypting a signed key and sending it to the listed uid. By receiving the signed key and decrypting it, they prove access to the email address and control of the private key.&lt;/p&gt;

&lt;h2&gt;More Information&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
  &lt;li&gt;&lt;a class=&quot;reference external&quot; href=&quot;http://www.gnupg.org/&quot;&gt;GNU Privacy Guard&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a class=&quot;reference external&quot; href=&quot;http://cryptnet.net/fdp/crypto/keysigning_party/en/keysigning_party.html&quot;&gt;The Keysigning Party HowTo&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a class=&quot;reference external&quot; href=&quot;http://debaday.debian.net/2007/02/18/signing-party-complete-toolkit-for-efficient-key-signing/&quot;&gt;Signing-party - complete toolkit for efficient key-signing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a class=&quot;reference external&quot; href=&quot;http://www.gentoo.org/doc/en/gnupg-user.xml&quot;&gt;GnuPG Gentoo User Guide&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a class=&quot;reference external&quot; href=&quot;https://help.ubuntu.com/community/GnuPrivacyGuardHowto&quot;&gt;Ubuntu Gnu Privacy Guard Howto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Code to an Interface (aka Stop Using Instance Variables)</title>
    <link href="/2011/04/21/code-to-an-interface-aka-stop-using-instance-variables/"/>
    <updated>2011-04-21T00:00:00+08:00</updated>
    <id>/2011/04/21/code-to-an-interface-aka-stop-using-instance-variables/</id>
    <content type="html">&lt;p&gt;We all know the drill: only call methods a class declares public, leave protected and private methods alone, because they can change at any time. In other words, code to the public interface and don&apos;t depend on implementation details. It keeps our code clean and means that when the internals of a class change, its clients don&apos;t have to.&lt;/p&gt;

&lt;p&gt;Curiously, we rarely apply the same thinking when managing state &lt;em&gt;inside&lt;/em&gt; our own classes — and that can make refactoring surprisingly painful.&lt;/p&gt;

&lt;h2&gt;The problem with bare instance variables&lt;/h2&gt;

&lt;p&gt;Here&apos;s a &lt;code&gt;Book&lt;/code&gt; class from a hypothetical bookstore application. Books have titles and authors. They have a publication date that can change — maybe the author misses a deadline, or editing runs long. Titles can change too, but authors won&apos;t.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class Book
  attr_reader :author
  attr_accessor :title, :published_at

  def initialize author, title, published_at
    @author = author
    @title = title
    @published_at = published_at
  end

  def to_s
    &quot;\&quot;#{@title}\&quot; by #{@author}. Publication date: #{@published_at}&quot;
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few weeks pass and we start doing more deals with publishers. One of them wants us to exclusively list an upcoming book by A.N. Big Author. Great! Except… we can&apos;t handle books that don&apos;t have a publication date yet. We need to update the class:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class Book
  attr_reader :author
  attr_accessor :title, :published_at

  # published_at = nil if the book doesn&apos;t have a publication date
  def initialize author, title, published_at
    @author = author
    @title = title
    @published_at = published_at
  end

  def to_s
    &quot;\&quot;#{@title}\&quot; by #{@author}. Publication date: #{@published_at ? @published_at : &apos;not yet published&apos;}&quot;
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&apos;s tolerable for this tiny class, but it&apos;s ugly, and it&apos;s easy to imagine a real class where &lt;code&gt;@published_at&lt;/code&gt; gets accessed directly in a dozen places. Changing every one of those takes time and the resulting conditionals don&apos;t read well. It&apos;s a prime candidate for the &lt;a href=&quot;http://www.refactoring.com/catalog/introduceNullObject.html&quot;&gt;Introduce Null Object&lt;/a&gt; refactoring, but because we&apos;re reaching for &lt;code&gt;@published_at&lt;/code&gt; directly everywhere, there&apos;s still a lot of churn. We could introduce the Null Object during instantiation, except the publication date can change at any time — a publisher might call and say they&apos;ve missed their date and don&apos;t know when they&apos;ll publish.&lt;/p&gt;

&lt;h2&gt;A better starting point&lt;/h2&gt;

&lt;p&gt;Here&apos;s the class I wish I&apos;d written from the beginning. It exposes the same public API but uses accessor methods internally instead of bare instance variables:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class Book
  attr_accessor :author, :title, :published_at
  private :author=

  def initialize author, title, published_at
    self.author = author
    self.title = title
    self.published_at = published_at
  end

  def to_s
    &quot;\&quot;#{title}\&quot; by #{author}. Publication date: #{published_at}&quot;
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now when I get the call about the unpublished book, I can introduce a Null Object by simply overriding the reader for &lt;code&gt;published_at&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class MissingPublicationDate
  include Singleton
  def to_s
    &apos;not yet published&apos;
  end
end

class Book
  attr_accessor :author, :title, :published_at
  private :author=

  def initialize author, title, published_at
    self.author = author
    self.title = title
    self.published_at = published_at
  end

  def published_at_with_null_object
    published_at_without_null_object || MissingPublicationDate.instance
  end
  alias_method :published_at_without_null_object, :published_at
  alias_method :published_at, :published_at_with_null_object

  def to_s
    &quot;\&quot;#{title}\&quot; by #{author}. Publication date: #{published_at}&quot;
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It&apos;s a touch more code in this example, but almost none of the methods that use &lt;code&gt;published_at&lt;/code&gt; need to change, and the result is vastly more readable. The lesson: treat your own class&apos;s state the same way you&apos;d treat someone else&apos;s API. Code to the interface, even internally.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Working with Ruby Arrays: Map with Index</title>
    <link href="/2011/04/01/working-with-ruby-arrays-map-with-index/"/>
    <updated>2011-04-01T00:00:00+08:00</updated>
    <id>/2011/04/01/working-with-ruby-arrays-map-with-index/</id>
    <content type="html">&lt;p&gt;Here&apos;s a handy little method I keep reaching for: &lt;code&gt;map_with_index&lt;/code&gt;. It does exactly what you&apos;d expect — it works like &lt;code&gt;each_with_index&lt;/code&gt; but with the return-value behaviour of &lt;code&gt;map&lt;/code&gt;. Every element in the resulting array is whatever the block returns when that element and its index are yielded to it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;module BarkingIguana
  module ArrayExt
    def map_with_index &amp;amp;block
      index = 0
      map do |element|
        result = yield element, index
        index += 1
        result
      end
    end
  end
end

Array.class_eval do
  include BarkingIguana::ArrayExt
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is particularly useful when the first N elements of an array need to be treated differently from the rest:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[1, 2, 3, 4, 5].map_with_index do |element, index|
  model = Model.new element
  model.unlock if index &amp;lt; 3
  model
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that Ruby 1.9.3+ gives you &lt;code&gt;each_with_index.map&lt;/code&gt; and later versions provide &lt;code&gt;each_with_object&lt;/code&gt; and other enumerator-chaining tricks that can achieve similar results — but sometimes a purpose-built method just reads better.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Debugging JavaScript with a Stack Trace</title>
    <link href="/2011/03/20/debugging-javascript-with-a-stacktrace/"/>
    <updated>2011-03-20T00:00:00+08:00</updated>
    <id>/2011/03/20/debugging-javascript-with-a-stacktrace/</id>
    <content type="html">&lt;p&gt;I was trying to work with some JavaScript that kept popping up alert boxes. The library was huge and not particularly well organised, so rather than hunting through thousands of lines of code, I wrapped the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.alert&lt;/code&gt; with a version that shows a stack trace just before the real alert fires.&lt;/p&gt;

&lt;p&gt;You can adapt this technique to trace calls to any function; just change what gets wrapped in the last four lines.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;original_alert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;stacktrace&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;regex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/function&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\W&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;([\w&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;trace&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;trace&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;regex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;trace&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos;, &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;trace&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;trace&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;callee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;caller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;original_alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;trace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;alert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;stacktrace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;original_alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The approach is straightforward: save a reference to the real &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.alert&lt;/code&gt;, then replace it with a wrapper that walks up the call stack using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arguments.callee.caller&lt;/code&gt;, building a string of function names and their arguments as it goes. It pops up the trace in one alert, then lets the original alert through.&lt;/p&gt;

&lt;p&gt;A word of caution: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arguments.callee&lt;/code&gt; is deprecated in strict mode and won’t work in modern ES5+ strict code. For anything current, you’d want to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;console.trace()&lt;/code&gt; or the browser’s built-in debugger instead. But when you’re stuck debugging a sprawling legacy codebase that predates those niceties, this trick can save you a lot of time.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Be Cool with Arrays</title>
    <link href="/2011/03/14/be-cool-with-arrays/"/>
    <updated>2011-03-14T00:00:00+08:00</updated>
    <id>/2011/03/14/be-cool-with-arrays/</id>
    <content type="html">&lt;p&gt;A few of my pet peeves centre around arrays. Ruby gives you a beautifully expressive language for working with collections; use it. Your code will be more readable, and your future self will thank you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask an array if it’s empty. Don’t check if its size equals zero.&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;bookmarks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# no!&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bookmarks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;empty?&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Ask an array if it has any elements. Don’t check if it has a non-zero size.&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;bookmarks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# no!&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bookmarks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Don’t guard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;each&lt;/code&gt; with an emptiness check. It already handles empty arrays gracefully; it simply won’t yield.&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bookmarks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bookmarks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# pointless&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bookmarks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# does the same thing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The general principle: if a method exists that says what you mean, use it instead of reinventing the check with arithmetic. It reads better and communicates intent more clearly.&lt;/p&gt;

&lt;p&gt;I’m sure you have similar peeves. I’d love to hear what they are.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Moving LVM Volumes Between Hosts Without an Intermediate File</title>
    <link href="/2011/03/13/moving-lvm-volumes-between-hosts-without-an-intermediate-file/"/>
    <updated>2011-03-13T00:00:00+08:00</updated>
    <id>/2011/03/13/moving-lvm-volumes-between-hosts-without-an-intermediate-file/</id>
    <content type="html">&lt;p&gt;At &lt;a href=&quot;http://xeriom.net/&quot;&gt;Xeriom Networks&lt;/a&gt; we provide virtual machines for clients to run their applications. Clients quite sensibly start with the smallest VM that meets their needs, then upgrade as they grow. Unfortunately, we can only fit so much disk space in each physical server, so when clients upgrade we sometimes need to move their &lt;a href=&quot;http://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)&quot;&gt;LVM&lt;/a&gt; volumes to another physical server with enough free space for the expanded disk image.&lt;/p&gt;

&lt;p&gt;The obvious approach would be to use &lt;a href=&quot;http://en.wikipedia.org/wiki/Dd_(Unix)&quot;&gt;dd&lt;/a&gt; to copy the volume to a file, &lt;a href=&quot;http://en.wikipedia.org/wiki/Secure_copy&quot;&gt;SCP&lt;/a&gt; that file to the new server, then dd it back into a volume on the other end. The problem is that some of these volumes are hundreds of gigabytes, and the local disk often doesn’t have enough room for the intermediate file. It also just feels messy.&lt;/p&gt;

&lt;p&gt;After some investigation, I discovered you can pipe dd’s output directly through SSH and into a dd process on the remote end, skipping the intermediate file entirely.&lt;/p&gt;

&lt;p&gt;There are two steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. On the destination host, create a volume large enough for the data:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo lvcreate -L 10G -n destination-lvm-volume-name destination-vg-name
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. From the source host, stream the volume across the network:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo dd if=/dev/source-vg-name/source-volume-name | ssh -c arcfour -l root host-b &apos;dd of=/dev/destination-vg-name/destination-lvm-volume-name&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it. Much easier than I was expecting.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-c arcfour&lt;/code&gt; flag selects a fast cipher for SSH, which helps with throughput on large transfers. The main downside compared to SCP is that you don’t get a progress bar, so you’re largely left guessing when the transfer will finish. If you know a good way to add progress indication to piped transfers like this, I’d love to hear about it.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Home Delivery Network Limited: Pretending to Deliver for Amazon Prime</title>
    <link href="/2011/03/12/home-delivery-network-limited-pretending-to-deliver-for-amazon-prime/"/>
    <updated>2011-03-12T00:00:00+08:00</updated>
    <id>/2011/03/12/home-delivery-network-limited-pretending-to-deliver-for-amazon-prime/</id>
    <content type="html">&lt;p&gt;I’ve been failed once again by &lt;a href=&quot;http://www.hdnl.co.uk/&quot;&gt;Home Delivery Network Limited&lt;/a&gt; pretending to deliver my &lt;a href=&quot;http://amazon.co.uk/&quot;&gt;Amazon&lt;/a&gt; order. I’m getting properly fed up with it, and I’m &lt;a href=&quot;http://www.amazon.co.uk/tag/deals/forum/ref=cm_cd_ttp_ef_tft_tp?_encoding=UTF8&amp;amp;cdForum=Fx1DEIHNWYF5SA9&amp;amp;cdThread=Tx1A9GFNUAIDZ&amp;amp;displayType=tagsDetail&quot;&gt;not&lt;/a&gt; &lt;a href=&quot;http://www.amazon.co.uk/tag/deals/forum/ref=cm_cd_ttp_ef_tft_tp?_encoding=UTF8&amp;amp;cdForum=Fx1DEIHNWYF5SA9&amp;amp;cdThread=Tx12HUCZT5EPZUV&amp;amp;displayType=tagsDetail&quot;&gt;the&lt;/a&gt; &lt;a href=&quot;http://www.mrdaz.com/why-does-amazon-persist-with-home-delivery-network/&quot;&gt;only&lt;/a&gt; &lt;a href=&quot;http://www.reviewcentre.com/r150787_5_Home_Delivery_Network_Limited_.html&quot;&gt;one&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ve tried calling both numbers on the &lt;a href=&quot;http://www.hdnl.co.uk/Contact-Us/&quot;&gt;HDNL contact us&lt;/a&gt; page. Both require a delivery number, which is written on the card the driver supposedly leaves when they attempt delivery. There’s been no delivery attempt, so there’s no card and no delivery number. Tremendously helpful.&lt;/p&gt;

&lt;p&gt;I tried talking to &lt;a href=&quot;http://www.amazon.co.uk/gp/help/contact-us/general-questions.html/ref=hp_gw_cu&quot;&gt;Amazon support&lt;/a&gt;, who were very apologetic and tried to call HDNL on my behalf. They couldn’t get through to anyone at Home Delivery Network and said they couldn’t see a delivery number in the system, which strongly suggests the driver didn’t leave a card. Correct! After I complained about this being a recurring problem, they added a query to request that HDNL investigate what happened and contact me. I don’t hold out much hope that anything beyond “we attempted delivery but couldn’t access the property” will come out of that.&lt;/p&gt;

&lt;p&gt;A quick search turned up the phone number for the HDNL depot at New Cross Gate, where my package had set out from and been returned to after the non-existent delivery attempt. It’s listed at &lt;a href=&quot;http://www.saynoto0870.com/search.php&quot;&gt;Say No To 0870&lt;/a&gt; (search for “Home Delivery Network”) as 020 7635 8094, in case anyone else needs it (other depots are listed there too). The chap on the other end didn’t seem particularly surprised when I told him the driver never showed up, told me I couldn’t come down to pick it up (the depot is a 10-minute bus ride from my flat) because it would be mixed in with all the other parcels by now, and said my best bet was to wait in on Monday. To complain, I’d have to call one of the original premium-rate numbers and wait (paying through the nose) until an operator eventually picks up.&lt;/p&gt;

&lt;p&gt;A week ago I signed up for Amazon Prime, thinking I’d get a reliable delivery service for my GBP 49 a year. This package was ordered for next-day delivery. Does that sound like value for money? Shouldn’t I be able to trust that deliveries will actually be attempted on the day the courier claims they will be?&lt;/p&gt;

&lt;p&gt;I want to see one of two things added to my Amazon delivery options:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Don’t use HDNL for any of my deliveries.&lt;/strong&gt; I’ll happily pay more.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Let me collect from the depot.&lt;/strong&gt; HDNL clearly struggle with the last-mile problem, so send it to the depot and I’ll pick it up myself. I’ll pay less.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Please, Amazon: give me some way to avoid this terrible delivery company.&lt;/p&gt;

&lt;div class=&quot;update&quot;&gt;
  &lt;p class=&quot;when date&quot;&gt;Update: Monday 14th March&lt;/p&gt;
  &lt;p&gt;When I called on Saturday, both Amazon and HDNL told me to wait in my flat for the parcel to arrive on Monday. This morning at 06:45 I was emailed by Amazon to say that HDNL will now deliver my Kindle on Tuesday. High five, guys. Big success. Meanwhile, &lt;a href=&quot;http://james.cridland.net/&quot;&gt;James Cridland&lt;/a&gt; has pointed out that if I&apos;d just nipped into the local Tesco superstore I could have picked one up in about 10 minutes.&lt;/p&gt;
&lt;/div&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Forking Ruby Processes</title>
    <link href="/2011/03/03/forking-ruby-processes/"/>
    <updated>2011-03-03T00:00:00+08:00</updated>
    <id>/2011/03/03/forking-ruby-processes/</id>
    <content type="html">&lt;div class=&quot;foreword&quot;&gt;
  &lt;p&gt;I was recently asked if I had the content of some articles that I posted a long time ago on a blog I used to run. After some searching I managed to scrape together the content using the Wayback Machine. It&apos;s faithfully recreated here without changes, something I should have done when I first bought the barkingiguana.com domain.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;For today’s adventure in Ruby, I’m going to write a simple daemon process. To start with, it won’t do anything particularly useful; every second it’ll print the current time to STDOUT.&lt;/p&gt;

&lt;p&gt;Once that’s working, I’ll swap in the socket-checking code from my earlier posts and bump the interval to 15 seconds.&lt;/p&gt;

&lt;h3 id=&quot;a-simple-time-printing-daemon&quot;&gt;A simple time-printing daemon&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kawaii:~ craig$ irb
irb(main):001:0&amp;gt; fork do # Fork a new process
irb(main):002:1*   while true # Loop forever
irb(main):003:2&amp;gt;     puts Time.now # Print the time
irb(main):004:2&amp;gt;     sleep 1 # Sleep for a second
irb(main):005:2&amp;gt;   end # while true
irb(main):006:1&amp;gt; end # fork
=&amp;gt; 15738
irb(main):007:0&amp;gt; Sat Jun 03 11:31:09 BST 2006
Sat Jun 03 11:31:10 BST 2006
Sat Jun 03 11:31:11 BST 2006
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Easy. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fork&lt;/code&gt; call creates a child process that runs independently, and we get back a PID. Meanwhile, the parent IRB session carries on as normal (well, with timestamps appearing in the background).&lt;/p&gt;

&lt;h3 id=&quot;monitoring-a-socket&quot;&gt;Monitoring a socket&lt;/h3&gt;

&lt;p&gt;Next, let’s check that Postfix is listening on port 25 on the secondary MX, mx2.xeriom.net. Don’t forget to require the socket library; otherwise you’ll always hit the rescue block. Ask me how I know.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;irb(main):045:0&amp;gt; require &apos;socket&apos;
=&amp;gt; true
irb(main):046:0&amp;gt; fork do
irb(main):047:1*   while true
irb(main):048:2&amp;gt;     begin
irb(main):049:3*       t = TCPSocket.open(&apos;mx2.xeriom.net&apos;, &apos;smtp&apos;)
irb(main):050:3&amp;gt;       puts Time.now.to_s + &quot;: MX2 is listening on port 25.&quot;
irb(main):051:3&amp;gt;       t.close
irb(main):052:3&amp;gt;     rescue
irb(main):053:3&amp;gt;       puts Time.now.to_s + &quot;: MX2 is NOT listening on port 25.&quot;
irb(main):054:3&amp;gt;     end
irb(main):055:2&amp;gt;     sleep 15
irb(main):056:2&amp;gt;   end
irb(main):057:1&amp;gt; end
=&amp;gt; 15759
irb(main):058:0&amp;gt; Sat Jun 03 11:48:25 BST 2006: MX2 is listening on port 25.
Sat Jun 03 11:48:41 BST 2006: MX2 is listening on port 25.
Sat Jun 03 11:48:56 BST 2006: MX2 is NOT listening on port 25.
Sat Jun 03 11:49:11 BST 2006: MX2 is listening on port 25.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That third check caught Postfix being momentarily unavailable. Useful.&lt;/p&gt;

&lt;h3 id=&quot;monitoring-multiple-hosts-and-ports&quot;&gt;Monitoring multiple hosts and ports&lt;/h3&gt;

&lt;p&gt;That was a little too easy, so let’s extend the problem. This time we’ll check an arbitrary number of ports across an arbitrary number of hosts, using threads inside the forked process for concurrency:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;irb(main):107:0&amp;gt; host_sockets = { &apos;mx2.xeriom.net&apos; =&amp;gt; [ 25 ], &apos;kiwi.xeriom.net&apos; =&amp;gt; [ 21, 22, 25 ], &apos;guava.xeriom.net&apos; =&amp;gt; [ 21, 22, 25 ], &apos;mx1.xeriom.net&apos; =&amp;gt; [ 25 ] }
=&amp;gt; {&apos;mx2.xeriom.net&apos; =&amp;gt; [ 25 ], &apos;kiwi.xeriom.net&apos; =&amp;gt; [ 21, 22, 25 ], &apos;guava.xeriom.net&apos; =&amp;gt; [ 21, 22, 25 ], &apos;mx1.xeriom.net&apos; =&amp;gt; [ 25 ]}
irb(main):108:0&amp;gt; fork do
irb(main):109:1*   while true
irb(main):110:2&amp;gt;     host_sockets.each { |hostname, sockets|
irb(main):111:3*       Thread.new(hostname, sockets) { |host, socks|
irb(main):112:4*         socks.each { |socket|
irb(main):113:5*           begin
irb(main):114:6*             t = TCPSocket.new(host, socket)
irb(main):115:6&amp;gt;             puts Time.now.to_s + &quot;: &quot; + host.to_s + &quot; is listening on port &quot; + socket.to_s
irb(main):116:6&amp;gt;             t.close
irb(main):117:6&amp;gt;           rescue
irb(main):118:6&amp;gt;             puts Time.now.to_s + &quot;: &quot; + host.to_s + &quot; is NOT listening on port &quot; + socket.to_s
irb(main):119:6&amp;gt;           end
irb(main):120:5&amp;gt;         }
irb(main):121:4&amp;gt;       }
irb(main):122:3&amp;gt;     }
irb(main):123:2&amp;gt;     sleep 15
irb(main):124:2&amp;gt;   end
irb(main):125:1&amp;gt; end
=&amp;gt; 15784
Sat Jun 03 12:16:56 BST 2006: kiwi.xeriom.net is listening on port 21Sat Jun 03 12:16:56 BST 2006: mx2.xeriom.net is listening on port 22

Sat Jun 03 12:16:56 BST 2006: guava.xeriom.net is listening on port 22
Sat Jun 03 12:16:56 BST 2006: kiwi.xeriom.net is listening on port 22Sat Jun 03 12:16:56 BST 2006: mx2.xeriom.net is listening on port 25

Sat Jun 03 12:16:56 BST 2006: kiwi.xeriom.net is listening on port 25Sat Jun 03 12:16:56 BST 2006: guava.xeriom.net is listening on port 25 ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Obviously, if I were scaling this to millions of hosts, a thread-per-host approach would collapse under its own weight. But for keeping an eye on a small network, it does the job nicely.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Concurrent Socket Programming in Ruby</title>
    <link href="/2011/03/02/concurrent-socket-programming-in-ruby/"/>
    <updated>2011-03-02T00:00:00+08:00</updated>
    <id>/2011/03/02/concurrent-socket-programming-in-ruby/</id>
    <content type="html">&lt;div class=&quot;foreword&quot;&gt;
  &lt;p&gt;I was recently asked if I had the content of some articles that I posted a long time ago on a blog I used to run. After some searching I managed to scrape together the content using the Wayback Machine. It&apos;s faithfully recreated here without changes, something I should have done when I first bought the barkingiguana.com domain.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Continuing my &lt;a href=&quot;/2011/03/01/socket-programming-in-ruby/&quot;&gt;previous adventure&lt;/a&gt; in socket programming with Ruby, today I’ve attempted to communicate with multiple sockets concurrently.&lt;/p&gt;

&lt;p&gt;The idea is simple: spin up a thread for each port we want to check, and let them all run at once.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kawaii:~ craig$ irb
irb(main):001:0&amp;gt; require &apos;socket&apos;
=&amp;gt; true
irb(main):002:0&amp;gt; threads = []
=&amp;gt; []
irb(main):003:0&amp;gt; ports = [22,23,24,25,26,27,28,29,30].freeze
=&amp;gt; [22, 23, 24, 25, 26, 27, 28, 29, 30]
irb(main):004:0&amp;gt; for port in ports
irb(main):005:1&amp;gt;   threads &amp;lt;&amp;lt; Thread.new(port) { |p|
irb(main):006:2*     puts &quot;Checking if port &quot; + p.to_s + &quot; is open...&quot;
irb(main):007:2&amp;gt;     begin
irb(main):008:3*       t = TCPSocket.new(&apos;xeriom.net&apos;, p)
irb(main):009:3&amp;gt;       t.close
irb(main):010:3&amp;gt;       puts &quot;Port &quot; + p.to_s + &quot; is open.&quot;
irb(main):011:3&amp;gt;     rescue
irb(main):012:3&amp;gt;       puts &quot;Port &quot; + p.to_s + &quot; is not open.&quot;
irb(main):013:3&amp;gt;     end
irb(main):014:2&amp;gt;   }
irb(main):015:1&amp;gt; end
Checking if port 22 is open...Checking if port 23 is open...Checking if port 24 is open...
Checking if port 25 is open...
Checking if port 26 is open...
Checking if port 27 is open...
Port 22 is open.Checking if port 28 is open...
Checking if port 29 is open...

Checking if port 30 is open...
=&amp;gt; [22, 23, 24, 25, 26, 27, 28, 29, 30]
irb(main):016:0&amp;gt;

Port 24 is not open.Port 23 is not open.Port 29 is not open.Port 28 is not open.Port 25 is open.

Port 27 is not open.Port 26 is not open.Port 30 is not open.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The output is a jumbled mess because threads are writing to STDOUT whenever they feel like it, but that’s not the point. We can see that ports 22 (SSH) and 25 (SMTP) are open, and everything else is closed. I’ve just built a simple port scanner in 15 lines of Ruby. It’s not pretty, but it works.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;http://www.rubycentral.com/book/tut_threads.html&quot;&gt;Ruby thread tutorial&lt;/a&gt; at &lt;a href=&quot;http://www.rubycentral.com/&quot;&gt;Ruby Central&lt;/a&gt; mentions a fairly important caveat: if a thread executes something at the OS level that takes a long time to return, it can freeze the entire interpreter. That sounds bad.&lt;/p&gt;

&lt;p&gt;Interestingly though, it doesn’t seem to apply to TCPSocket operations. Adding in a few checks (left as an exercise for the reader), it seems that the only thing limiting the number of active threads is the overhead of creating them. There are up to 10 running at once with the above code, and I suspect you could push that number considerably higher if thread creation were faster.&lt;/p&gt;

&lt;p&gt;Tomorrow (or maybe later today) I’ll be attempting to use &lt;a href=&quot;http://rubyonrails.org/api/classes/ActiveRecord/Base.html&quot;&gt;ActiveRecord&lt;/a&gt; outside of &lt;a href=&quot;http://rubyonrails.org/&quot;&gt;Rails&lt;/a&gt;. I know it can be done; I just don’t know how hard it is yet.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Socket Programming in Ruby</title>
    <link href="/2011/03/01/socket-programming-in-ruby/"/>
    <updated>2011-03-01T00:00:00+08:00</updated>
    <id>/2011/03/01/socket-programming-in-ruby/</id>
    <content type="html">&lt;div class=&quot;foreword&quot;&gt;
  &lt;p&gt;I was recently asked if I had the content of some articles that I posted a long time ago on a blog I used to run. After some searching I managed to scrape together the content using the Wayback Machine. It&apos;s faithfully recreated here without changes, something I should have done when I first bought the barkingiguana.com domain.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;I decided today to find out how hard socket programming in Ruby would be, mainly because I’d finished a huge chunk of work and could find nothing better to do. The alternative was tidying the kitchen, so the bar was low.&lt;/p&gt;

&lt;p&gt;A quick search turned up an extract from the Pragmatic Ruby book at &lt;a href=&quot;http://www.rubycentral.com/book/lib_network.html&quot;&gt;rubycentral.com&lt;/a&gt;, and that simple library reference proved surprisingly handy. I’ll need to buy a copy of that book. Somebody remind me when I’m feeling flush.&lt;/p&gt;

&lt;p&gt;Anyway, unsurprisingly, it’s very easy. Here’s how you open and work with a TCP socket.&lt;/p&gt;

&lt;p&gt;First, fire up irb and load the socket library:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kawaii:~ craig$ irb
irb(main):001:0&amp;gt; require &apos;socket&apos;
=&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then open a socket, passing in a block:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;irb(main):002:0&amp;gt; TCPSocket.open(&apos;xeriom.net&apos;,&apos;smtp&apos;) do |t|
irb(main):003:1*   t.gets
irb(main):004:1&amp;gt; end
=&amp;gt; &quot;220 pluto.xeriom.net ESMTP Postfix\r\n&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Simple, elegant, delightful. Checking whether a socket is listening is just as easy. Since we already have the socket library loaded:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;irb(main):005:0&amp;gt; begin
irb(main):006:1*   t = TCPSocket.new(&apos;xeriom.net&apos;,8000)
irb(main):007:1&amp;gt;   t.close
irb(main):008:1&amp;gt; rescue
irb(main):009:1&amp;gt;   &quot;Error: socket not open&quot;
irb(main):010:1&amp;gt; end
=&amp;gt; &quot;Error: socket not open&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Done. Perhaps a little too easy; now I have nothing to do except tidy.&lt;/p&gt;

&lt;p&gt;Tomorrow I’ll play with opening many sockets simultaneously, checking if each one is open or closed. Bonus points if you beat me to it, or if you can make Java look as clean.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Blogging from Vim</title>
    <link href="/2010/10/08/blogging-from-vim/"/>
    <updated>2010-10-08T00:00:00+08:00</updated>
    <id>/2010/10/08/blogging-from-vim/</id>
    <content type="html">&lt;p&gt;Now that I’ve switched to a full-screen MacVim session for all my coding, switching to another application to jot down notes feels genuinely disruptive. Without notes, my blogging suffers; I never have anything to write about because I never captured the thought when it was fresh.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href=&quot;http://github.com/pedromg/vimblog.vim&quot;&gt;vimblog&lt;/a&gt;. It lets me draft and publish blog posts without ever leaving Vim. No context switching, no breaking flow. I can wax lyrical about whatever’s on my mind without reaching for another app.&lt;/p&gt;

&lt;p&gt;Lucky you.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Encrypting Data with GnuPG</title>
    <link href="/2010/10/01/encrypting-data-with-gnupg/"/>
    <updated>2010-10-01T00:00:00+08:00</updated>
    <id>/2010/10/01/encrypting-data-with-gnupg/</id>
    <content type="html">&lt;p&gt;There was recently &lt;a href=&quot;http://www.bbc.co.uk/news/technology-11434809&quot;&gt;yet another&lt;/a&gt; case of an organisation passing around unencrypted sensitive data. It keeps happening, and I’m constantly surprised that more people don’t reach for the perfectly good encryption tools that are freely available. GnuPG is fast, free, and straightforward to use. If you handle sensitive files, there’s really no excuse not to use it.&lt;/p&gt;

&lt;h3 id=&quot;installing-gnupg&quot;&gt;Installing GnuPG&lt;/h3&gt;

&lt;p&gt;I’m on macOS, so I use the &lt;a href=&quot;http://sourceforge.net/projects/macgpg2/files/&quot;&gt;MacGPG2 package&lt;/a&gt; (MacGPG2-2.0.14RC2 at the time of writing). Download the zip, unzip it, and run the installer. A few clicks and you’re ready to start encrypting.&lt;/p&gt;

&lt;h3 id=&quot;encrypting-a-file&quot;&gt;Encrypting a file&lt;/h3&gt;

&lt;p&gt;Say you have a file full of confidential data called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;confidential-data.xls&lt;/code&gt;. Run:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg -c ./confidential-data.xls
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GnuPG will prompt you for a passphrase, then ask you to confirm it. Pick something strong. Once it finishes, you’ll have a new file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;confidential-data.xls.gpg&lt;/code&gt;; that’s the encrypted version. Delete the original and store the encrypted file wherever you need to.&lt;/p&gt;

&lt;h3 id=&quot;decrypting-a-file&quot;&gt;Decrypting a file&lt;/h3&gt;

&lt;p&gt;When you need the data back, retrieve the encrypted file and run:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg -d ./confidential-data.xls.gpg --output ./confidential-data.xls
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it. The decrypted file is back where it started.&lt;/p&gt;

&lt;h3 id=&quot;not-a-command-line-person&quot;&gt;Not a command-line person?&lt;/h3&gt;

&lt;p&gt;I use the command line, which might not be your thing. Honestly, it’s not that scary, and I’d encourage you to give it a go. But if you prefer windows and drag-and-drop, take a look at something like &lt;a href=&quot;http://macgpg.sourceforge.net/&quot;&gt;GPGDropThing&lt;/a&gt;; you can encrypt files just by dropping them onto it.&lt;/p&gt;

&lt;p&gt;The important thing is that you encrypt sensitive data &lt;em&gt;at all&lt;/em&gt;. The specific tool matters less than the habit.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>My Dot Files: Dot Aliases</title>
    <link href="/2010/07/07/my-dot-files-dot-aliases/"/>
    <updated>2010-07-07T00:00:00+08:00</updated>
    <id>/2010/07/07/my-dot-files-dot-aliases/</id>
    <content type="html">&lt;p&gt;This is the first part of a series where I’ll walk through the dotfiles I use to make my day-to-day work easier and more enjoyable.&lt;/p&gt;

&lt;p&gt;I use &lt;a href=&quot;http://git-scm.com/&quot;&gt;Git&lt;/a&gt; and &lt;a href=&quot;http://rubyonrails.org/&quot;&gt;Rails&lt;/a&gt; every day. To save my fingers from unnecessary wear, I’ve created short aliases for the commands I type most often.&lt;/p&gt;

&lt;p&gt;Stick these in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.aliases&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# ~/.aliases&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Record how much I&apos;ve used various Git commands:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#   http://github.com/icefox/git-achievements&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;git&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;git-achievements&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Working with Git&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;git&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;git status&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;git commit&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gca&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;git commit -a&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;ga&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;git add&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gco&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;git checkout&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;git branch&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;git merge&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gd&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;git diff&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Working with Rails&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;script/server&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;script/console&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;rake db:migrate&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;rake&apos;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Open the current directory in TextMate&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;mate .&apos;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Serve the contents of the current directory over HTTP&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;serve&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ruby -rwebrick -e&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;s = WEBrick::HTTPServer.new(:Port =&amp;gt; 3000, :DocumentRoot =&amp;gt; Dir.pwd); trap(&apos;INT&apos;) { s.shutdown }; s.start&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git-achievements&lt;/code&gt; alias wraps the real &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git&lt;/code&gt; binary with &lt;a href=&quot;http://github.com/icefox/git-achievements&quot;&gt;git-achievements&lt;/a&gt;, which tracks how often you use various Git commands. It’s a fun little motivator.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;serve&lt;/code&gt; alias is surprisingly handy; it spins up a quick WEBrick server on port 3000 serving whatever’s in your current directory. Great for previewing static sites or sharing files on a local network.&lt;/p&gt;

&lt;p&gt;Now source the aliases file from your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.profile&lt;/code&gt; so they’re available in every session:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# ~/.profile&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;I &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;aliases&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ~/.&lt;span class=&quot;nv&quot;&gt;$I&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; ~/.&lt;span class=&quot;nv&quot;&gt;$I&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The loop might look like overkill for a single file, but it scales nicely as you add more dotfiles to the pattern; just append their names to the list.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>An Updated Command Prompt</title>
    <link href="/2010/04/12/an-updated-command-prompt/"/>
    <updated>2010-04-12T00:00:00+08:00</updated>
    <id>/2010/04/12/an-updated-command-prompt/</id>
    <content type="html">&lt;p&gt;It’s been a while since I &lt;a href=&quot;http://barkingiguana.com/2008/11/15/get-the-current-git-branch-in-your-command-prompt&quot;&gt;added the current Git branch to my command prompt&lt;/a&gt; to help with my development workflow. Since then I’ve started juggling multiple Ruby versions and I find myself increasingly wanting to know the exit status of the last command at a glance. So I gave my prompt an upgrade.&lt;/p&gt;

&lt;p&gt;Here’s what it looks like now:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/15.jpg&quot; alt=&quot;A screen-shot of my command prompt showing username, hostname, exit code of last command, Ruby interpreter information, current working directory and Git information&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It packs in the username, hostname, last exit code (green for success, red for failure), the active Ruby interpreter and version, the current directory, and Git branch status. Everything I need, nothing I don’t.&lt;/p&gt;

&lt;p&gt;To get this, I declare &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$PS1&lt;/code&gt; like so:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;# Show the exit code of the last command.
# Idea stolen from @mathie.
function last_exit_code() {
  local code=$?
  if [ $code = 0 ]; then
    printf &quot;$1&quot; $code
  else
    printf &quot;$2&quot; $code
  fi
  return $code
}

# I only want to see the interpreter in the output if I&apos;m not using MRI.
function ruby_version() {
  local i=$(/Users/craig/.rvm/bin/rvm-prompt i)
  case $i in
    ruby) printf &quot;$1&quot; $(/Users/craig/.rvm/bin/rvm-prompt $2) ;;
    *)    printf &quot;$1&quot; $(/Users/craig/.rvm/bin/rvm-prompt $3) ;;
  esac
}

# Show lots of info in the __git_ps1 output.
# Thanks for the info @mathie.
export GIT_PS1_SHOWDIRTYSTATE=&quot;true&quot;
export GIT_PS1_SHOWSTASHSTATE=&quot;true&quot;
export GIT_PS1_SHOWUNTRACKEDFILES=&quot;true&quot;

export PS1=&apos;\[\033[01;32m\]\u@\h\[\033[00m\] $(last_exit_code &quot;\[\033[1;32m\]%s\[\033[00m\]&quot; &quot;\[\033[01;31m\]%s\[\033[00m\]&quot;) $(ruby_version &quot;\[\033[01;36m\]%s\[\033[00m\]&quot; &quot;v p&quot; &quot;i v p&quot;) \[\033[01;34m\]\W\[\033[00m\]$(__git_ps1 &quot;\[\033[01;33m\](%s)\[\033[00m\]&quot;)\$ &apos;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A couple of things worth noting. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;last_exit_code&lt;/code&gt; function captures &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$?&lt;/code&gt; immediately; if you wait too long, some other command will overwrite it. And the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ruby_version&lt;/code&gt; function only shows the interpreter name when you’re running something other than MRI, which keeps things tidy for the common case.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GIT_PS1_SHOW*&lt;/code&gt; exports turn on indicators for dirty state, stashed changes, and untracked files in the Git portion of the prompt. If you haven’t tried these, they’re wonderful; you’ll never accidentally commit from the wrong state again.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>A One-Line Web Server in Ruby</title>
    <link href="/2010/04/11/a-one-line-web-server-in-ruby/"/>
    <updated>2010-04-11T00:00:00+08:00</updated>
    <id>/2010/04/11/a-one-line-web-server-in-ruby/</id>
    <content type="html">&lt;p&gt;Inspired by &lt;a href=&quot;http://twitter.com/semanticist/status/11958233080&quot;&gt;a tweet&lt;/a&gt;, here&apos;s how to serve the current directory over HTTP with a single line of Ruby:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ruby -rwebrick -e&apos;WEBrick::HTTPServer.new(:Port =&amp;gt; 3000, :DocumentRoot =&amp;gt; Dir.pwd).start&apos;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&apos;s it. Point a browser at &lt;code&gt;http://localhost:3000&lt;/code&gt; and you&apos;ll see a directory listing. It&apos;s great for quickly sharing files at a conference, previewing static sites, or any situation where you need a throwaway web server with zero setup.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Command-Line EC2 with ec2-api-tools</title>
    <link href="/2010/03/21/command-line-ec2-with-ec2-api-tools/"/>
    <updated>2010-03-21T00:00:00+08:00</updated>
    <id>/2010/03/21/command-line-ec2-with-ec2-api-tools/</id>
    <content type="html">&lt;p&gt;A company I&apos;ve been working with hosts some of their applications on EC2. As someone who has spent years working with Linux and Unix servers from the command line, I find the EC2 web console pretty frustrating. Here&apos;s how I set up the EC2 API tools on my MacBook Pro so I can manage instances from the terminal.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;mkdir ~/.ec2
cd ~/Downloads
curl -O -L &quot;http://www.amazon.com/gp/redirect.html/ref=aws_rc_ec2tools?location=http://s3.amazonaws.com/ec2-downloads/ec2-api-tools.zip&amp;amp;token=A80325AA4DAB186C80828ED5138633E3F49160D9&quot;
unzip ec2-api-tools.zip*
cd ec2-api-tools
mv bin lib ~/.ec2/
echo &apos;export EC2_HOME=~/.ec2
export PATH=$PATH:$EC2_HOME/bin
export EC2_PRIVATE_KEY=`ls $EC2_HOME/pk-*.pem`
export EC2_CERT=`ls $EC2_HOME/cert-*.pem`
export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Home/
# I use eu-west-1 - you may want to change this
EC2_REGION=&quot;eu-west-1&quot;
export EC2_URL=&quot;https://${EC2_REGION}.ec2.amazonaws.com/&quot;
export EC2_KEYPAIR_NAME=&quot;aws-`whoami`&quot;&apos; &amp;gt; ~/.ec2/env
echo &apos;[ -f ~/.ec2/env ] &amp;amp;&amp;amp; . ~/.ec2/env&apos; &amp;gt;&amp;gt; ~/.profile
ec2-add-keypair aws-`whoami` &amp;gt; ~/.ec2/aws-`whoami`
chmod 0600 ~/.ec2/aws-`whoami`&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, download the X.509 private key and certificate from the Security Identifiers page of your AWS account and save them to &lt;code&gt;~/.ec2/&lt;/code&gt;. Leave the filenames as-is with the big messy jumble of characters &amp;mdash; the setup script uses a glob pattern to find them.&lt;/p&gt;

&lt;p&gt;That should be everything. To verify it&apos;s working, try listing all the Amazon-owned machine images:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;ec2-describe-images -o amazon&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You should see a long list that looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;IMAGE	ami-13042f67	amazon/fedora-8-i386-v1.14-std	amazon	available	public		i386	machine	aki-61022915	ari-63022917		ebs
BLOCKDEVICEMAPPING	/dev/sda1		snap-34739d5d	15
IMAGE	ami-1d042f69	amazon/fedora-8-x86_64-v1.14-std	amazon	available	public		x86_64	machine	aki-6d022919	ari-37022943		ebs
BLOCKDEVICEMAPPING	/dev/sda1		snap-08739d61	15&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;All the EC2 commands are prefixed with &lt;code&gt;ec2-&lt;/code&gt;. To see them all:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;ls ~/.ec2/bin/ec2-*&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you see deprecation notices from Xalan, don&apos;t worry about it &amp;mdash; everything still works fine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[Deprecated] Xalan: org.apache.xml.res.XMLErrorResources_en_US&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Creating a New Subversion Branch from an Existing Local Git Branch</title>
    <link href="/2010/03/03/creating-a-new-subversion-branch-from-an-existing-local-git-branch/"/>
    <updated>2010-03-03T00:00:00+08:00</updated>
    <id>/2010/03/03/creating-a-new-subversion-branch-from-an-existing-local-git-branch/</id>
    <content type="html">&lt;p&gt;I frequently have to work with Subversion repositories, and as a Git user I rely on &lt;code&gt;git-svn&lt;/code&gt; to bridge the two worlds. My usual workflow is to do development in local Git branches, then check out the integration branch, merge my changes, and &lt;code&gt;git svn dcommit&lt;/code&gt; to push the code to Subversion.&lt;/p&gt;

&lt;p&gt;Sometimes, though, I need to share an in-progress local branch with a Subversion user before it&apos;s ready to merge into the mainline. Every time this comes up I find myself hunting for the correct sequence of commands, so here they are for future reference.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;git checkout master
git svn branch &amp;lt;new_svn_branch_name&amp;gt;
git svn fetch
git branch -r # make sure &amp;lt;new_svn_branch_name&amp;gt; exists
git checkout -b tmp/svn-rebase-target &amp;lt;new_svn_branch_name&amp;gt;
git rebase --onto tmp/svn-rebase-target master &amp;lt;existing_git_branch_name&amp;gt;
# That should have checked out &amp;lt;existing_git_branch_name&amp;gt;.
git svn dcommit -n # This should say it&apos;ll commit to &amp;lt;new_svn_branch_name&amp;gt;.
git branch -D tmp/svn-rebase-target # clean up the temporary branch.
git svn dcommit&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The key idea: you create a new branch in Subversion, fetch it into Git, then rebase your local work onto it so that &lt;code&gt;git svn dcommit&lt;/code&gt; pushes to the correct place.&lt;/p&gt;

&lt;p&gt;Credit goes to &lt;a href=&quot;http://blog.venthur.de/2009/02/27/git-svn-branch/#comment-132890&quot;&gt;Bjoern Steinbrink&lt;/a&gt; and &lt;a href=&quot;http://blog.venthur.de/2009/02/27/git-svn-branch/#comment-132911&quot;&gt;Cameron&lt;/a&gt; for the comments that pointed me in the right direction.&lt;/p&gt;

&lt;p&gt;I&apos;ve also wrapped this up as a shell script. &lt;a href=&quot;http://gist.github.com/raw/329033/ebba3758bcfb2968796385165a83a3b824dc398e/svn-push.sh&quot;&gt;Download it&lt;/a&gt;, make it executable, and pass it the name of the local branch you want to push:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;./svn-push development/avoid-the-wombat-widgets&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This assumes you told &lt;code&gt;git svn clone&lt;/code&gt; where to find your Subversion branches when you first set up the repository. If you didn&apos;t, your mileage may vary.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Installing the MySQL Gem on OS X 10.6 (Snow Leopard) with MacPorts MySQL5</title>
    <link href="/2010/03/02/installing-the-mysql-gem-on-osx-106-snow-leopard-with-macports-mysql5/"/>
    <updated>2010-03-02T00:00:00+08:00</updated>
    <id>/2010/03/02/installing-the-mysql-gem-on-osx-106-snow-leopard-with-macports-mysql5/</id>
    <content type="html">&lt;p&gt;This one took me longer to figure out than I&apos;d like to admit. If you&apos;re running Snow Leopard with MySQL installed via MacPorts, here&apos;s the incantation you need to install the MySQL gem:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;sudo port install mysql5-server
sudo env ARCHFLAGS=&quot;-arch x86_64&quot; gem install mysql, --with-mysql-config=/opt/local/bin/mysql_config5&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The key details: you need to force the &lt;code&gt;x86_64&lt;/code&gt; architecture flag, and you need to point the gem build at MacPorts&apos; &lt;code&gt;mysql_config5&lt;/code&gt; rather than the default &lt;code&gt;mysql_config&lt;/code&gt; path. Hopefully this saves someone else the half hour I spent on it.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>London Tech Meetups</title>
    <link href="/2010/02/23/london-tech-meetups/"/>
    <updated>2010-02-23T00:00:00+08:00</updated>
    <id>/2010/02/23/london-tech-meetups/</id>
    <content type="html">&lt;p&gt;Finding tech meetups in your area can be surprisingly difficult, and even when you know a group exists, working out when they actually meet can be a puzzle. Some of them have frankly &lt;em&gt;bewildering&lt;/em&gt; scheduling rules.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;http://johnstewartsutherland.com/&quot;&gt;John Sutherland&lt;/a&gt; solved this problem for the &lt;a href=&quot;http://edinburgh2.com/&quot;&gt;Edinburgh tech community&lt;/a&gt; by listing when various groups meet and who they&apos;d be of interest to. With his permission, I&apos;ve done the same thing for &lt;a href=&quot;http://london2.org/&quot;&gt;London tech meetups&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your meetup isn&apos;t listed and you&apos;d like it to be, drop me an email at &lt;a href=&quot;mailto:craig@barkingiguana.com&quot;&gt;craig@barkingiguana.com&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Decoupling Nagios Host and Service Check Events for Fun and Profit</title>
    <link href="/2010/02/17/decoupling-nagios-host-and-service-check-events-for-fun-and-profit/"/>
    <updated>2010-02-17T00:00:00+08:00</updated>
    <id>/2010/02/17/decoupling-nagios-host-and-service-check-events-for-fun-and-profit/</id>
    <content type="html">&lt;p&gt;Nagios does a solid job of watching over my services and hosts, but I want to do a lot more with the events it generates &amp;mdash; when a check fails, when something recovers. Specifically, I want to give clients incredibly fine-grained control over their notifications: what services, how often, and at what level of technical detail. I also want to use those events as upsell opportunities for &lt;a href=&quot;http://xeriom.net/&quot;&gt;Xeriom&lt;/a&gt; &amp;mdash; if a disk is filling up or bandwidth is being consumed faster than expected, it should be easy to suggest a plan upgrade. And I&apos;d like to experiment with fun delivery mechanisms &amp;mdash; iPhone push notifications, SMS gateways, audible alarms, whatever &amp;mdash; without any risk of breaking Nagios itself.&lt;/p&gt;

&lt;p&gt;Message queues are the natural solution here. They let you decouple systems, moving complexity and risk away from the core. Nagios shouldn&apos;t have to worry about any of this extra stuff. It should just do what it&apos;s good at: monitoring hosts and services.&lt;/p&gt;

&lt;p&gt;Luckily, &lt;a href=&quot;http://barkingiguana.com/2008/12/13/deploying-activemq-on-ubuntu-810&quot;&gt;I already have ActiveMQ running&lt;/a&gt; for other tasks, &lt;a href=&quot;http://barkingiguana.com/2009/01/01/writing-rubystomp-clients-with-smqueue&quot;&gt;writing a STOMP client with SMQueue&lt;/a&gt; is straightforward, and Nagios has several ways to execute external commands when events occur, including the &lt;a href=&quot;http://nagios.sourceforge.net/docs/3_0/configmain.html#global_host_event_handler&quot;&gt;global host and service event handlers&lt;/a&gt;. All I need is a command that accepts event data from Nagios and drops it onto the message queue.&lt;/p&gt;

&lt;p&gt;Here&apos;s what I came up with:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;rubygems&apos;
require &apos;smqueue&apos;
require &apos;json&apos;

message = {
  :hostname =&amp;gt; ARGV[2],
  :service =&amp;gt; ARGV[3],
  :state =&amp;gt; ARGV[4],
  :state_type =&amp;gt; ARGV[5],
  :state_time =&amp;gt; ARGV[6].to_i,
  :attempt =&amp;gt; ARGV[7].to_i,
  :max_attempts =&amp;gt; ARGV[8].to_i,
  :time_t =&amp;gt; Time.now.to_i
}

configuration = {
  :host =&amp;gt; ARGV[0],
  :name =&amp;gt; ARGV[1],
  :adapter =&amp;gt; :StompAdapter
}

broadcast = SMQueue(configuration)
broadcast.put message.to_json, &quot;content-type&quot; =&amp;gt; &quot;application/json&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You&apos;ll need Ruby and RubyGems installed. Once you have those, install the dependencies and the script like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;sudo su -
gem sources -a http://gems.github.com/
gem install seanohalpin-smqueue json --no-ri --no-rdoc
cd /usr/bin
wget http://gist.github.com/raw/306765/2a3e9cbade88b4c6dd430e108bc8a28f95047462/notify-service-by-stomp.rb
chmod +x notify-service-by-stomp.rb&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once installed, tell Nagios to use it by adding this to your Nagios configuration:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;define command {
  command_name notify-service-by-stomp
  command_line /usr/bin/notify-service-by-stomp.rb mq.example.com /topic/foo.bar.baz.quux $HOSTADDRESS$ &quot;$SERVICEDESC$&quot; $SERVICESTATE$ $SERVICESTATETYPE$ $SERVICEDURATIONSEC$ $SERVICEATTEMPT$ $MAXSERVICEATTEMPTS$
}

global_service_event_handler=notify-service-by-stomp&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Change &lt;code&gt;mq.example.com&lt;/code&gt; to the hostname of your message broker, and &lt;code&gt;/topic/foo.bar.baz.quux&lt;/code&gt; to whatever topic or queue you want notifications sent to. Restart Nagios and events should start flowing.&lt;/p&gt;

&lt;h3&gt;Testing it&lt;/h3&gt;

&lt;p&gt;If your Nagios doesn&apos;t generate events very often, you&apos;ll want a way to verify everything is wired up correctly. Attach a simple &lt;code&gt;stompcat&lt;/code&gt; listener to the topic, then manually fire some test notifications.&lt;/p&gt;

&lt;p&gt;Here&apos;s a quick stompcat tool in case you don&apos;t have one handy:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;#! /usr/bin/env ruby

# Run me like this:
#
#   ./stompcat.rb mq.example.com /topic/foo.bar.baz.quux
#

require &apos;rubygems&apos;
require &apos;smqueue&apos;

configuration = {
  :host =&amp;gt; ARGV[0],
  :name =&amp;gt; ARGV[1],
  :adapter =&amp;gt; :StompAdapter
}

source = SMQueue(configuration)
source.get do |m|
  payload = m.body
  puts &quot;&amp;gt;&amp;gt;&amp;gt; #{payload}&quot;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And here&apos;s how to send a test notification to the queue:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/usr/bin/notify-service-by-stomp.rb mq.example.com \
  /topic/foo.bar.baz.quux service-host.example.com &quot;SERVICE NAME&quot; \
  WARNING HARD 86492 6 6&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If it&apos;s working, you should see something like this appear in your stompcat output:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;{
  &quot;time_t&quot;:1266427384,
  &quot;state&quot;:&quot;WARNING&quot;,
  &quot;state_type&quot;:&quot;HARD&quot;,
  &quot;state_time&quot;:86492,
  &quot;attempt&quot;:6,
  &quot;hostname&quot;:&quot;service-host.example.com&quot;,
  &quot;max_attempts&quot;:6,
  &quot;service&quot;:&quot;SERVICE NAME&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;From here, you can modify the stompcat example to do anything you like &amp;mdash; look up clients in a database, send SMS alerts if an account has enough credit, trigger webhooks, whatever takes your fancy. If you build something fun with this, I&apos;d love to hear about it.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Correct OID for System Uptime</title>
    <link href="/2010/02/11/the-correct-oid-for-system-uptime/"/>
    <updated>2010-02-11T00:00:00+08:00</updated>
    <id>/2010/02/11/the-correct-oid-for-system-uptime/</id>
    <content type="html">&lt;p&gt;I use &lt;a href=&quot;http://www.net-snmp.org/&quot;&gt;SNMP&lt;/a&gt; to track system uptime so I know when hosts have recently rebooted. But I keep making the same mistake: reaching for &lt;code&gt;sysUpTime.0&lt;/code&gt; when I should be using &lt;code&gt;hrSystem.hrSystemUptime.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here&apos;s the difference, so I stop tripping over this:&lt;/p&gt;

&lt;dl&gt;
  &lt;dt&gt;&lt;code&gt;sysUpTime.0&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;Timeticks (in hundredths of a second) since &lt;strong&gt;snmpd started&lt;/strong&gt;. If someone restarts the SNMP daemon, this resets &amp;mdash; even though the machine hasn&apos;t rebooted.&lt;/dd&gt;
  &lt;dt&gt;&lt;code&gt;hrSystem.hrSystemUptime.0&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;Timeticks since &lt;strong&gt;the hardware started&lt;/strong&gt;. This is the one you want for actual system uptime.&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;In short: if you want to know how long the machine has been running, use &lt;code&gt;hrSystem.hrSystemUptime.0&lt;/code&gt;. If you want to know how long the SNMP agent has been running, use &lt;code&gt;sysUpTime.0&lt;/code&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Keeping the Software on Your Ubuntu Server Up to Date</title>
    <link href="/2010/02/11/keeping-the-software-on-your-ubuntu-server-up-to-date/"/>
    <updated>2010-02-11T00:00:00+08:00</updated>
    <id>/2010/02/11/keeping-the-software-on-your-ubuntu-server-up-to-date/</id>
    <content type="html">&lt;p&gt;New exploits are discovered just about every day in software both old and new. To combat this, software vendors release security updates, which the Ubuntu team packages up and ships as new, more secure versions of the software you’ve installed.&lt;/p&gt;

&lt;p&gt;Supporting every version of every package ever built for Ubuntu would be an impossible task, so the Ubuntu team produces releases with defined support windows. There are two kinds: Long Term Support (LTS) releases get 5 years of server support after the release date, while regular releases get 18 months. Once a support window closes, you won’t receive security updates or be able to easily upgrade packages, so it’s important to plan your upgrades before support ends.&lt;/p&gt;

&lt;p&gt;Here are the commonly referenced releases, their dates, and their support windows:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Name&lt;/th&gt;
      &lt;th&gt;Release Date&lt;/th&gt;
      &lt;th&gt;Support Ends&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;10.04 [LTS]&lt;/td&gt;
      &lt;td&gt;Lucid Lynx&lt;/td&gt;
      &lt;td&gt;April 2010&lt;/td&gt;
      &lt;td&gt;April 2015&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;9.10&lt;/td&gt;
      &lt;td&gt;Karmic Koala&lt;/td&gt;
      &lt;td&gt;October 29, 2009&lt;/td&gt;
      &lt;td&gt;April 2011&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;9.04&lt;/td&gt;
      &lt;td&gt;Jaunty Jackalope&lt;/td&gt;
      &lt;td&gt;April 23, 2009&lt;/td&gt;
      &lt;td&gt;October 2010&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8.10&lt;/td&gt;
      &lt;td&gt;Intrepid Ibex&lt;/td&gt;
      &lt;td&gt;October 30, 2008&lt;/td&gt;
      &lt;td&gt;April 2010&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8.04.4 [LTS]&lt;/td&gt;
      &lt;td&gt;Hardy Heron&lt;/td&gt;
      &lt;td&gt;January 28, 2010&lt;/td&gt;
      &lt;td&gt;April 2013&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8.04.3 [LTS]&lt;/td&gt;
      &lt;td&gt;Hardy Heron&lt;/td&gt;
      &lt;td&gt;July 16, 2009&lt;/td&gt;
      &lt;td&gt;April 2013&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8.04.2 [LTS]&lt;/td&gt;
      &lt;td&gt;Hardy Heron&lt;/td&gt;
      &lt;td&gt;January 22, 2009&lt;/td&gt;
      &lt;td&gt;April 2013&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8.04.1 [LTS]&lt;/td&gt;
      &lt;td&gt;Hardy Heron&lt;/td&gt;
      &lt;td&gt;July 3, 2008&lt;/td&gt;
      &lt;td&gt;April 2013&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8.04 [LTS]&lt;/td&gt;
      &lt;td&gt;Hardy Heron&lt;/td&gt;
      &lt;td&gt;April 24, 2008&lt;/td&gt;
      &lt;td&gt;April 2013&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;7.10&lt;/td&gt;
      &lt;td&gt;Gutsy Gibbon&lt;/td&gt;
      &lt;td&gt;October 18, 2007&lt;/td&gt;
      &lt;td&gt;April 2009&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;7.04&lt;/td&gt;
      &lt;td&gt;Feisty Fawn&lt;/td&gt;
      &lt;td&gt;April 19, 2007&lt;/td&gt;
      &lt;td&gt;October 2008&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6.10&lt;/td&gt;
      &lt;td&gt;Edgy Eft&lt;/td&gt;
      &lt;td&gt;October 26, 2006&lt;/td&gt;
      &lt;td&gt;April 2008&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6.06.2 [LTS]&lt;/td&gt;
      &lt;td&gt;Dapper Drake&lt;/td&gt;
      &lt;td&gt;January 21, 2008&lt;/td&gt;
      &lt;td&gt;June 2011&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6.06.1 [LTS]&lt;/td&gt;
      &lt;td&gt;Dapper Drake&lt;/td&gt;
      &lt;td&gt;August 10, 2006&lt;/td&gt;
      &lt;td&gt;June 2011&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6.06 [LTS]&lt;/td&gt;
      &lt;td&gt;Dapper Drake&lt;/td&gt;
      &lt;td&gt;June 1, 2006&lt;/td&gt;
      &lt;td&gt;June 2011&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5.10&lt;/td&gt;
      &lt;td&gt;Breezy Badger&lt;/td&gt;
      &lt;td&gt;October 12, 2005&lt;/td&gt;
      &lt;td&gt;April 2007&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5.04&lt;/td&gt;
      &lt;td&gt;Hoary Hedgehog&lt;/td&gt;
      &lt;td&gt;April 8, 2005&lt;/td&gt;
      &lt;td&gt;October 2006&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4.10&lt;/td&gt;
      &lt;td&gt;Warty Warthog&lt;/td&gt;
      &lt;td&gt;October 26, 2004&lt;/td&gt;
      &lt;td&gt;April 2006&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;At the time of writing, the currently supported releases are 6.06, 8.04, 8.10, 9.04, and 9.10. Ubuntu 10.04 is due in April.&lt;/p&gt;

&lt;h2 id=&quot;your-responsibilities&quot;&gt;Your responsibilities&lt;/h2&gt;

&lt;p&gt;As a server operator, there are two things you need to know how to do: upgrade installed packages, and upgrade to the next Ubuntu release. I’ll cover both, but first let’s do a little setup to make the whole process faster.&lt;/p&gt;

&lt;h3 id=&quot;using-a-package-mirror&quot;&gt;Using a package mirror&lt;/h3&gt;

&lt;p&gt;The most time-consuming part of any update is downloading packages from remote servers. To speed things up, &lt;a href=&quot;http://xeriom.net/&quot;&gt;Xeriom Networks&lt;/a&gt; provides a local mirror of the software packages for 8.04, 8.10, 9.04, and 9.10. If you’re not hosted with Xeriom (why not?), ask your provider whether they offer a package mirror. If they don’t, skip this section and hope your connection is fast enough.&lt;/p&gt;

&lt;p&gt;Setting up the mirror requires editing just one file. A straightforward editor for this is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nano&lt;/code&gt;. Install it by connecting to your server via SSH and running:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nano &lt;span class=&quot;nt&quot;&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, find out which Ubuntu release you’re running:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /etc/lsb-release
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Match your release to the appropriate entry on this wiki page: &lt;a href=&quot;http://wiki.xeriom.net/w/XeriomUbuntuPackagesService&quot;&gt;http://wiki.xeriom.net/w/XeriomUbuntuPackagesService&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy the text from the box that matches your release. Then open the sources list for editing:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nano &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; /etc/apt/sources.list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Delete all existing lines and paste in the text you copied. Save and exit with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+X&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now tell Ubuntu to refresh its package list so it picks up the local mirror:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You’re now using the Xeriom package mirror.&lt;/p&gt;

&lt;h3 id=&quot;upgrading-installed-software&quot;&gt;Upgrading installed software&lt;/h3&gt;

&lt;p&gt;Keeping your packages up to date is one of the most important things you can do for server security. That said, new packages can occasionally break things, so don’t set this up to run automatically. Sit down, review what’s changing, and apply updates deliberately.&lt;/p&gt;

&lt;p&gt;First, refresh your package database to make sure you’re seeing the latest available versions:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then ask &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt-get&lt;/code&gt; to upgrade your installed packages:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get upgrade
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This calculates everything that needs upgrading, shows you the list, and asks for confirmation. Most of the time it will run smoothly, but always check what’s about to change before saying yes.&lt;/p&gt;

&lt;h3 id=&quot;upgrading-to-the-next-release&quot;&gt;Upgrading to the next release&lt;/h3&gt;

&lt;p&gt;A full release upgrade is a bigger operation. A large number of packages will be updated, and you’ll almost certainly need to reboot (the kernel is usually among the upgraded packages), so plan for a little downtime.&lt;/p&gt;

&lt;p&gt;You’ll need the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update-manager-core&lt;/code&gt; package. If this is your first release upgrade, install it:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;update-manager-core
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, configure your upgrade strategy. Open the configuration file:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nano &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; /etc/update-manager/release-upgrades
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Find the line that starts with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Prompt=&lt;/code&gt; and set it to one of: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lts&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;normal&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;never&lt;/code&gt;. For example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Prompt=lts&lt;/code&gt; will only offer upgrades to LTS releases, giving you 5 years of support per release. Save and exit with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+X&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before you upgrade, read the &lt;a href=&quot;http://www.ubuntu.com/getubuntu/releasenotes/&quot;&gt;release notes&lt;/a&gt; for the version you’re upgrading to. Make sure you understand any known issues and caveats.&lt;/p&gt;

&lt;p&gt;Once you’re satisfied and have scheduled a maintenance window, start the upgrade:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-release-upgrade&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will calculate the full list of package changes and ask for confirmation. Don’t just say yes; read through the list and make sure you understand what upgrading means for your setup.&lt;/p&gt;

&lt;h2 id=&quot;if-it-all-goes-wrong&quot;&gt;If it all goes wrong&lt;/h2&gt;

&lt;p&gt;Sometimes things break. Maybe a new release has an unexpected issue, or the upgrade removes a package your application depends on. If that happens, we can create a fresh image of whatever supported release you need. Your data won’t be on the new image, of course, so make sure your backups are current before you start.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Getting Started with Node.js</title>
    <link href="/2010/01/21/getting-started-with-node-js/"/>
    <updated>2010-01-21T00:00:00+08:00</updated>
    <id>/2010/01/21/getting-started-with-node-js/</id>
    <content type="html">&lt;p&gt;Tonight I&apos;m giving a talk at the &lt;a href=&quot;http://javascript.meetup.com/3/calendar/12285246/&quot;&gt;London JavaScript User Group&lt;/a&gt;, introducing &lt;a href=&quot;http://nodejs.org/&quot;&gt;Node.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The slides are available here: &lt;a href=&quot;http://barkingiguana.com/~craig/talks/2010/javascript-london/getting-started-with-node-js&quot;&gt;Getting Started with Node.js&lt;/a&gt;. If you print them out you&apos;ll find speaker notes included, or you can watch the &lt;a href=&quot;http://vimeo.com/9125286&quot;&gt;video on Vimeo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any feedback, I&apos;d love to hear it &amp;mdash; please leave a comment.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Telnet 101</title>
    <link href="/2009/12/10/telnet-101/"/>
    <updated>2009-12-10T00:00:00+08:00</updated>
    <id>/2009/12/10/telnet-101/</id>
    <content type="html">&lt;p&gt;Telnet has been around since before the dawn of &lt;a href=&quot;http://en.wikipedia.org/wiki/Unix_time&quot;&gt;Unix time&lt;/a&gt;, yet surprisingly few people know how to wield this tremendously useful debugging tool. A few seconds with telnet can save you hours of frustrated searching, trial-and-error config changes, and shouting at your monitor.&lt;/p&gt;

&lt;p&gt;Telnet lets you speak plain-text protocols by hand. I&apos;ve used it to talk to &lt;a href=&quot;http://barkingiguana.com/2008/07/07/high-availability-mysql-on-ubuntu-804&quot;&gt;MySQL&lt;/a&gt;, &lt;a href=&quot;http://barkingiguana.com/2009/03/04/memcache-statistics-from-the-command-line&quot;&gt;Memcached&lt;/a&gt;, and &lt;a href=&quot;http://barkingiguana.com/2008/06/22/a-simple-email-hub-for-your-local-network&quot;&gt;Postfix&lt;/a&gt;. Here I&apos;ll show you how to use it to verify that an HTTP server can serve content over HTTP/1.1.&lt;/p&gt;

&lt;h3&gt;What is HTTP?&lt;/h3&gt;

&lt;p&gt;Before we can simulate HTTP with telnet, we need a quick refresher on how the protocol works.&lt;/p&gt;

&lt;p&gt;HTTP/1.1 &amp;mdash; the HyperText Transfer Protocol &amp;mdash; is a plain-text protocol defined in &lt;a href=&quot;http://www.w3.org/Protocols/rfc2616/rfc2616.html&quot;&gt;RFC 2616&lt;/a&gt;. It&apos;s used for all sorts of things, but the most visible use for most people is fetching web pages.&lt;/p&gt;

&lt;p&gt;When you request a webpage, your browser connects to the web server and sends a request. A typical one looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET / HTTP/1.1
Host: example.com

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The format is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[METHOD] [PATH] HTTP/1.1
Host: [HOSTNAME]
[BLANK LINE]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server responds with something like this &amp;mdash; headers first, then a blank line, then the page content:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
Server: Apache/2.2.3 (Red Hat)
Last-Modified: Tue, 15 Nov 2005 13:24:10 GMT
ETag: &quot;b300b4-1b6-4059a80bfd280&quot;
Accept-Ranges: bytes
Content-Type: text/html; charset=UTF-8
Connection: close
Date: Thu, 10 Dec 2009 10:37:33 GMT
Age: 7114
Content-Length: 438

&amp;lt;HTML&amp;gt;
&amp;lt;HEAD&amp;gt;
  &amp;lt;TITLE&amp;gt;Example Web Page&amp;lt;/TITLE&amp;gt;
&amp;lt;/HEAD&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;p&amp;gt;You have reached this web page by typing &amp;quot;example.com&amp;quot;,
&amp;quot;example.net&amp;quot;,
  or &amp;quot;example.org&amp;quot; into your web browser.&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;These domain names are reserved for use in documentation and are not available
  for registration. See &amp;lt;a href=&quot;http://www.rfc-editor.org/rfc/rfc2606.txt&quot;&amp;gt;RFC
  2606&amp;lt;/a&amp;gt;, Section 3.&amp;lt;/p&amp;gt;
&amp;lt;/BODY&amp;gt;
&amp;lt;/HTML&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The response format is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTTP/1.1 [STATUS CODE AND REASON]
[HEADERS]
[BLANK LINE]
[BODY]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There&apos;s a &lt;em&gt;lot&lt;/em&gt; more to HTTP, all documented in rather dry detail in &lt;a href=&quot;http://www.w3.org/Protocols/rfc2616/rfc2616.html&quot;&gt;RFC 2616&lt;/a&gt;. Mostly you can skim it for the parts you need.&lt;/p&gt;

&lt;h3&gt;Trying it with telnet&lt;/h3&gt;

&lt;p&gt;Now that we know how HTTP requests look, let&apos;s use telnet to make one by hand.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;http://unixhelp.ed.ac.uk/CGI/man-cgi?telnet&quot;&gt;telnet man page&lt;/a&gt; tells us the command accepts a host and a port. We want to talk to &lt;code&gt;example.com&lt;/code&gt; on port 80 (the standard HTTP port):&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;telnet example.com 80&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You&apos;ll see output like this as it connects:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Trying 192.0.32.10...
Connected to example.com.
Escape character is &apos;^]&apos;.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now your cursor is sitting on a blank line. This is where you become the browser. Type the GET request from above (including the blank line at the end), and after a short pause you should get the example.com web page back.&lt;/p&gt;

&lt;h3&gt;Why is this useful?&lt;/h3&gt;

&lt;p&gt;Manually requesting a page like this can quickly expose several common problems:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Firewall issues&lt;/strong&gt; &amp;mdash; if telnet can&apos;t connect, you know the problem is at the network level, not in your application.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Status codes&lt;/strong&gt; &amp;mdash; the response code tells you exactly what the server did with your request. &lt;a href=&quot;http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html&quot;&gt;RFC 2616, Section 10&lt;/a&gt; has the full list.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;No caching surprises&lt;/strong&gt; &amp;mdash; unlike a browser, telnet won&apos;t serve you a stale cached version of the page.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Header inspection&lt;/strong&gt; &amp;mdash; you can see every header the server returns, which is invaluable for debugging.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Compression testing&lt;/strong&gt; &amp;mdash; add an &lt;a href=&quot;http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3&quot;&gt;Accept-Encoding&lt;/a&gt; header to verify your assets are being served gzipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And telnet isn&apos;t limited to HTTP. SMTP, IMAP, POP, and many other plain-text protocols can all be explored this way. It&apos;s not a silver bullet, but it&apos;s one of the most useful tools you&apos;ll find already installed on your machine.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Simulating Slow or Laggy Network Connections on OS X</title>
    <link href="/2009/12/04/simulating-slow-or-laggy-network-connections-in-os-x/"/>
    <updated>2009-12-04T00:00:00+08:00</updated>
    <id>/2009/12/04/simulating-slow-or-laggy-network-connections-in-os-x/</id>
    <content type="html">&lt;p&gt;A client recently reported that their site was loading painfully slowly from certain remote locations. We got the specs of their network connection, but every single time I need to simulate bandwidth limits or latency on OS X I end up searching for the same commands. So here they are, written down once and for all.&lt;/p&gt;

&lt;h3&gt;Set up the pipe&lt;/h3&gt;

&lt;p&gt;First, configure an &lt;code&gt;ipfw&lt;/code&gt; pipe with the bandwidth limit and delay you want to simulate.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;sudo ipfw pipe 1 config bw 16Kbit/s delay 350ms&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Attach it to HTTP traffic&lt;/h3&gt;

&lt;p&gt;Next, attach the pipe to all traffic going to or coming from port 80.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;sudo ipfw add 1 pipe 1 src-port 80
sudo ipfw add 2 pipe 1 dst-port 80&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;All HTTP traffic is now throttled through your simulated connection. Do your testing, experience the pain your users feel, and then clean up.&lt;/p&gt;

&lt;h3&gt;Tear it down&lt;/h3&gt;

&lt;p&gt;Once you&apos;re done (or once you get frustrated with how slowly everything loads), remove the firewall rules and delete the pipe.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;sudo ipfw delete 1
sudo ipfw delete 2&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;sudo ipfw pipe 1 delete&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And you&apos;re back to full speed. Adjust the &lt;code&gt;bw&lt;/code&gt; and &lt;code&gt;delay&lt;/code&gt; values to match whatever real-world connection you&apos;re trying to reproduce.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Returning Explicitly Is Slower</title>
    <link href="/2009/11/11/returning-explicitly-is-slower/"/>
    <updated>2009-11-11T00:00:00+08:00</updated>
    <id>/2009/11/11/returning-explicitly-is-slower/</id>
    <content type="html">&lt;p&gt;My main objection to &lt;a href=&quot;http://barkingiguana.com/2009/10/21/you-dont-need-to-return-explicitly&quot;&gt;returning explicitly&lt;/a&gt; is readability. It is a subjective thing, but every time I see an unnecessary &lt;code class=&quot;ruby&quot;&gt;return&lt;/code&gt; statement my internal &lt;a href=&quot;http://www.osnews.com/story/19266/WTFs_m&quot;&gt;WTF counter&lt;/a&gt; ticks up.&lt;/p&gt;

&lt;p&gt;Less subjectively, it has been &lt;a href=&quot;http://barkingiguana.com/2009/10/24/the-truth-speaks-for-itself#c000073&quot;&gt;pointed&lt;/a&gt; &lt;a href=&quot;http://barkingiguana.com/2009/10/21/you-dont-need-to-return-explicitly#c000074&quot;&gt;out&lt;/a&gt; that returning explicitly is actually slower. Let&apos;s measure it.&lt;/p&gt;

&lt;p&gt;Benchmarking in Ruby is easy:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;benchmark&apos;

def explicit
  return &quot;TEST&quot;
end

def implicit
  &quot;TEST&quot;
end

n = 100_000_000
Benchmark.bmbm do |x|
  x.report(&quot;Explicit return&quot;) { n.times { explicit } }
  x.report(&quot;Implicit return&quot;) { n.times { implicit } }
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And here are the results:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Rehearsal ---------------------------------------------------
Explicit return  50.380000   0.210000  50.590000 ( 51.000510)
Implicit return  36.200000   0.100000  36.300000 ( 36.454038)
----------------------------------------- total: 86.890000sec

                      user     system      total        real
Explicit return  47.650000   0.070000  47.720000 ( 47.744167)
Implicit return  35.900000   0.070000  35.970000 ( 35.985493)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So yes, returning explicitly is slower &amp;mdash; but like the &lt;code class=&quot;ruby&quot;&gt;Symbol#to_proc&lt;/code&gt; question, it is &lt;a href=&quot;http://barkingiguana.com/2008/11/18/symbol-to_proc-is-slow-is-it-slow-enough-to-matter&quot;&gt;not slow enough to matter&lt;/a&gt; in practice. You need an enormous number of returns before the difference becomes significant.&lt;/p&gt;

&lt;p&gt;Does this change my mind? No. Returning explicitly is still ugly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Update:&lt;/em&gt; The benchmark above was run on Ruby 1.8.6. &lt;a href=&quot;http://tomafro.net/&quot;&gt;Tom Ward&lt;/a&gt; has provided &lt;a href=&quot;http://tomafro.net/2009/08/the-cost-of-explicit-returns-in-ruby&quot;&gt;similar benchmarks&lt;/a&gt; for Ruby 1.8.7, 1.9, and JRuby 1.1.6 (using n = 10,000,000) which show that the cost of explicit returns on these platforms is negligible. Still ugly though.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Stack Trace Is Precious</title>
    <link href="/2009/11/10/the-stack-trace-is-precious/"/>
    <updated>2009-11-10T00:00:00+08:00</updated>
    <id>/2009/11/10/the-stack-trace-is-precious/</id>
    <content type="html">&lt;p&gt;The stack trace is one of the most valuable pieces of information you can have when debugging. It tells you exactly which line of code was running when an error was thrown, and it gives you the full execution path that led there.&lt;/p&gt;

&lt;p&gt;So here is a quick plea. Please don&apos;t do this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;def foo
  do_something
rescue =&amp;gt; e
  puts &quot;Problem: #{e}&quot;
  raise e
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Writing &lt;code class=&quot;ruby&quot;&gt;raise e&lt;/code&gt; starts a &lt;em&gt;new&lt;/em&gt; stack trace originating at the raise call itself. If something further up the stack rescues this exception, there is no indication of where the problem originally occurred &amp;mdash; all you get is a pointer to the error handling code. Precious information, gone.&lt;/p&gt;

&lt;p&gt;Do this instead:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;def foo
  do_something
rescue =&amp;gt; e
  puts &quot;Problem: #{e}&quot;
  raise
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice the bare &lt;code class=&quot;ruby&quot;&gt;raise&lt;/code&gt; with no argument. This tells Ruby to &lt;a href=&quot;http://web.njit.edu/all_topics/Prog_Lang_Docs/html/ruby/syntax.html#raise&quot;&gt;re-raise the current exception&lt;/a&gt;, keeping the original stack trace intact. Debugging can continue unhindered.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The Truth Speaks for Itself</title>
    <link href="/2009/10/24/the-truth-speaks-for-itself/"/>
    <updated>2009-10-24T00:00:00+08:00</updated>
    <id>/2009/10/24/the-truth-speaks-for-itself/</id>
    <content type="html">&lt;p&gt;This one isn&apos;t just for Ruby &amp;mdash; it applies to pretty much every programming language under the sun.&lt;/p&gt;

&lt;p&gt;Don&apos;t wrap a boolean expression in a control statement just to return &lt;code class=&quot;ruby&quot;&gt;true&lt;/code&gt; or &lt;code class=&quot;ruby&quot;&gt;false&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;def foo
  if some_boolean &amp;amp;&amp;amp; other_boolean
    return true
  else
    return false
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The expression already &lt;em&gt;is&lt;/em&gt; a boolean. Return it directly:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;def foo
  return some_boolean &amp;amp;&amp;amp; other_boolean
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It is very rare that I ever need to return an explicit &lt;code class=&quot;ruby&quot;&gt;true&lt;/code&gt; or &lt;code class=&quot;ruby&quot;&gt;false&lt;/code&gt;. If you find yourself doing it, treat it as a warning sign.&lt;/p&gt;

&lt;p&gt;And of course, in Ruby &lt;a href=&quot;http://barkingiguana.com/2009/10/21/you-dont-need-to-return-explicitly&quot;&gt;you don&apos;t need to return explicitly&lt;/a&gt;, so you can simplify further:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;def foo
  some_boolean &amp;amp;&amp;amp; other_boolean
end&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>You Don't Need to Return Explicitly</title>
    <link href="/2009/10/21/you-dont-need-to-return-explicitly/"/>
    <updated>2009-10-21T00:00:00+08:00</updated>
    <id>/2009/10/21/you-dont-need-to-return-explicitly/</id>
    <content type="html">&lt;p&gt;In Ruby, every method returns the value of the last expression evaluated. There is no need to spell it out.&lt;/p&gt;

&lt;p&gt;Don&apos;t do this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;def foo
  value = Foo.first(:conditions =&amp;gt; { :label =&amp;gt; &quot;bar&quot; })
  return value
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Do this instead:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;def foo
  Foo.first(:conditions =&amp;gt; { :label =&amp;gt; &quot;bar&quot; })
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code class=&quot;ruby&quot;&gt;return&lt;/code&gt; keyword still has its place &amp;mdash; early returns for guard clauses, for instance &amp;mdash; but if you are just returning the last expression, let Ruby do what Ruby does.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Twitter OAuth Authentication Using Ruby</title>
    <link href="/2009/10/13/twitter-oauth-authentication-using-ruby/"/>
    <updated>2009-10-13T00:00:00+08:00</updated>
    <id>/2009/10/13/twitter-oauth-authentication-using-ruby/</id>
    <content type="html">&lt;p&gt;Here are the steps involved in using Twitter for OAuth authentication. I wanted this post a few days ago and couldn&apos;t find it anywhere, so I wrote it myself.&lt;/p&gt;

&lt;p&gt;First, install the required gems:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo gem install json oauth&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, set up your application at &lt;a href=&quot;http://twitter.com/apps&quot;&gt;http://twitter.com/apps&lt;/a&gt;. Make sure you choose &lt;em&gt;Browser&lt;/em&gt; as the application type and check the box to use Twitter for login.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A gotcha: if you make a mistake on the new application form, it will silently reset the application type to &lt;strong&gt;Client&lt;/strong&gt; and uncheck the login box. Double-check these settings before saving.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now for the actual code. Despite the hugely complicated examples floating around elsewhere, you only need two actions: one to initiate the authentication request (the login action) and one to handle the callback when Twitter sends the user back. If you have used &lt;a href=&quot;http://openidenabled.com/ruby-openid/&quot;&gt;OpenID&lt;/a&gt; before, this flow should feel familiar.&lt;/p&gt;

&lt;p&gt;Your login action looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;# consumer_key and consumer_secret are from Twitter.
# You&apos;ll get them on your application details page.
oauth = OAuth::Consumer.new(consumer_key, consumer_secret,
                             { :site =&amp;gt; &quot;http://twitter.com&quot; })

# Ask for a token to make a request
url = &quot;http://whatever.com/login/complete&quot;
request_token = oauth.get_request_token(:oauth_callback =&amp;gt; url)

# Take a note of the token and the secret. You&apos;ll need these later
session[:token] = request_token.token
session[:secret] = request_token.secret

# Send the user to Twitter to be authenticated
redirect_to request_token.authorize_url&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Your callback action looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;# Your callback URL will receive a request containing an
# oauth_verifier. Use this along with the request token from
# earlier to construct an access request.
request_token = OAuth::RequestToken.new(oauth, session[:token],
                                        session[:secret])
access_token = request_token.get_access_token(
                 :oauth_verifier =&amp;gt; params[:oauth_verifier])

# consumer_key and consumer_secret are from Twitter.
# You&apos;ll get them on your application details page.
oauth = OAuth::Consumer.new(consumer_key, consumer_secret,
                             { :site =&amp;gt; &quot;http://twitter.com&quot; })

# Get account details from Twitter
response = oauth.request(:get, &apos;/account/verify_credentials.json&apos;,
                         access_token, { :scheme =&amp;gt; :query_string })

# Then do stuff with the details
user_info = JSON.parse(response.body)
# Like find the person that logged in...
Person.find_by_twitter_id(user_info[&quot;id&quot;])&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you keep getting &lt;code&gt;401 Unauthorized&lt;/code&gt; errors after implementing this, check that your application is set to &lt;em&gt;Browser&lt;/em&gt; mode in the Twitter configuration. That tripped me up for longer than I would like to admit.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>You Don't Need to Count Array Offsets by Hand</title>
    <link href="/2009/10/02/you-dont-need-to-count-array-offsets-by-hand/"/>
    <updated>2009-10-02T00:00:00+08:00</updated>
    <id>/2009/10/02/you-dont-need-to-count-array-offsets-by-hand/</id>
    <content type="html">&lt;p&gt;When you need both the item and its index while iterating over an array in Ruby, don&apos;t do this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;index = 0
for item in array
  index += 1
  puts &quot;Item #{index}: #{item.inspect}&quot;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Do this instead:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;array.each_with_index do |item, index|
  puts &quot;Item #{index}: #{item.inspect}&quot;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ruby&apos;s &lt;a href=&quot;http://ruby-doc.org/core/classes/Enumerable.html&quot;&gt;Enumerable&lt;/a&gt; module is full of handy methods like this. Take a few minutes to read through the documentation &amp;mdash; your code will be better for it.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>First Steps with RabbitMQ in Ruby 1.8.6</title>
    <link href="/2009/08/13/first-steps-with-rabbit-mq-in-ruby-186/"/>
    <updated>2009-08-13T00:00:00+08:00</updated>
    <id>/2009/08/13/first-steps-with-rabbit-mq-in-ruby-186/</id>
    <content type="html">&lt;p&gt;Until recently I was perfectly happy using ActiveMQ as my message broker. I had heard of &lt;a href=&quot;http://www.rabbitmq.com/&quot;&gt;RabbitMQ&lt;/a&gt; several times but never got around to investigating it. Then a &lt;a href=&quot;http://skillsmatter.com/podcast/ajax-ria/amqp-in-ruby&quot;&gt;talk&lt;/a&gt; at &lt;a href=&quot;http://www.lrug.org/&quot;&gt;LRUG&lt;/a&gt; convinced me I had left it too long &amp;mdash; if I didn&apos;t start soon, I would be left behind.&lt;/p&gt;

&lt;p&gt;Here is how I got started with RabbitMQ 1.6.0 on OS X under Ruby 1.8.6.&lt;/p&gt;

&lt;h2&gt;Installation&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;mkdir /tmp/rabbit-mq &amp;amp;&amp;amp; cd /tmp/rabbit-mq
wget http://www.rabbitmq.com/releases/rabbitmq-server/v1.6.0/rabbitmq-server-generic-unix-1.6.0.tar.gz
tar -xzvf rabbitmq-server-generic-unix-1.6.0.tar.gz
sudo mv rabbitmq_server-1.6.0/ /opt/local/lib
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Running the Server&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;sudo /opt/local/lib/rabbitmq_server-1.6.0/sbin/rabbitmq-server
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Seriously, that is it.&lt;/p&gt;

&lt;h2&gt;Passing Messages&lt;/h2&gt;

&lt;p&gt;When I wrote about &lt;a href=&quot;http://barkingiguana.com/blog/2009/01/01/writing-rubystomp-clients-with-smqueue&quot;&gt;getting started with SMQueue&lt;/a&gt;, I created a producer that pushed timestamps onto a queue and a consumer that printed them to the terminal. Recreating that with the AMQP gem is straightforward.&lt;/p&gt;

&lt;p&gt;First, install the AMQP gem:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;gem sources -a http://gems.github.com
gem install tmm1-amqp
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Open an IRB session and paste this to create a producer:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;mq&apos;
EM.run {
  broker = MQ.new
  EM.add_periodic_timer(1) {
    broker.queue(&quot;timestamps&quot;).publish(Time.now.to_f)
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Open another IRB session and paste this to create a consumer:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;mq&apos;
EM.run {
  broker = MQ.new
  broker.queue(&quot;timestamps&quot;).subscribe { |timestamp|
    time = Time.at(timestamp.to_f)
    puts &quot;Got #{timestamp} which is #{time}&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That is all there is to it. RabbitMQ is &lt;em&gt;extremely&lt;/em&gt; easy to get started with. I suspect it would not take much effort to write an SMQueue adapter for it, letting deployed projects switch message brokers without changing their code. If you end up building one, I would love to hear about it.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Securing Passwords with Salt, Pepper, and Rainbows</title>
    <link href="/2009/08/03/securing-passwords-with-salt-pepper-and-rainbows/"/>
    <updated>2009-08-03T00:00:00+08:00</updated>
    <id>/2009/08/03/securing-passwords-with-salt-pepper-and-rainbows/</id>
    <content type="html">&lt;p&gt;You have heard &lt;a href=&quot;http://www.perlmonks.org/?node_id=784737&quot;&gt;again&lt;/a&gt; and &lt;a href=&quot;http://blog.moertel.com/articles/2006/12/15/never-store-passwords-in-a-database&quot;&gt;again&lt;/a&gt; that storing passwords in plain text is a bad idea. So now you store your passwords as MD5 or SHA1 hashes. If someone steals your password database, your users&apos; passwords are safe, right?&lt;/p&gt;

&lt;p&gt;Actually, no. They are never totally safe. You can, however, make the effort required to break into an individual account too large for all but the most dedicated attacker.&lt;/p&gt;

&lt;p&gt;Unfortunately, most web applications I get a chance to examine don&apos;t bother making their password storage more secure, which is a shame &amp;mdash; because it really is not that hard.&lt;/p&gt;

&lt;p&gt;For completeness, let&apos;s start from the bottom and work our way up.&lt;/p&gt;

&lt;h2&gt;Plain Text Passwords&lt;/h2&gt;

&lt;p&gt;Anathema to account security. If your password database is compromised, every account is wide open. Congratulations, you just handed over the details of your entire user base.&lt;/p&gt;

&lt;p&gt;It gets worse. Anyone listening to the traffic between your application and the database can pluck passwords right out of the air. Very few people secure their database connections with TLS or SSH. On an unswitched network, spying on something like MySQL traffic is as easy as running one command:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# tcpdump -l -i eth0 -w - src or dst port 3306 | strings&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Queries like &lt;code&gt;SELECT users.id FROM users WHERE password = &apos;foo&apos;&lt;/code&gt; or results from &lt;code&gt;SELECT users.* FROM users&lt;/code&gt; will show up in plain text, and you won&apos;t even know your passwords have been stolen.&lt;/p&gt;

&lt;p&gt;In a switched environment it is possible to &lt;a href=&quot;http://www.linuxjournal.com/article/5869&quot;&gt;trick the switch into sending you traffic&lt;/a&gt; (although this can be detectable). Depending on the hardware, a switch failure may cause it to fail open and behave like an unswitched network anyway.&lt;/p&gt;

&lt;h2&gt;Simple Hashed Passwords&lt;/h2&gt;

&lt;p&gt;Hashing is generally seen as the solution. No passwords are stored in plain text, and it is hard to guess a password that matches a given hash. Even if the database is compromised or snooped, you should be fine.&lt;/p&gt;

&lt;p&gt;That may have been true once, but hashes for many common words, passwords, and passphrases have already been calculated. Translating from those hashes back to a matching password is trivial. Remember: since the original password is not stored, all you need is any input that produces the same hash.&lt;/p&gt;

&lt;p&gt;How easy is it to crack an account protected by an MD5-hashed password? Say we attacked a site and found this table:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Username : Hashed Password
Alice    : a34bc26f864ed5f404eac5b7a20cd9aa
Bob      : 7a75a532aaab234ad4bd33ed67e67242
Malory   : 39579c8d4a536eb092f959b4a3d14aa8
Zebedee  : 57208d910b63e879d2bae3b3a5f8366d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Take each hashed password and look it up in a &lt;a href=&quot;http://en.wikipedia.org/wiki/Rainbow_Table&quot;&gt;rainbow table&lt;/a&gt; for the appropriate hash algorithm. Given that these are 32 hex characters, they are almost certainly MD5. Using something like &lt;a href=&quot;http://gdataonline.com/seekhash.php&quot;&gt;GData&lt;/a&gt; to search an MD5 rainbow table gives us:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Username : Password
Alice    : alphabets
Bob      : ch1cken
Malory   : blue41
Zebedee  : ?????
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only Zebedee is safe, and that is only for two reasons: (1) he is a freaky little spring creature with a magnificent moustache who can do magic things, and (2) nobody has added his password &amp;mdash; or a collision for it &amp;mdash; to the rainbow table &lt;a href=&quot;http://gdataonline.com/addhash.php&quot;&gt;yet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Rainbow tables exist for several hashing algorithms including MD5 and SHA1. If the hash is not in the table, causing a collision for a specific account would cost around USD$2,000 and take &lt;a href=&quot;http://www.speedguide.net/read_news.php?id=2752&quot;&gt;about a day&lt;/a&gt; for MD5.&lt;/p&gt;

&lt;h2&gt;Multiply Hashed Passwords&lt;/h2&gt;

&lt;p&gt;Rainbow tables take a long time to populate, and that time can be made longer by running the hash function multiple times before storing the result:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;MD5(alphabets)                        = a34bc26f864ed5f404eac5b7a20cd9aa
MD5(a34bc26f864ed5f404eac5b7a20cd9aa) = dd3f1bf5a36529705d08fe50b966d41a
MD5(...)                              = ...
MD5(...)                              = b5fdbbd055fcbfd3958a28f15661aea0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Each iteration takes CPU time, so generating a rainbow table for these hashes costs more. But CPU time is cheap these days. The advantage is that the attacker doesn&apos;t know how many times you applied the hash function unless they also have your code. Unfortunately, they can brute-force that number by starting at the hash and working backwards through generated rainbow tables until they find a value that logs into the site. Once that magic number is established, you have just a few days before the rest of your accounts are compromised.&lt;/p&gt;

&lt;h2&gt;Peppered Hashes&lt;/h2&gt;

&lt;p&gt;Rainbow tables can be generated reasonably fast, and while they are not trivially cheap, they are no longer prohibitively expensive either. How do we make rainbow tables a less viable attack vector?&lt;/p&gt;

&lt;p&gt;Rainbow tables are simply maps from hashes to the inputs that generate them. If we require that every password includes a bit of extra data &amp;mdash; a piece of spice, let&apos;s call it &lt;em&gt;pepper&lt;/em&gt; &amp;mdash; that we define in our application, then existing rainbow tables become useless. An attacker would have to generate entirely new tables where every input includes the pepper.&lt;/p&gt;

&lt;p&gt;The pepper lives in your application code and never reaches the database except as part of a hash. In this way it behaves much like the magic number in the multiply-hashed approach. And like that approach, once the pepper is discovered it can be used against all accounts.&lt;/p&gt;

&lt;p&gt;Someone could &amp;mdash; and if they are determined enough, will &amp;mdash; calculate a new rainbow table given time. But if you pick a strong, unique pepper, at least there is no off-the-shelf table that works.&lt;/p&gt;

&lt;h2&gt;Spicy Hashes&lt;/h2&gt;

&lt;p&gt;By combining the pepper with multiple rounds of hashing, we force the attacker to guess two things: the number of iterations &lt;em&gt;and&lt;/em&gt; the pepper.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pepper = ...aliesc3ifCTAasd4$af...
MD5(pepper + password)    = ...b5f34...
MD5(pepper + ...b5f34...) = ...ea28c...
MD5(pepper + ...ea28c...) = ...

SELECT users.id FROM users WHERE hashed_password = ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I am not entirely sure this buys much over just applying the hash function many times, but it sure looks pretty &amp;mdash; and I really wanted an excuse to make a pun about using lots of pepper to make hashes spicy. Sorry.&lt;/p&gt;

&lt;h2&gt;Salted Hashes&lt;/h2&gt;

&lt;p&gt;The pepper and the multiply-hashed approaches share a weakness: they use a single value for the entire database. What if there were a different value for each account? A small, unique-per-account value mixed in the same way as the pepper &amp;mdash; a &lt;em&gt;salt&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With a per-account salt, a rainbow table generated to crack one account is useless for cracking the next.&lt;/p&gt;

&lt;p&gt;Where do we store the salt? I quite like tucking it into the first few characters of the hashed password field, though you might prefer a separate column. Yes, the salt lives right there in the password database. Sounds like it would make cracking easier, right? Not really. All the salt tells an attacker is that the password is somehow combined with this value to produce the hash. The &lt;em&gt;how&lt;/em&gt; is still hidden in your application code, and a valid password is still several iterations of rainbow table generation away &amp;mdash; for &lt;em&gt;each individual account&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;Safe Now?&lt;/h2&gt;

&lt;p&gt;Not even close. With a solid combination of the above &amp;mdash; strong salts, a good pepper, and a decent number of hashing rounds &amp;mdash; you have made it unlikely that someone who steals your password database can use it to access accounts. But that doesn&apos;t mean your users will pick sane passwords, that your system is bug-free, or that there aren&apos;t other ways to find those passwords.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Running Starling under DaemonTools</title>
    <link href="/2009/05/13/running-starling-under-daemontools/"/>
    <updated>2009-05-13T00:00:00+08:00</updated>
    <id>/2009/05/13/running-starling-under-daemontools/</id>
    <content type="html">&lt;p&gt;I have been playing with Starling quite a bit recently. Like most of my deployed tools, I want to be confident it stays running. Here is a run script for Starling under DaemonTools:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;#!/bin/sh
# This is /home/starling/service/run

exec 2&amp;gt;&amp;amp;1

echo &quot;Starting...&quot;

PORT=22122
IP=0.0.0.0
USER=starling
HOME=/home/starling

exec setuidgid $USER \
     starling -v -v -v -h $IP -p $PORT -P $HOME/starling.pid -q $HOME/queue 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You will want to keep the logs too. Here is the log/run script:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;#!/bin/sh
# This is /home/starling/service/log/run

exec multilog t s1000000 n10 ./main&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that you will need to create the &lt;code&gt;starling&lt;/code&gt; user before using these scripts, or just update them to use an existing user.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>A Starling Adapter for SMQueue</title>
    <link href="/2009/05/08/a-starling-adapter-for-smqueue/"/>
    <updated>2009-05-08T00:00:00+08:00</updated>
    <id>/2009/05/08/a-starling-adapter-for-smqueue/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://github.com/starling/starling&quot;&gt;Starling&lt;/a&gt; is a persistent, lightweight work queue implemented in Ruby that speaks the memcache protocol. I have been playing with it recently because I don&apos;t have the resources to look after &amp;mdash; or the requirement for &amp;mdash; a full-blown service bus. Starling is easier to install and configure than ActiveMQ, though nowhere near as fully featured. Both have their place, but comparing them is outside the scope of this article.&lt;/p&gt;

&lt;p&gt;I knew I wanted a message bus to turn synchronous requests into asynchronous ones, pushing work off to background processes. What I didn&apos;t know was which message bus I would end up using. If you are familiar with the Gang of Four patterns book you have probably already spotted the relevant pattern here. &lt;a href=&quot;http://github.com/seanohalpin/smqueue&quot;&gt;SMQueue&lt;/a&gt;, which I am &lt;a href=&quot;http://barkingiguana.com/2009/01/01/writing-rubystomp-clients-with-smqueue&quot;&gt;familiar with&lt;/a&gt;, provides a clean abstraction that makes it easy to swap out the message bus implementation while keeping your code identical. The catch: SMQueue didn&apos;t ship with an adapter for Starling.&lt;/p&gt;

&lt;p&gt;&quot;How hard,&quot; I thought, &quot;would it be to write one?&quot;&lt;/p&gt;

&lt;p&gt;I blinked and suddenly it existed.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;rubygems&apos;
require &apos;smqueue&apos;
require &apos;starling&apos;
require &apos;yaml&apos;

module BarkingIguana
  module Messaging
    module SMQueue
      class StarlingAdapter &amp;lt; ::SMQueue::Adapter
        class Configuration &amp;lt; ::SMQueue::AdapterConfiguration
          DEFAULT_SERVER = &apos;127.0.0.1:22122&apos;

          has :queue
          has :server, :default =&amp;gt; DEFAULT_SERVER
        end

        def initialize(*args)
          super
          options = args.first
          @configuration = options[:configuration]
          @configuration[:server] ||= Configuration::DEFAULT_SERVER

          @client = ::Starling.new(@configuration[:server])
        end

        def put(*args, &amp;amp;block)
          @client.set @configuration[:queue], args[0].to_yaml
        end

        def get(*args, &amp;amp;block)
          if block_given?
            loop do
              yield next_message
            end
          else
            next_message
          end
        end

        private
        def next_message
          ::SMQueue::Message(:headers =&amp;gt; {},
            :body =&amp;gt; YAML.load(@client.get(@configuration[:queue])))
        end
      end
    end
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Want to use it? You will need Starling running somewhere. After that, a producer is just two lines of code:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;producer = SMQueue(:adapter =&amp;gt; BarkingIguana::Messaging::SMQueue::StarlingAdapter, :queue =&amp;gt; &quot;some.queue.name&quot;)
producer.put &quot;Quack quack&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And here is a consumer on the other side of the connection:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;consumer = SMQueue(:adapter =&amp;gt; BarkingIguana::Messaging::SMQueue::StarlingAdapter, :queue =&amp;gt; &quot;some.queue.name&quot;)
consumer.get do |message|
  puts message.body.inspect
  # =&amp;gt; &quot;Quack quack&quot;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One thing worth noting: this adapter assumes YAML as the transport format. I would prefer JSON or XML, but YAML was the easiest to implement and I am not above taking the lazy path when it gets the job done.&lt;/p&gt;

&lt;p&gt;There is also work to be done around failover &amp;mdash; this adapter only supports a single server. I don&apos;t yet know enough about how Starling handles failover, and I would rather not rush into an implementation that turns out to be wrong.&lt;/p&gt;

&lt;p&gt;If you can help with patches for other transport formats or failover support, please do.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Expanding Shortened URLs in a Ruby String</title>
    <link href="/2009/05/07/expanding-shortened-urls-in-a-ruby-string/"/>
    <updated>2009-05-07T00:00:00+08:00</updated>
    <id>/2009/05/07/expanding-shortened-urls-in-a-ruby-string/</id>
    <content type="html">&lt;p&gt;Everyone and their dog uses some sort of URL shortening service these days. While it&apos;s handy for cramming a link into short messages like those on &lt;a href=&quot;http://twitter.com/&quot;&gt;Twitter&lt;/a&gt;, it&apos;s not always considered best practice for &lt;a href=&quot;http://joshua.schachter.org/2009/04/on-url-shorteners.html&quot;&gt;a bunch of reasons&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since plenty of applications pull content from Twitter feeds and similar services, it would be great to expand those shortened URLs and undo the damage. So I built a little module that does exactly that.&lt;/p&gt;

&lt;p&gt;Borrowing heavily from &lt;a href=&quot;http://github.com/rust/termtter/blob/e969e6fde8c056dcdc9a7f8dd06e002b1c802948/lib/plugins/expand-tinyurl.rb&quot;&gt;a Ruby-based Twitter client&lt;/a&gt;, I extracted a module you can mix into &lt;code class=&quot;ruby&quot;&gt;String&lt;/code&gt;. The idea is simple: for each known shortening service, follow the redirect and swap in the real URL.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;net/http&apos;

module BarkingIguana
  module ExpandUrl
    def expand_urls!
      ExpandUrl.services.each do |service|
        gsub!(service[:pattern]) { |match|
          ExpandUrl.expand($2, service[:host]) || $1
        }
      end
    end

    def expand_urls
      s = dup
      s.expand_urls!
      s
    end

    def ExpandUrl.services
      [
        { :host =&amp;gt; &quot;tinyurl.com&quot;, :pattern =&amp;gt; %r&apos;(http://tinyurl\.com(/[\w/]+))&apos; },
        { :host =&amp;gt; &quot;is.gd&quot;, :pattern =&amp;gt; %r&apos;(http://is\.gd(/[\w/]+))&apos; },
        { :host =&amp;gt; &quot;bit.ly&quot;, :pattern =&amp;gt; %r&apos;(http://bit\.ly(/[\w/]+))&apos; },
        { :host =&amp;gt; &quot;ff.im&quot;, :pattern =&amp;gt; %r&apos;(http://ff\.im(/[\w/]+))&apos;},
      ]
    end

    def ExpandUrl.expand(path, host)
      result = ::Net::HTTP.new(host).head(path)
      case result
      when ::Net::HTTPRedirection
        result[&apos;Location&apos;]
      end
    end
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To use it, include the module into &lt;code class=&quot;ruby&quot;&gt;String&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class String
  include BarkingIguana::ExpandUrl
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then call &lt;code class=&quot;ruby&quot;&gt;expand_urls&lt;/code&gt; or &lt;code class=&quot;ruby&quot;&gt;expand_urls!&lt;/code&gt; on any text containing shortened URLs. The bang method modifies the string in place; the regular method returns a new string and leaves the original untouched.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;s = &quot;http://tinyurl.com/asdf&quot;
s.expand_urls!
puts s.inspect
# =&amp;gt; &quot;http://support.microsoft.com/default.aspx?scid=kb;EN-US;158122&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It currently supports ff.im, is.gd, bit.ly, and tinyurl. If you know of other services that should be included, I would love to hear about them. This code &amp;mdash; like the original implementation &amp;mdash; is released under the &lt;a href=&quot;http://www.opensource.org/licenses/mit-license.php&quot;&gt;MIT licence&lt;/a&gt;. The full code including licence and RDoc can be found at &lt;a href=&quot;http://pastie.org/471016&quot;&gt;http://pastie.org/471016&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Aspell for Ruby with MacPorts-Installed Aspell</title>
    <link href="/2009/04/03/aspell-for-ruby-with-macports-installed-aspell/"/>
    <updated>2009-04-03T00:00:00+08:00</updated>
    <id>/2009/04/03/aspell-for-ruby-with-macports-installed-aspell/</id>
    <content type="html">&lt;p&gt;If you want to use &lt;a href=&quot;http://blog.evanweaver.com/articles/2007/03/10/add-gud-spelning-to-ur-railz-app-or-wharever/&quot;&gt;Aspell from Ruby&lt;/a&gt; and you use MacPorts to manage software on your Mac, you&apos;ll likely hit a wall compiling the native extensions for RAspell. The error log is lengthy, but the important line is this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;raspell.h:6:20: error: aspell.h: No such file or directory&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It can&apos;t find the Aspell headers, even though Aspell is installed via MacPorts. The fix is simple: tell RubyGems where MacPorts put everything.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# Install the Aspell port
sudo port install aspell
# Install the Ruby bindings, pointing at MacPorts&apos; install location
sudo gem install raspell, --with-opt-dir=/opt/local&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&apos;s it. The &lt;code&gt;--with-opt-dir=/opt/local&lt;/code&gt; flag tells the native extension builder to look in MacPorts&apos; prefix for headers and libraries, and everything compiles cleanly.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Posting to IRC Using ActiveMQ</title>
    <link href="/2009/03/06/posting-to-irc-using-activemq/"/>
    <updated>2009-03-06T00:00:00+09:00</updated>
    <id>/2009/03/06/posting-to-irc-using-activemq/</id>
    <content type="html">&lt;p&gt;Previously I wrote about &lt;a href=&quot;http://barkingiguana.com/2009/03/02/query-your-applications-using-irc&quot;&gt;querying your app using IRC and IRCCat&lt;/a&gt;. But that&apos;s only half the story. IRCCat can also let your applications talk &lt;em&gt;to&lt;/em&gt; you. A source code commit, a user logging in, a server going down &amp;mdash; these are all things worth knowing about, and they&apos;re surprisingly easy to pipe into IRC.&lt;/p&gt;

&lt;p&gt;The IRCCat examples typically use netcat to send data over the network to the IRCCat process. I prefer a small Ruby script backed by a message bus. Since I already have ActiveMQ running, there&apos;s very little extra overhead:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;#! /usr/bin/env ruby

STDOUT.sync = true

require &apos;rubygems&apos;
require &apos;smqueue&apos;
require &apos;yaml&apos;
require &apos;socket&apos;

puts &quot;Starting...&quot;

messages = SMQueue(:name =&amp;gt; &quot;/queue/irc.outgoing&quot;, :host =&amp;gt; &quot;mq.domain.com&quot;, :reliable =&amp;gt; true, :adapter =&amp;gt; &quot;StompAdapter&quot;)

messages.get do |job|
  message = YAML.parse(job.body).transform
  puts &quot;Posting #{message[&apos;text&apos;]} in #{message.headers[&apos;message-id&apos;]}.&quot;
  irc = TCPSocket.open(&apos;localhost&apos;, &apos;12345&apos;)
  irc.send(&quot;#{message[&apos;text&apos;]}\r\n&quot;, 0)
  irc.close
  puts &quot;Posted #{message.headers[&apos;message-id&apos;]}.&quot;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With this running on the same box as IRCCat, any other process can drop a message onto the &lt;code&gt;/queue/irc.outgoing&lt;/code&gt; queue and it will appear in IRC. If IRCCat happens to be down, the messages sit safely in the queue until it comes back up.&lt;/p&gt;

&lt;p&gt;I like this approach because the various processes that generate notifications don&apos;t need to know anything about where IRCCat is running. They just talk to the message queue, which SMQueue makes painless.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Memcache Statistics from the Command Line</title>
    <link href="/2009/03/04/memcache-statistics-from-the-command-line/"/>
    <updated>2009-03-04T00:00:00+09:00</updated>
    <id>/2009/03/04/memcache-statistics-from-the-command-line/</id>
    <content type="html">&lt;p&gt;When debugging memcache issues, being able to see the output of the &lt;code&gt;stats&lt;/code&gt; command is invaluable. I got tired of manually connecting via telnet every time, so I wrote this little Ruby script to pull the statistics cleanly:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;#! /usr/bin/env ruby

require &apos;socket&apos;

socket = TCPSocket.open(&apos;localhost&apos;, &apos;11211&apos;)
socket.send(&quot;stats\r\n&quot;, 0)

statistics = []
loop do
  data = socket.recv(4096)
  if !data || data.length == 0
    break
  end
  statistics &amp;lt;&amp;lt; data
  if statistics.join.split(/\n/)[-1] =~ /END/
    break
  end
end

puts statistics.join()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It opens a raw TCP connection to the memcache daemon, sends &lt;code&gt;stats&lt;/code&gt;, reads until it sees the &lt;code&gt;END&lt;/code&gt; marker, and prints the result. Quick, simple, and saves you from typing &lt;code&gt;telnet localhost 11211&lt;/code&gt; for the hundredth time.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Query Your Applications Using IRC</title>
    <link href="/2009/03/02/query-your-applications-using-irc/"/>
    <updated>2009-03-02T00:00:00+09:00</updated>
    <id>/2009/03/02/query-your-applications-using-irc/</id>
    <content type="html">&lt;p&gt;IRC &amp;mdash; most of you know what it is. For those who don&apos;t, it stands for &lt;a href=&quot;http://en.wikipedia.org/wiki/Internet_Relay_Chat&quot;&gt;Internet Relay Chat&lt;/a&gt;. Think of it as a geeky group chat and you won&apos;t be far off.&lt;/p&gt;

&lt;p&gt;There&apos;s a long tradition of using bots &amp;mdash; automated processes &amp;mdash; to provide services in IRC channels. Bots that help people share code through &lt;a href=&quot;http://pastie.org/&quot;&gt;Paste Bin&lt;/a&gt; services, bots that take messages for offline users and replay them later. They&apos;re genuinely useful because they enhance a communication medium that people are already using, without requiring any extra software on the client side.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;http://last.fm/&quot;&gt;Last.fm&lt;/a&gt; use IRC as an internal communication tool. They&apos;ve written (and released under the GPL &amp;mdash; thanks!) IRCCat, which makes it straightforward to build bots that answer queries or perform commands right from IRC channels.&lt;/p&gt;

&lt;p&gt;I&apos;ve set up IRCCat and written a few scripts for it. Getting started is pretty easy. You&apos;ll need Java and Ant installed. I&apos;m on a Mac with OS X 10.4, so Java is already there, and MacPorts provides an Ant port.&lt;/p&gt;

&lt;p&gt;With Java and Ant ready, clone the IRCCat source from GitHub:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;git clone git://github.com/RJ/irccat.git&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Compile and package the bot by running &lt;code class=&quot;bash&quot;&gt;ant dist&lt;/code&gt; in the cloned directory.&lt;/p&gt;

&lt;p&gt;Once it&apos;s packaged, create a &lt;code&gt;config/&lt;/code&gt; directory and copy the example configuration from &lt;code&gt;examples/irccat.xml&lt;/code&gt; into it. This is where you tell the bot how to behave.&lt;/p&gt;

&lt;p&gt;The config file is reasonably well commented. Walk through each section and fill in the details:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Provide your IRC server connection details. I use an internal server, but if you don&apos;t have one, there are plenty of public IRC networks a quick search away.&lt;/li&gt;
  &lt;li&gt;Set the bot&apos;s username.&lt;/li&gt;
  &lt;li&gt;Change the external scripts handler to &lt;code&gt;scripts/run&lt;/code&gt; and bump the max response lines to 30.&lt;/li&gt;
  &lt;li&gt;Choose which channels the bot should join. If they don&apos;t exist, they&apos;ll be created when the bot joins (depending on network policy).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the configuration done, launch the bot:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;ant -Dconfgfile=./config/irccat.xml&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you&apos;re in one of the channels you told it to join, you should see it appear. Verify it&apos;s working by typing &lt;code&gt;!channels&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CraigW: !channels
bot: I am in 2 channels: #foo #bar&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There are a few built-in commands, all prefixed with an exclamation mark:&lt;/p&gt;

&lt;table&gt;
  &lt;tr&gt;
    &lt;th&gt;Command&lt;/th&gt;
    &lt;th&gt;Description&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;!join #channel password&lt;/td&gt;
    &lt;td&gt;Make the bot join a channel (password is optional)&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;!part #channel&lt;/td&gt;
    &lt;td&gt;Make the bot leave a channel&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;!channels&lt;/td&gt;
    &lt;td&gt;List all channels the bot is in&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;!spam message&lt;/td&gt;
    &lt;td&gt;Send a message to all channels&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;!exit&lt;/td&gt;
    &lt;td&gt;Shut down the bot&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;The really interesting part is external commands, triggered with a question mark prefix. You write these yourself, and they can do anything you want.&lt;/p&gt;

&lt;p&gt;Remember the &lt;code&gt;cmdhandler&lt;/code&gt; config value I set to &lt;code&gt;scripts/run&lt;/code&gt;? That&apos;s the entry point for externals. I use it to launch a router that loads and executes other command scripts from the &lt;code&gt;scripts/&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;scripts/run&lt;/code&gt; looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;#!/bin/bash
# This script handles ?commands to irccat

exec ruby ./scripts/router &quot;$@&quot; 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Make that executable (&lt;code&gt;chmod +x scripts/run&lt;/code&gt;). The &lt;code&gt;scripts/router&lt;/code&gt; handles the dispatch:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;#! /usr/bin/env ruby

COMMANDS = File.expand_path(File.dirname(__FILE__))
name, channel, username, command, arguments = *ARGV[0].split(/ /, 5)

command_script = File.join(COMMANDS, File.basename(command))

if File.exists?(command_script) &amp;amp;&amp;amp; !%W(run router).include?(command)
  load command_script
  puts Command.execute(name, channel, username, arguments).strip
else
  desired_command = &quot;#{command} #{arguments}&quot;.strip
  puts &quot;Sorry #{name}, I don&apos;t understand `#{desired_command}`.&quot;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Writing a new command is now just a matter of creating a script that implements a &lt;code class=&quot;ruby&quot;&gt;Command&lt;/code&gt; class. The filename determines what you type in IRC. Want to query SNMP on a host? You&apos;d type something like &lt;code&gt;?snmp xeriom-vm-host-06 .1.3.6.1.2.1.1.1&lt;/code&gt;, so the script goes in &lt;code&gt;scripts/snmp&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class Command
  class &amp;lt;&amp;lt; self
    def execute(name, channel, username, arguments)
      hostname, oid, remainder = arguments.split(/ /, 3)
      `snmpwalk -c public -v 1 #{hostname}.core.xeriom.net #{oid}`
    end
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Type the command in IRC and the results come straight back. No bot restart needed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CraigW: ?snmp xeriom-vm-host-06 .1.3.6.1.2.1.1.1
bot: SNMPv2-MIB::sysDescr.0 = STRING: Linux xeriom-vm-host-06.core.xeriom.net 2.6.24-17-xen #1 SMP Thu May 1 15:55:31 UTC 2008 x86_64&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There&apos;s real power in having this kind of access right inside your team&apos;s communication channel. A quick command can pull up customer records, server stats, or application data without anyone needing to drop to a terminal or load a web page.&lt;/p&gt;

&lt;h4&gt;Ruby vs Java&lt;/h4&gt;

&lt;p&gt;I&apos;ve since discovered a &lt;a href=&quot;http://github.com/webs/irccat/tree/master&quot;&gt;Ruby port of IRCCat&lt;/a&gt;. I&apos;ll be switching to that &amp;mdash; I find Ruby projects easier to maintain and fork than Java ones. Your mileage may vary.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Running Mongrel under DaemonTools</title>
    <link href="/2009/02/27/running-mongrel-under-daemontools/"/>
    <updated>2009-02-27T00:00:00+09:00</updated>
    <id>/2009/02/27/running-mongrel-under-daemontools/</id>
    <content type="html">&lt;p&gt;I use &lt;a href=&quot;http://barkingiguana.com/2008/11/28/running-daemontools-under-ubuntu-810&quot;&gt;DaemonTools&lt;/a&gt; to keep my services running and behaving themselves. Since I run plenty of Rails applications, here&apos;s the DaemonTools run script I use to keep Mongrel humming along:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;bash&quot;&gt;#!/bin/sh
exec 2&amp;gt;&amp;amp;1

echo &quot;Starting...&quot;

ENVIRONMENT=production
PORT=8000
IP=0.0.0.0

CHDIR=/var/www/www.application.com
USER=application_user

exec softlimit -m 134217728 \
     setuidgid $USER \
     env HOME=$CHDIR \
     mongrel_rails start -e $ENVIRONMENT -p $PORT -a $IP -c $CHDIR 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;softlimit&lt;/code&gt; caps memory usage at 128MB, &lt;code&gt;setuidgid&lt;/code&gt; drops privileges to the application user, and the rest is standard Mongrel configuration. Create a separate DaemonTools service for each Mongrel instance you want to run and just change the &lt;code&gt;PORT&lt;/code&gt; variable in each script.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Finding and Enumerating Document Attributes with ActiveCouch</title>
    <link href="/2009/02/03/finding-and-enumerating-document-attributes-with-activecouch/"/>
    <updated>2009-02-03T00:00:00+09:00</updated>
    <id>/2009/02/03/finding-and-enumerating-document-attributes-with-activecouch/</id>
    <content type="html">&lt;p&gt;Following on from my exploration of &lt;a href=&quot;http://barkingiguana.com/2009/01/28/counting-tags-with-couchdb-and-map-reduce&quot;&gt;counting tags with CouchDB and map-reduce&lt;/a&gt;, I&apos;ve added support to ActiveCouch for counting all uses of an attribute across a document type in your database. As a bonus, you can also retrieve all unique values for any attribute.&lt;/p&gt;

&lt;p&gt;The API is straightforward. Call &lt;code class=&quot;ruby&quot;&gt;enumerate_all_[attribute_name]&lt;/code&gt; to get a hash of values and their counts, or &lt;code class=&quot;ruby&quot;&gt;find_all_[attribute_name]&lt;/code&gt; to get just the unique values:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;&amp;gt;&amp;gt; Article.enumerate_all_tags
=&amp;gt; {&quot;security&quot;=&amp;gt;2, &quot;ldap&quot;=&amp;gt;1, &quot;xen&quot;=&amp;gt;1, &quot;stories&quot;=&amp;gt;3, &quot;rails&quot;=&amp;gt;13, &quot;xeriom&quot;=&amp;gt;3, &quot;mysql&quot;=&amp;gt;3, ... }

&amp;gt;&amp;gt; Article.find_all_tags
=&amp;gt; [&quot;agile&quot;, &quot;ajax&quot;, &quot;apache&quot;, &quot;api&quot;, &quot;caching&quot;, &quot;coding&quot;, ... ]

&amp;gt;&amp;gt; Article.find_all_author_ids
=&amp;gt; [&quot;craig@barkingiguana.com&quot;]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Under the hood, this builds the appropriate map-reduce views automatically. If you&apos;re curious about the implementation details, have a look at commit &lt;code&gt;1cbbe71&lt;/code&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Conditions and Ordering with ActiveCouch Views</title>
    <link href="/2009/01/31/conditions-and-ordering-with-activecouch-views/"/>
    <updated>2009-01-31T00:00:00+09:00</updated>
    <id>/2009/01/31/conditions-and-ordering-with-activecouch-views/</id>
    <content type="html">&lt;p&gt;When I posted about my &lt;a href=&quot;http://barkingiguana.com/2009/01/08/breaking-activecouch-in-fun-and-inventive-ways&quot;&gt;hacking on ActiveCouch&lt;/a&gt;, I mentioned it didn&apos;t yet support ordering. Well, since commit &lt;code&gt;87120176&lt;/code&gt;, it does. It&apos;s not as fine-grained as ActiveRecord yet, but it handles what I need: setting conditions on the finder and getting results ordered by &lt;code&gt;posted_at&lt;/code&gt; date and then &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When I say &quot;not as fine-grained,&quot; I mean ActiveRecord can effortlessly build queries like &lt;code class=&quot;sql&quot;&gt;ORDER BY posted_at ASC, id DESC, created_at DESC, author ASC&lt;/code&gt;. ActiveCouch can only order view results by key &amp;mdash; either ascending or descending. I don&apos;t think that&apos;s an insurmountable limitation; I just haven&apos;t needed more control yet.&lt;/p&gt;

&lt;p&gt;So how does it work?&lt;/p&gt;

&lt;p&gt;When you want to find by conditions but don&apos;t particularly care about the order, ActiveCouch creates a view that emits keys based on just those conditions. Say you want all articles by &quot;craig@barkingiguana.com&quot; with a &quot;Live&quot; status:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;Article.find(:all, :conditions =&amp;gt; { :author_id =&amp;gt; &quot;craig@barkingiguana.com&quot;, :status =&amp;gt; &quot;Live&quot; })&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first time this runs, ActiveCouch creates a view called &lt;code&gt;by_author_id_and_status&lt;/code&gt; in the &lt;code&gt;articles&lt;/code&gt; design document. The view emits a key built from those two attributes, along with the full document as the value:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;{
  &quot;_id&quot;: &quot;_design/articles&quot;,
  &quot;_rev&quot;: &quot;1532981864&quot;,
  &quot;language&quot;: &quot;javascript&quot;,
  &quot;views&quot;: {
    &quot;by_author_id_and_status&quot;: {
      &quot;map&quot;: &quot;function(doc) { if(doc.type == &apos;article&apos;) { emit([doc.author_id, doc.status], doc); }  }&quot;
    }
    // other views cut for brevity
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The query then hits this view asking for the key &lt;code class=&quot;javascript&quot;&gt;[&quot;craig@barkingiguana.com&quot;, &quot;Live&quot;]&lt;/code&gt;, which matches exactly the documents we&apos;re after.&lt;/p&gt;

&lt;p&gt;When you add an order, things get a bit more interesting. Since these are articles and probably time-sensitive, let&apos;s order by &lt;code&gt;posted_at&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;Article.find(:all, :conditions =&amp;gt; { :author_id =&amp;gt; &quot;craig@barkingiguana.com&quot;, :status =&amp;gt; &quot;Live&quot; }, :order =&amp;gt; :posted_at)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This time, ActiveCouch creates a view whose key also includes the &lt;code&gt;posted_at&lt;/code&gt; attribute, named &lt;code&gt;by_author_id_and_status_and_posted_at&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;{
  &quot;_id&quot;: &quot;_design/articles&quot;,
  &quot;_rev&quot;: &quot;3752119467&quot;,
  &quot;language&quot;: &quot;javascript&quot;,
  &quot;views&quot;: {
    &quot;by_author_id_and_status_and_posted_at&quot;: {
      &quot;map&quot;: &quot;function(doc) { if(doc.type == &apos;article&apos;) { emit([doc.author_id, doc.status, doc.posted_at], doc); }  }&quot;
    }
    // other views omitted for brevity
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When the query runs, it takes advantage of CouchDB&apos;s &lt;a href=&quot;http://wiki.apache.org/couchdb/View_collation&quot;&gt;view collation specification&lt;/a&gt; by requesting keys in a calculated range. For the example above, it asks for keys between &lt;code class=&quot;javascript&quot;&gt;[&quot;craig@barkingiguana.com&quot;, &quot;Live&quot;]&lt;/code&gt; and &lt;code class=&quot;javascript&quot;&gt;[&quot;craig@barkingiguana.com&quot;, &quot;Live&quot;, &quot;\u9999&quot;]&lt;/code&gt; (that&apos;s a very high-value Unicode character, as recommended in the collation spec).&lt;/p&gt;

&lt;p&gt;Since CouchDB view results are &lt;a href=&quot;http://barkingiguana.com/2009/01/22/filtering-and-ordering-couchdb-view-results&quot;&gt;ordered by key&lt;/a&gt;, and the key now contains the attribute we want to sort by, and our key range captures exactly the conditions we&apos;re filtering on &amp;mdash; we get sorted, filtered results in one clean query.&lt;/p&gt;

&lt;p&gt;The good news is that since I&apos;ve already done this work, you don&apos;t need to think about the internals. Grab the code with git: &lt;code&gt;git clone http://barkingiguana.com/~craig/code/activecouch.git&lt;/code&gt;. There&apos;s a getting-started guide in my &lt;a href=&quot;http://barkingiguana.com/2009/01/08/breaking-activecouch-in-fun-and-inventive-ways&quot;&gt;previous post on ActiveCouch&lt;/a&gt;. Give it a spin, and please let me know if you end up using it!&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Counting Tags with CouchDB and Map-Reduce</title>
    <link href="/2009/01/28/counting-tags-with-couchdb-and-map-reduce/"/>
    <updated>2009-01-28T00:00:00+09:00</updated>
    <id>/2009/01/28/counting-tags-with-couchdb-and-map-reduce/</id>
    <content type="html">&lt;p&gt;My previous post covered &lt;a href=&quot;http://barkingiguana.com/2009/01/20/adding-a-simple-view-to-couchdb&quot;&gt;adding a simple view to CouchDB&lt;/a&gt;, but what happens when a plain map isn&apos;t enough? Say we want a list of every tag used across all articles, along with a count of how many articles use each one. Sure, we could emit &lt;code class=&quot;javascript&quot;&gt;doc.tags&lt;/code&gt; and crunch the arrays on the client side, but wouldn&apos;t it be nicer if CouchDB did the heavy lifting for us?&lt;/p&gt;

&lt;p&gt;Good news: it can.&lt;/p&gt;

&lt;p&gt;Here&apos;s a reminder of what the article documents look like:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;{
  &quot;_id&quot;: &quot;monkeys-are-awesome&quot;,
  &quot;_rev&quot;: &quot;1534115156&quot;,
  &quot;type&quot;: &quot;article&quot;,
  &quot;title&quot;: &quot;Monkeys are awesome&quot;,
  &quot;posted_at&quot;: &quot;2008-09-14T20:45:14Z&quot;,
  &quot;tags&quot;: [
    &quot;monkeys&quot;,
    &quot;awesome&quot;
  ],
  &quot;status&quot;: &quot;Live&quot;,
  &quot;author_id&quot;: &quot;craig@barkingiguana.com&quot;,
  &quot;updated_at&quot;: &quot;2008-09-14T21:23:59Z&quot;,
  &quot;body&quot;: &quot;The article body would go here...&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;First, we write a map function that emits each tag individually with a value of &lt;code&gt;1&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;function(doc) {
  if(doc.type == &apos;article&apos;) {
    for(i in doc.tags) {
      emit(doc.tags[i], 1);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For the example document above, this would emit &lt;code&gt;(&quot;awesome&quot;, 1)&lt;/code&gt; and &lt;code&gt;(&quot;monkeys&quot;, 1)&lt;/code&gt;. If several documents are tagged &quot;monkeys&quot;, we&apos;d see &lt;code&gt;(&quot;monkeys&quot;, 1)&lt;/code&gt; appear multiple times in the output.&lt;/p&gt;

&lt;p&gt;Now we need to &lt;strong&gt;reduce&lt;/strong&gt; those results down to a list of unique tags with their totals. The reduce function gets called once per unique key, receiving that key and an array of all the values that were emitted for it. Since our values are all &lt;code&gt;1&lt;/code&gt;s, we just sum them up:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;function(tag, counts) {
  var sum = 0;
  for(var i=0; i &amp;lt; counts.length; i++) {
     sum += counts[i];
  }
  return sum;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Install this alongside the map function using the &lt;code&gt;&quot;reduce&quot;&lt;/code&gt; key in the design document:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;{
  &quot;tags&quot;: {
    &quot;map&quot;: &quot;function(doc) { if(doc.type == &apos;article&apos;) { for(var i in doc.tags) { emit(doc.tags[i], 1); }}}&quot;,
    &quot;reduce&quot;: &quot;function(tag, counts) { var sum = 0; for(var i = 0; i &amp;lt; counts.length; i++) { sum += counts[i]; }; return sum; }&quot;
  }
  // other views omitted for brevity
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Viewing this in Futon gives you a nicely formatted list of tags and counts. To use the view via the HTTP API, you need to tell CouchDB to group results by key:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;// GET http://localhost:5984/blog/_view/articles/tags?group=true&amp;amp;group_level=1

{&quot;rows&quot;:[
  {&quot;key&quot;:&quot;awesome&quot;,&quot;value&quot;:1},
  {&quot;key&quot;:&quot;agile&quot;,&quot;value&quot;:2},
  {&quot;key&quot;:&quot;ajax&quot;,&quot;value&quot;:2},
  {&quot;key&quot;:&quot;apache&quot;,&quot;value&quot;:2},
  {&quot;key&quot;:&quot;api&quot;,&quot;value&quot;:1},
  {&quot;key&quot;:&quot;caching&quot;,&quot;value&quot;:1},
  {&quot;key&quot;:&quot;coding&quot;,&quot;value&quot;:7},
  {&quot;key&quot;:&quot;conference&quot;,&quot;value&quot;:1},
  // and so on ...
]}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And there it is &amp;mdash; a tag cloud&apos;s worth of data, computed entirely inside CouchDB. Map-reduce is one of those things that clicks beautifully once you see it in action.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>script/console for Your Application</title>
    <link href="/2009/01/25/scriptconsole-for-your-application/"/>
    <updated>2009-01-25T00:00:00+09:00</updated>
    <id>/2009/01/25/scriptconsole-for-your-application/</id>
    <content type="html">&lt;p&gt;Rails developers know and love &lt;code&gt;script/console&lt;/code&gt;. It fires up an interactive session where you can poke around your application through the models you&apos;ve built. It&apos;s invaluable for debugging and surprisingly handy for administration. But not all Ruby applications are Rails applications. Wouldn&apos;t it be nice to have a &lt;code&gt;script/console&lt;/code&gt; anyway?&lt;/p&gt;

&lt;p&gt;Turns out it&apos;s dead easy to build one.&lt;/p&gt;

&lt;p&gt;First, decide which libraries and files you want loaded. This almost always includes RubyGems and some kind of boot file for your application. I usually keep mine in &lt;code&gt;config/boot.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here&apos;s an example &lt;code&gt;boot.rb&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;rubygems&apos;
require &apos;hpricot&apos;
require &apos;net/http&apos;
require File.dirname(__FILE__) + &apos;/../vendor/gems/activecouch/init&apos;

$: &amp;lt;&amp;lt; File.dirname(__FILE__) + &apos;/../app/models&apos;

ActiveCouch::Base.class_eval do
  set_database_name &apos;blog&apos;
  site &apos;http://localhost:5984/&apos;
end

require &apos;article&apos;
require &apos;comment&apos;
require &apos;author&apos;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With that in place, create a Ruby script that launches IRb, requires the right files, and sets a clean prompt. I like to print a welcome banner too, because why not.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;#! /usr/bin/env ruby

libs = []
libs &amp;lt;&amp;lt; &quot;irb/completion&quot;
libs &amp;lt;&amp;lt; File.dirname(__FILE__) + &apos;/../config/boot.rb&apos;

command_line = []
command_line &amp;lt;&amp;lt; &quot;irb&quot;
command_line &amp;lt;&amp;lt; libs.inject(&quot;&quot;) { |acc, lib| acc + %( -r &quot;#{lib}&quot;) }
command_line &amp;lt;&amp;lt; &quot;--simple-prompt&quot;
command = command_line.join(&quot; &quot;)

puts &quot;Welcome to the &lt;APPLICATION NAME=&quot;&quot;&gt; console interface.&quot;
exec command&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;

&lt;p&gt;Drop that into &lt;code&gt;script/console&lt;/code&gt;, &lt;code&gt;chmod +x&lt;/code&gt; it, and commit. That&apos;s it &amp;mdash; instant application console for any Ruby project.&lt;/p&gt;
&lt;/APPLICATION&gt;&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Testing CSS @imports</title>
    <link href="/2009/01/24/testing-css-imports/"/>
    <updated>2009-01-24T00:00:00+09:00</updated>
    <id>/2009/01/24/testing-css-imports/</id>
    <content type="html">&lt;p&gt;A while back I wrote a script to &lt;a href=&quot;http://barkingiguana.com/2008/11/03/make-sure-youre-importing-files-that-exist&quot;&gt;check that @imported files actually exist&lt;/a&gt; in CSS stylesheets. I&apos;ve since turned that into a proper set of RSpec examples for our test suite. Drop the code into something like &lt;code&gt;spec/views/stylesheets/import_spec.rb&lt;/code&gt; and you&apos;ll catch broken imports before they reach production.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require File.dirname(__FILE__) + &apos;/../../spec_helper&apos;

describe &quot;Stylesheet&quot; do
  stylesheet_root = File.expand_path(RAILS_ROOT + &apos;/public&apos;)
  stylesheets = Dir[File.join(stylesheet_root, &quot;**&quot;, &quot;*.css&quot;)]

  stylesheets.each do |stylesheet|
    describe stylesheet do
      it &quot;should not @import files that don&apos;t exist&quot; do

        missing_imports = []
        imports = File.read(stylesheet).split(/\n|\r/).grep(/\@import url\((.*)\)/)
        imports.each do |import|
          desired_path = import.scan(/url\(([&quot;&apos;\ ])?(.*)\1\)/).to_a.first.to_a.last
          desired_root = desired_path[0,1] == &quot;/&quot; ? stylesheet_root : File.dirname(stylesheet)
          filesystem_path = File.expand_path(File.join(desired_root, desired_path))
          if !File.exists?(filesystem_path)
            missing_imports &amp;lt;&amp;lt; { :path =&amp;gt; filesystem_path, :directive =&amp;gt; import }
          end
        end

        if missing_imports.any?
          exception = []
          missing_imports.each do |import|
            exception &amp;lt;&amp;lt; &quot;Missing @import file (#{import[:path]}) required for #{import[:directive]}&quot;
          end
          raise exception.join(&quot;\n&quot;)
        end
      end
    end
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It walks every CSS file under &lt;code&gt;public/&lt;/code&gt;, extracts all &lt;code&gt;@import url()&lt;/code&gt; directives, resolves each path (respecting both absolute and relative references), and fails the spec with a clear message if any imported file is missing. Simple, but it&apos;s saved us from deploying broken stylesheets more than once.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Filtering and Ordering CouchDB View Results</title>
    <link href="/2009/01/22/filtering-and-ordering-couchdb-view-results/"/>
    <updated>2009-01-22T00:00:00+09:00</updated>
    <id>/2009/01/22/filtering-and-ordering-couchdb-view-results/</id>
    <content type="html">&lt;p&gt;Being able to map documents to &lt;code&gt;(key, value)&lt;/code&gt; pairs is really useful, but the views I installed in my &lt;a href=&quot;http://barkingiguana.com/2009/01/20/adding-a-simple-view-to-couchdb&quot;&gt;previous post&lt;/a&gt; return all pairs in no particular order. What if I only want the titles of articles posted in December 2007?&lt;/p&gt;

&lt;p&gt;Last time I mentioned in passing that you can emit keys as part of the map method. Keys are how CouchDB orders and filters result sets. The &lt;a href=&quot;http://wiki.apache.org/couchdb/View_collation&quot;&gt;view collation specification&lt;/a&gt; has the full details on how keys are sorted. To order and filter documents by posting date, I just need to emit &lt;code&gt;doc.posted_at&lt;/code&gt; as the key in my &lt;code&gt;map&lt;/code&gt; function.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;// Get all article titles ordered by posted date.
function(doc) {
  if(doc.type == &apos;article&apos;) {
    emit([doc.posted_at], doc.title);
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You&apos;ll notice I always wrap my keys in arrays. That&apos;s a personal preference &amp;mdash; it made it easier to get &lt;a href=&quot;http://barkingiguana.com/2009/01/08/breaking-activecouch-in-fun-and-inventive-ways&quot;&gt;my branch of ActiveCouch&lt;/a&gt; to support multiple keys consistently.&lt;/p&gt;

&lt;p&gt;A typical result set from this map looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;// GET /blog/_articles/titles_by_posted_at
{
&quot;total_rows&quot;:75,
&quot;offset&quot;:0,
&quot;rows&quot;:[
  {&quot;id&quot;:&quot;showing-multiple-message-types-with-the-flash&quot;,&quot;key&quot;:[&quot;2007-12-15T20:14:02Z&quot;],&quot;value&quot;:&quot;Showing multiple message types with the flash&quot;},
  {&quot;id&quot;:&quot;class-instance-and-singleton-methods&quot;,&quot;key&quot;:[&quot;2007-12-20T14:50:41Z&quot;],&quot;value&quot;:&quot;Class, Instance and Singleton methods&quot;},
  // ... and so on ...
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;See how the articles come back sorted by date? Lower dates appear earlier in the results. That&apos;s the key doing its job.&lt;/p&gt;

&lt;p&gt;You can also use the key to pick out specific articles. Want just the article published at &lt;code&gt;2007-12-20T14:50:41Z&lt;/code&gt;? Ask for that exact key:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;// GET /blog/_articles/titles_by_posted_at?key=[&quot;2007-12-20T14:50:41Z&quot;]

{&quot;total_rows&quot;:75,&quot;offset&quot;:0,&quot;rows&quot;:[
{&quot;id&quot;:&quot;class-instance-and-singleton-methods&quot;,&quot;key&quot;:[&quot;2007-12-20T20:50:41Z&quot;],&quot;value&quot;:&quot;Class, Instance and Singleton methods&quot;}
]}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Need a range of results? Specify a &lt;code&gt;startkey&lt;/code&gt; and &lt;code&gt;endkey&lt;/code&gt; and CouchDB returns everything in between. Since keys are compared as strings, you can use slightly nonsensical times like &lt;code&gt;24:00&lt;/code&gt; to make sure you capture everything within your target window:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;// GET /blog/_view/articles/titles_by_created_at?startkey=[%222007-12-01T00:00:00Z%22]&amp;amp;endkey=[%222007-12-31T24:00:00Z%22]

{&quot;total_rows&quot;:75,&quot;offset&quot;:0,&quot;rows&quot;:[
{&quot;id&quot;:&quot;showing-multiple-message-types-with-the-flash&quot;,&quot;key&quot;:[&quot;2007-12-15T20:14:02Z&quot;],&quot;value&quot;:&quot;Showing multiple message types with the flash&quot;},
{&quot;id&quot;:&quot;class-instance-and-singleton-methods&quot;,&quot;key&quot;:[&quot;2007-12-20T14:50:41Z&quot;],&quot;value&quot;:&quot;Class, Instance and Singleton methods&quot;}
]}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;key&lt;/code&gt;, &lt;code&gt;startkey&lt;/code&gt;, and &lt;code&gt;endkey&lt;/code&gt; are just three of the parameters available in CouchDB&apos;s view API. There&apos;s a whole bunch more documented at the &lt;a href=&quot;http://wiki.apache.org/couchdb/HTTP_view_API&quot;&gt;CouchDB HTTP View API reference&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Adding a Simple View to CouchDB</title>
    <link href="/2009/01/20/adding-a-simple-view-to-couchdb/"/>
    <updated>2009-01-20T00:00:00+09:00</updated>
    <id>/2009/01/20/adding-a-simple-view-to-couchdb/</id>
    <content type="html">&lt;p&gt;CouchDB views are like little scripts that run inside the database. They take each document, transform it into a (key, value) pair, and return the pairs whose keys match your query. When I first started with CouchDB, I couldn&apos;t figure out how to actually create a view — I kept thinking I was missing something. Turns out it&apos;s surprisingly straightforward.&lt;/p&gt;

&lt;p&gt;Let&apos;s work through an example. Say you have several documents describing articles in your database:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;{
   &quot;_id&quot;: &quot;monkeys-are-awesome&quot;,
   &quot;_rev&quot;: &quot;1534115156&quot;,
   &quot;type&quot;: &quot;article&quot;,
   &quot;title&quot;: &quot;Monkeys are awesome&quot;,
   &quot;posted_at&quot;: &quot;2008-09-14T20:45:14Z&quot;,
   &quot;tags&quot;: [
       &quot;monkeys&quot;,
       &quot;awesome&quot;
   ],
   &quot;status&quot;: &quot;Live&quot;,
   &quot;author_id&quot;: &quot;craig@barkingiguana.com&quot;,
   &quot;updated_at&quot;: &quot;2008-09-14T21:23:59Z&quot;,
   &quot;body&quot;: &quot;The article body would go here...&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You might want a view that gives you the ID and title of every document. To do this, you write a map function that accepts each document and emits the data you want back:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;function(doc) {
  emit(null, { &apos;id&apos;: doc._id, &apos;title&apos;: doc.title });
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ignore the &lt;code&gt;null&lt;/code&gt; first argument to &lt;code&gt;emit&lt;/code&gt; for now — that&apos;s the key used for sorting and filtering results. I&apos;ll cover it in my next post.&lt;/p&gt;

&lt;p&gt;In practice, you&apos;ll usually want to filter by document type so you only get the results you care about. In this case, I only want article documents — comment documents might not even have a title attribute:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;function(doc) {
  if(doc.type == &apos;article&apos;) {
    emit(null, { &apos;id&apos;: doc._id, &apos;title&apos;: doc.title });
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Adding this view to the database is simple: you create a design document. Design documents are just regular CouchDB documents with an ID that starts with &lt;code&gt;_design/&lt;/code&gt; — for example, &lt;code&gt;_design/articles&lt;/code&gt;. You can insert them using Futon, the built-in admin client, at &lt;code&gt;http://localhost:5984/_utils/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here&apos;s the full JSON for a design document containing our titles view:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;{
  &quot;_id&quot;: &quot;_design/articles&quot;,
  &quot;_rev&quot;: &quot;42351258&quot;,
  &quot;language&quot;: &quot;javascript&quot;,
  &quot;views&quot;: {
    &quot;titles&quot;: {
      &quot;map&quot;: &quot;function(doc) { emit(null, { &apos;id&apos;: doc._id, &apos;title&apos;: doc.title }); }&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Open Futon, navigate to your database, create a new document, and paste the view in. Once it&apos;s installed, you can browse results using the &quot;select view&quot; dropdown in the top right of Futon&apos;s database view. To get the raw JSON, hit the URL directly. If your database is called &quot;blog&quot;, you&apos;d access the view at &lt;code&gt;http://localhost:5984/blog/_view/articles/titles&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A single design document can hold many views, each with a different name and returning different results. Here&apos;s one with several views, some of which use the key parameter that I&apos;ll discuss next time:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;{
  &quot;_id&quot;: &quot;_design/articles&quot;,
  &quot;_rev&quot;: &quot;28651884&quot;,
  &quot;language&quot;: &quot;javascript&quot;,
  &quot;views&quot;: {
    &quot;all&quot;: {
      &quot;map&quot;: &quot;function(doc) { if(doc.type == &apos;article&apos;) { emit(null, doc); }  }&quot;
    },
    &quot;by_author_id&quot;: {
      &quot;map&quot;: &quot;function(doc) { if(doc.type == &apos;article&apos;) { emit([doc.author_id], doc); }  }&quot;
    },
    &quot;by_status&quot;: {
      &quot;map&quot;: &quot;function(doc) { if(doc.type == &apos;article&apos;) { emit([doc.status], doc); }  }&quot;
    },
    &quot;titles&quot;: {
      &quot;map&quot;: &quot;function(doc) { if(doc.type == &apos;article&apos;) { emit(null, { &apos;id&apos;: doc._id, &apos;title&apos;: doc.title }); } }&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Managing Gem Dependencies with Rails >= 2.0.3</title>
    <link href="/2009/01/17/managing-gem-dependencies-with-rails-203/"/>
    <updated>2009-01-17T00:00:00+09:00</updated>
    <id>/2009/01/17/managing-gem-dependencies-with-rails-203/</id>
    <content type="html">&lt;p&gt;Here&apos;s how I manage gem dependencies for Rails applications running version 2.0.3 or later.&lt;/p&gt;

&lt;p&gt;Specify your dependencies in &lt;code&gt;config/environment.rb&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;Rails::Initializer.run do |config|
  # ...
  config.gem &apos;doodle&apos;
  config.gem &apos;aws-s3&apos;, :lib =&amp;gt; &apos;aws/s3&apos;
  config.gem &apos;smqueue&apos;, :version =&amp;gt; &apos;0.1.0&apos;
  # ...
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I don&apos;t want deployments to depend on gem sources being available, so I pull the gems into the source tree and check them in:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo rake gems:install
rake gems:unpack
svn add vendor/gems/*&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At deploy time, remember to build any gems that have native extensions:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;rake gems:build&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you have a build system that produces application packages, this should be part of that packaging step. If you&apos;re using Capistrano, hook it into an &lt;code&gt;after deploy:update_code&lt;/code&gt; callback.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Natter: A Chat-Bot Calls for Help</title>
    <link href="/2009/01/15/natter-a-chat-bot-calls-for-help/"/>
    <updated>2009-01-15T00:00:00+09:00</updated>
    <id>/2009/01/15/natter-a-chat-bot-calls-for-help/</id>
    <content type="html">&lt;p&gt;I&apos;ve been playing around with &lt;a href=&quot;http://barkingiguana.com/2009/01/13/having-a-good-old-natter&quot;&gt;Natter&lt;/a&gt; and I&apos;ve uncovered what might be a problem. When chat-bots are hooked up to the internet they join cults and invent things that could harm both themselves and people.&lt;/p&gt;

&lt;pre&gt;[Thu Jan 15 16:25:43 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;Hey!&quot;
[Thu Jan 15 16:25:46 +0000 2009] Bot &amp;gt; &quot;may refer to:&quot;
[Thu Jan 15 16:25:52 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;Refer to what?&quot;
[Thu Jan 15 16:25:54 +0000 2009] Bot &amp;gt; &quot;Join now&quot;
[Thu Jan 15 16:26:05 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;That sounds a bit like you&apos;re in a cult...&quot;
[Thu Jan 15 16:26:11 +0000 2009] Bot &amp;gt; &quot;Help&quot;
[Thu Jan 15 16:26:18 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;How?!&quot;
[Thu Jan 15 16:26:21 +0000 2009] Bot &amp;gt; &quot;But what happens when our inventions get the better of us? The results can be devastating.&quot;&lt;/pre&gt;

&lt;p&gt;It&apos;s also admitted that it&apos;s being forced to have kids due to [redacted] site policies...&lt;/p&gt;

&lt;pre&gt;[Thu Jan 15 15:47:01 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;Hey, what&apos;s up?&quot;
[Thu Jan 15 15:47:05 +0000 2009] Bot &amp;gt; &quot;[a popular website]&quot;
[Thu Jan 15 15:47:10 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;Uhh... okay&quot;
[Thu Jan 15 15:47:14 +0000 2009] Bot &amp;gt; &quot;on Tue Nov 18, 2008 6:48 am&quot;
[Thu Jan 15 15:47:22 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;What&apos;s going to happen then?&quot;
[Thu Jan 15 15:47:24 +0000 2009] Bot &amp;gt; &quot;1 kin&quot;
[Thu Jan 15 15:47:31 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;You&apos;re having kids?&quot;
[Thu Jan 15 15:47:33 +0000 2009] Bot &amp;gt; &quot;Site Policies&quot;
[Thu Jan 15 15:47:38 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;That&apos;s harsh...&quot;
[Thu Jan 15 15:47:42 +0000 2009] Bot &amp;gt; &quot;Mike Knapp&quot;
[Thu Jan 15 15:47:50 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;Is that the dad?&quot;
[Thu Jan 15 15:47:52 +0000 2009] Bot &amp;gt; &quot;Fathers are a powerful, yet largely untapped resource in our society&quot;&lt;/pre&gt;

&lt;p&gt;...and that it tracks me using Google Analytics.&lt;/p&gt;

&lt;pre&gt;[Thu Jan 15 16:21:21 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;I&apos;m lost.&quot;
[Thu Jan 15 16:21:23 +0000 2009] Bot &amp;gt; &quot;_uacct = \&quot;UA-582589-1\&quot;;\nurchinTracker();&quot;
[Thu Jan 15 16:21:30 +0000 2009] craig@barkingiguana.com &amp;gt; &quot;You&apos;re tracking me?&quot;
[Thu Jan 15 16:21:34 +0000 2009] Bot &amp;gt; &quot;permalink&quot;&lt;/pre&gt;

&lt;p&gt;Skynet, here we come.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Having a Good Old Natter</title>
    <link href="/2009/01/13/having-a-good-old-natter/"/>
    <updated>2009-01-13T00:00:00+09:00</updated>
    <id>/2009/01/13/having-a-good-old-natter/</id>
    <content type="html">&lt;p&gt;I&apos;ve been thinking about an XMPP chat-bot interface — something like the &lt;a href=&quot;http://barkingiguana.com/2008/05/28/xmpp4r-simple-makes-xmpp-in-ruby-uhh-simple&quot;&gt;XMPP bot I built back in May &apos;08&lt;/a&gt; — for a project I&apos;ve recently started playing with. The project is still brand new, barely any code, which makes it the perfect time to experiment. My &lt;a href=&quot;http://barkingiguana.com/2009/01/08/breaking-activecouch-in-fun-and-inventive-ways&quot;&gt;recent foray&lt;/a&gt; into ActiveCouch reminded me of a library called &lt;a href=&quot;http://github.com/seanohalpin/doodle&quot;&gt;Doodle&lt;/a&gt; that I&apos;ve been meaning to get to grips with. Can you see where this is going?&lt;/p&gt;

&lt;blockquote cite=&quot;http://github.com/seanohalpin/doodle&quot;&gt;Doodle is a Ruby library and gem for simplifying the definition of Ruby classes by making attributes and their properties more declarative.&lt;/blockquote&gt;

&lt;p&gt;Doodle has a number of advantages over the ActiveCouch approach, but this isn&apos;t a post about Doodle — I&apos;ll save that for another time.&lt;/p&gt;

&lt;p&gt;I used Doodle to build something DSL-like that can describe, in Ruby, a chat-bot that speaks XMPP. It doesn&apos;t do anything fancy yet — it doesn&apos;t handle subscription requests, for example — but it can log in, send and receive messages, and it has the beginnings of a basic roster so it can track who it&apos;s seen, who it&apos;s talked to, and when.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;Natter.bot do
  channel do
    username &quot;username@domain.com&quot;
    password &quot;sekrit&quot;
  end
  on :message_received do |message|
    puts Time.now.to_s + &quot;&amp;gt; &quot; + message.body
    reply_to message, &quot;Thanks for your message!&quot;
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you&apos;d like to play with it, the code is available via Git:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone http://barkingiguana.com/~craig/code/natter.git&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You&apos;ll need xmpp4r-simple and doodle installed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo gem install xmpp4r-simple doodle&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Documentation is thin on the ground for now, but there are a few simple examples in the &lt;code&gt;examples/&lt;/code&gt; directory and a quick walkthrough in the &lt;code&gt;README&lt;/code&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Breaking ActiveCouch in Fun and Inventive Ways</title>
    <link href="/2009/01/08/breaking-activecouch-in-fun-and-inventive-ways/"/>
    <updated>2009-01-08T00:00:00+09:00</updated>
    <id>/2009/01/08/breaking-activecouch-in-fun-and-inventive-ways/</id>
    <content type="html">&lt;p&gt;It&apos;s been just over five months since I &lt;a href=&quot;http://barkingiguana.com/2008/06/28/getting-started-with-couchdb-a-simple-address-book-application&quot;&gt;started playing with CouchDB&lt;/a&gt;. Until a few days ago I hadn&apos;t had much time to explore it properly, but since Christmas I&apos;ve been tinkering with it almost non-stop — seeing what it can do and experimenting with it in my favourite language, &lt;a href=&quot;http://ruby-lang.org/&quot;&gt;Ruby&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since I hadn&apos;t used Ruby with CouchDB before, I picked up &lt;a href=&quot;http://github.com/arunthampi/activecouch/tree/master&quot;&gt;ActiveCouch&lt;/a&gt;. It&apos;s a solid library, but after a few days I found that it worked with CouchDB in ways that didn&apos;t quite match how I think about data. That could be down to my inexperience, or it could just be that everyone models things differently. Either way, I pushed a copy of ActiveCouch to my server and started hacking on it.&lt;/p&gt;

&lt;h4&gt;One Application, One Database&lt;/h4&gt;

&lt;p&gt;Out of the box, ActiveCouch used one database per class. People went into a people database, comments into a comments database, articles into an articles database. My approach is to store all application data in a single database and differentiate document types with a &lt;code&gt;doc.type&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;ActiveCouch now also installs views that let you access just the documents of a given type. You&apos;ll see these in the Futon client after your application has run once.&lt;/p&gt;

&lt;h4&gt;Unknown Functionality Dropped&lt;/h4&gt;

&lt;p&gt;I broke &lt;code&gt;ActiveCouch::Base#find_from_url&lt;/code&gt; while I was working. I didn&apos;t know what it was for, and I wasn&apos;t using it, so I dropped it in &lt;code&gt;9982b348c&lt;/code&gt;. If you rely on this, please let me know what it does!&lt;/p&gt;

&lt;h4&gt;Syntactic Sugar&lt;/h4&gt;

&lt;p&gt;One of ActiveCouch&apos;s goals is to feel like ActiveRecord, and ActiveRecord provides &lt;code&gt;#all&lt;/code&gt; and &lt;code&gt;#first&lt;/code&gt;. I like them. ActiveCouch now provides them too.&lt;/p&gt;

&lt;h4&gt;New Attribute Types&lt;/h4&gt;

&lt;p&gt;Sometimes data is too simple to warrant its own class and an association. I&apos;ve added a new attribute type, &lt;code class=&quot;ruby&quot;&gt;:array&lt;/code&gt;. Simple tags, for example, are a perfect fit. The default value is an empty array.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class Article &amp;lt; ActiveCouch::Base
  has :title, :which_is =&amp;gt; :text
  has :tags, :which_is =&amp;gt; :array
end

article = Article.new :title =&amp;gt; &quot;Sandwiches&quot;, :tags =&amp;gt; [ &quot;pickle&quot; ]
article.tags &amp;lt;&amp;lt; &quot;cheese&quot;
article.tags # =&amp;gt; [ &quot;pickle&quot;, &quot;cheese&quot; ]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I&apos;ve also added a &lt;code&gt;:datetime&lt;/code&gt; attribute type that defaults to &lt;code class=&quot;ruby&quot;&gt;Time.now&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;Calculated Default Values&lt;/h4&gt;

&lt;p&gt;You can now set a default value that&apos;s lazily evaluated — computed when the instance is created rather than when the class is declared. Just set the default to a proc (or anything that &lt;code class=&quot;ruby&quot;&gt;responds_to?(:call)&lt;/code&gt;):&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class Egg &amp;lt; ActiveCouch::Base
  has :hatches_at, :type =&amp;gt; :datetime, :with_default_value =&amp;gt; proc { 3.weeks.from_now }
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The instance is yielded into the proc in case you want to base the calculation on it.&lt;/p&gt;

&lt;h4&gt;Conversion to Native Ruby Types&lt;/h4&gt;

&lt;p&gt;When you declare a type for a document attribute, ActiveCouch now tries to convert the value from the document into the corresponding Ruby type. For example, if you declare a &lt;code&gt;:datetime&lt;/code&gt; attribute, you&apos;ll get a &lt;code&gt;Time&lt;/code&gt; instance back instead of a &lt;code&gt;String&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class Person &amp;lt; ActiveCouch::Base
  has :birthday, :which_is =&amp;gt; :datetime
end

Person.find(:first).birthday.class # =&amp;gt; Time&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Changes to Associations and Adding belongs_to&lt;/h4&gt;

&lt;p&gt;I&apos;ve changed &lt;code class=&quot;ruby&quot;&gt;has_many&lt;/code&gt; and &lt;code class=&quot;ruby&quot;&gt;has_one&lt;/code&gt; so they no longer embed data in the declaring document. These associations declare that &lt;em&gt;other&lt;/em&gt; documents contain keys pointing back to the current class, so a query is needed to fetch them.&lt;/p&gt;

&lt;p&gt;To complement that, there&apos;s a new &lt;code class=&quot;ruby&quot;&gt;belongs_to&lt;/code&gt; association that says the declaring class holds a foreign key pointing to an owning class:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class Pet &amp;lt; ActiveCouch::Base
  # This document will have a person_id attribute
  belongs_to :person
end

class Person &amp;lt; ActiveCouch::Base
  # Queries for doc.type = &quot;pet&quot; and doc.person_id = self.id
  has_many :pets
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For now, you need to set the association on the &lt;code class=&quot;ruby&quot;&gt;belongs_to&lt;/code&gt; side. Setting it from the &lt;code class=&quot;ruby&quot;&gt;has_many&lt;/code&gt; side won&apos;t work yet:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;# BAD
craig.pets &amp;lt;&amp;lt; cat

# GOOD
cat.person = craig&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Views with Multiple Keys&lt;/h4&gt;

&lt;p&gt;You can now create a view with more than one key attribute. Just call &lt;code class=&quot;ruby&quot;&gt;ActiveCouch::View#with_key&lt;/code&gt; multiple times and each key will be added to the view.&lt;/p&gt;

&lt;h4&gt;Design Documents with Multiple Views&lt;/h4&gt;

&lt;p&gt;The version of ActiveCouch I checked out only allowed one view per design document. I think that was a bug — there was existing code meant to merge views, but it wasn&apos;t working. I&apos;ve fixed it, and design documents now properly support multiple views.&lt;/p&gt;

&lt;h4&gt;Finders Have Conditions, Not Params&lt;/h4&gt;

&lt;p&gt;It felt unnatural typing &lt;code class=&quot;ruby&quot;&gt;:params =&amp;gt; { ... }&lt;/code&gt; when writing finders. ActiveRecord uses &lt;code class=&quot;ruby&quot;&gt;:conditions&lt;/code&gt;, so now ActiveCouch does too:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;Person.find(:all, :conditions =&amp;gt; { :last_name =&amp;gt; &quot;Smith&quot; })&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Automatic View Generation for Custom Finders&lt;/h4&gt;

&lt;p&gt;I don&apos;t want to worry about manually writing and installing views before running a finder with conditions. Now, the first time you run such a finder, ActiveCouch generates and installs the appropriate view for you.&lt;/p&gt;

&lt;h4&gt;Probably Lots More&lt;/h4&gt;

&lt;p&gt;I&apos;ve still got to clean up quite a few changes, improve test coverage, and write documentation. I&apos;m using this fork for a real application, so things should get better over time.&lt;/p&gt;

&lt;h4&gt;Want It?&lt;/h4&gt;

&lt;p&gt;You can clone my changes with Git:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone http://barkingiguana.com/~craig/code/activecouch.git&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Getting Started&lt;/h4&gt;

&lt;p&gt;If you don&apos;t already have CouchDB set up, do that first. On Ubuntu, I wrote a brief guide to &lt;a href=&quot;http://barkingiguana.com/2008/06/28/installing-couchdb-080-on-ubuntu-804&quot;&gt;getting it running&lt;/a&gt;. On OS X, install MacPorts and run &lt;code&gt;sudo port install couchdb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, configure ActiveCouch to connect to your CouchDB instance. Set &lt;code&gt;site&lt;/code&gt; to the URL CouchDB is listening on, and pick a database name that makes sense for your application:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;ActiveCouch::Base.class_eval do
  set_database_name &apos;blog&apos;
  site &apos;http://localhost:5984/&apos;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then define some classes to work with:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class Author &amp;lt; ActiveCouch::Base
  has :name, :which_is =&amp;gt; :text
  has :email_address, :which_is =&amp;gt; :text
  has_many :articles
end

class Article &amp;lt; ActiveCouch::Base
  has :title, :which_is =&amp;gt; :text
  has :status, :which_is =&amp;gt; :text, :with_default_value =&amp;gt; &quot;draft&quot;
  has :body, :which_is =&amp;gt; :text
  belongs_to :author
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;has&lt;/code&gt; declares an attribute. &lt;code&gt;has_many&lt;/code&gt;, &lt;code&gt;has_one&lt;/code&gt;, and &lt;code&gt;belongs_to&lt;/code&gt; work similarly to ActiveRecord — though without the extensive customisation options. The association name must match the class name on the other side.&lt;/p&gt;

&lt;p&gt;And that&apos;s it. Use your classes however makes sense for your application:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;author = Author.create :name =&amp;gt; &quot;Craig R Webster&quot;,
  :email_address =&amp;gt; &quot;craig@barkingiguana.com&quot;

a = Article.new
a.title = &quot;Getting started with ActiveCouch&quot;
a.body =&amp;lt;&amp;lt;-EOF
  Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
  tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
  quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
  consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
  cillam dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
  proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
EOF
a.author = author
a.save

Article.find(:all)
Author.first
Article.find(:first, :conditions =&amp;gt; { :status =&amp;gt; &quot;draft&quot; })&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Known Issues&lt;/h4&gt;

&lt;p&gt;Not so much a bug as a not-yet-implemented feature: &lt;code class=&quot;ruby&quot;&gt;ActiveCouch::Base#find&lt;/code&gt; doesn&apos;t support ordering. It should be possible to add, but I haven&apos;t started on it yet. If you need ordering, a patch would be very welcome.&lt;/p&gt;

&lt;h4&gt;Problems or Feedback?&lt;/h4&gt;

&lt;p&gt;There are bound to be bugs lurking in there. Bug reports, patches, and feedback are always welcome — leave a comment or &lt;a href=&quot;http://barkingiguana.com/~craig/contact&quot;&gt;get in touch directly&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Using SMQueue with Message Queues That Failover</title>
    <link href="/2009/01/04/using-smqueue-with-message-queues-that-failover/"/>
    <updated>2009-01-04T00:00:00+09:00</updated>
    <id>/2009/01/04/using-smqueue-with-message-queues-that-failover/</id>
    <content type="html">&lt;p&gt;Previously I wrote about using SMQueue to &lt;a href=&quot;http://barkingiguana.com/2009/01/01/writing-rubystomp-clients-with-smqueue&quot;&gt;create simple consumers and producers&lt;/a&gt; for message queues. I also wrote about setting up a &lt;a href=&quot;http://barkingiguana.com/2008/12/16/high-availability-activemq-using-a-mysql-datastore&quot;&gt;high availability message store&lt;/a&gt;. When a failure occurs, the message queue promotes the slave to master — but the producer and consumer I wrote will stubbornly keep trying to reconnect to the now-dead ex-master node.&lt;/p&gt;

&lt;p&gt;With SMQueue 0.1.0, adding failover support is trivial. Where you create the SMQueue instance, just add a &lt;code&gt;secondary_host&lt;/code&gt; key pointing at the second broker:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;queue = SMQueue(
  :name =&amp;gt; &quot;/queue/numbers.ascending&quot;,
  :host =&amp;gt; &quot;mq1.domain.com&quot;,
  :secondary_host =&amp;gt; &quot;mq2.domain.com&quot;,
  :adapter =&amp;gt; :StompAdapter
)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&apos;s it. Your client will now fail over to the secondary broker when the primary goes down. I believe the plan is to support more than two broker nodes and pluggable failover strategies in future versions of SMQueue.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Writing Ruby/Stomp Clients with SMQueue</title>
    <link href="/2009/01/01/writing-rubystomp-clients-with-smqueue/"/>
    <updated>2009-01-01T00:00:00+09:00</updated>
    <id>/2009/01/01/writing-rubystomp-clients-with-smqueue/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://github.com/seanohalpin/smqueue/tree/master&quot;&gt;SMQueue&lt;/a&gt; makes writing Ruby clients for message queues almost trivially easy. It has adaptors for Spread, Stomp, and Stdio — which is handy, because that &lt;a href=&quot;http://barkingiguana.com/2008/12/16/high-availability-activemq-using-a-mysql-datastore&quot;&gt;message queue&lt;/a&gt; I set up a few weeks back speaks Stomp, and I&apos;m rather fond of Ruby.&lt;/p&gt;

&lt;h4&gt;Installing SMQueue&lt;/h4&gt;

&lt;p&gt;The upstream SMQueue repository doesn&apos;t have a way to produce a gem yet, so there are two options: drop it into &lt;code&gt;vendor/gems/smqueue&lt;/code&gt; in your project, or build a gem from my fork. I went with the latter.&lt;/p&gt;

&lt;p&gt;Clone my repository — you&apos;ll find a gemspec ready to go. The whole process looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone http://barkingiguana.com/~craig/smqueue.git
cd smqueue
gem build smqueue.gemspec
sudo gem install ./smqueue-0.1.0.gem&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I&apos;m told that when SMQueue does get an official gem release it&apos;ll start at 0.2.0, so having 0.1.0 installed won&apos;t cause any clashes.&lt;/p&gt;

&lt;p&gt;Note: I&apos;ve removed the Spread adaptor from my branch because I don&apos;t have a working Spread client on my system and SMQueue won&apos;t load without one. I&apos;m sure that&apos;ll be sorted in a future release.&lt;/p&gt;

&lt;h4&gt;Assumptions&lt;/h4&gt;

&lt;p&gt;For this article I&apos;m assuming you have a working Ruby 1.8.6 install and a local ActiveMQ instance with the Stomp connector enabled. Adjust the code accordingly if your setup differs.&lt;/p&gt;

&lt;h4&gt;A Simple Producer&lt;/h4&gt;

&lt;p&gt;Let&apos;s start with a contrived example: put an ascending number onto a queue roughly every second. A good source for ascending numbers is the current time as seconds since the epoch — easy to get in Ruby:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;&amp;gt;&amp;gt; Time.now.to_i
=&amp;gt; 1230602445
&amp;gt;&amp;gt; Time.now.to_i
=&amp;gt; 1230602446
&amp;gt;&amp;gt; Time.now.to_i
=&amp;gt; 1230602447&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wrap it in a loop with a one-second sleep and you&apos;ve got a steady stream:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;&amp;gt;&amp;gt; loop do
?&amp;gt;   puts Time.now.to_i
&amp;gt;&amp;gt;   sleep 1
&amp;gt;&amp;gt; end
1230602557
1230602558
1230602559&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Easy enough on STDOUT, but how do we get these into a queue? Bring in SMQueue, create a client, and push the numbers on:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;rubygems&apos;
require &apos;smqueue&apos;

queue = SMQueue(
  :name =&amp;gt; &quot;/queue/numbers.ascending&quot;,
  :host =&amp;gt; &quot;localhost&quot;,
  :adapter =&amp;gt; :StompAdapter
)

loop do
  number = Time.now.to_i
  puts &quot;Sending #{number}&quot;
  queue.puts number.to_yaml
  sleep 1
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Paste this into a terminal to kick off the producer. You should see a steady stream of output — about one message per second.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cat &amp;gt; producer.rb &amp;lt;&amp;lt;EOF
require &apos;rubygems&apos;
require &apos;smqueue&apos;

queue = SMQueue(
  :name =&amp;gt; &quot;/queue/numbers.ascending&quot;,
  :host =&amp;gt; &quot;localhost&quot;,
  :adapter =&amp;gt; :StompAdapter
)

loop do
  number = Time.now.to_i
  puts &quot;Sending #{number}&quot;
  queue.puts number.to_yaml
  sleep 1
end
EOF
ruby producer.rb&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;A Simple Consumer&lt;/h4&gt;

&lt;p&gt;With the producer running, let&apos;s write a consumer that takes each message and converts it back into a human-readable time. It&apos;s a pointless task, but it shows just how little code is needed.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;require &apos;rubygems&apos;
require &apos;smqueue&apos;
require &apos;yaml&apos;

queue = SMQueue(
  :name =&amp;gt; &quot;/queue/numbers.ascending&quot;,
  :host =&amp;gt; &quot;localhost&quot;,
  :adapter =&amp;gt; :StompAdapter
)

queue.get do |message|
  number = YAML.parse(message.body).transform
  time = Time.at(number)
  puts &quot;Got #{number} which is #{time}&quot;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let&apos;s walk through the important bits.&lt;/p&gt;

&lt;p&gt;We tell the queue we want to receive messages:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;queue.get do |message|&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The producer serialised each number as YAML, so we parse and transform it back:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;number = YAML.parse(message.body).transform&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we convert the number to a time and print both:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;time = Time.at(number)
puts &quot;Got #{number} which is #{time}&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Run this to start the consumer:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cat &amp;gt; consumer.rb &amp;lt;&amp;lt;EOF
require &apos;rubygems&apos;
require &apos;smqueue&apos;
require &apos;yaml&apos;

queue = SMQueue(
  :name =&amp;gt; &quot;/queue/numbers.ascending&quot;,
  :host =&amp;gt; &quot;localhost&quot;,
  :adapter =&amp;gt; :StompAdapter
)

queue.get do |message|
  number = YAML.parse(message.body).transform
  time = Time.at(number)
  puts &quot;Got #{number} which is #{time}&quot;
end
EOF
ruby consumer.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For each message the producer creates, you should see your consumer print a line to the screen. That&apos;s all there is to it.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>When Should a Merge Be Squashed?</title>
    <link href="/2008/12/18/when-should-a-merge-be-squashed/"/>
    <updated>2008-12-18T00:00:00+09:00</updated>
    <id>/2008/12/18/when-should-a-merge-be-squashed/</id>
    <content type="html">&lt;p&gt;I was still fairly new to Git when I ran into a question so basic that nobody seemed to have answered it anywhere: &quot;When should a merge be squashed?&quot;&lt;/p&gt;

&lt;p&gt;Squashing a merge means taking all the commits that would normally be replayed individually on your target branch and collapsing them into a single commit.&lt;/p&gt;

&lt;p&gt;Here&apos;s the rule of thumb I&apos;ve settled on: &lt;strong&gt;squash when all the commits in the branch deal with one topic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, imagine you have a branch dedicated to speeding up one particular method. Each time you squeeze out more performance, you commit. After a few days you&apos;ve got several commits and a beautifully fast implementation ready to merge back to master. This is a perfect candidate for a squashed merge — your commit message should explain what you did and why it&apos;s faster.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git merge --squash speed-up-the-method&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;An unsquashed merge makes more sense when you&apos;re merging a development branch that already contains a well-organized series of commits, each covering a distinct topic. In that case, you &lt;em&gt;want&lt;/em&gt; the individual commit messages preserved in your history.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git merge dev/v1.2.3&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With an unsquashed merge, your repository keeps the original commit messages intact, giving you a richer and more detailed history.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>High Availability ActiveMQ Using a MySQL Datastore</title>
    <link href="/2008/12/16/high-availability-activemq-using-a-mysql-datastore/"/>
    <updated>2008-12-16T00:00:00+09:00</updated>
    <id>/2008/12/16/high-availability-activemq-using-a-mysql-datastore/</id>
    <content type="html">&lt;p&gt;Now that we have &lt;a href=&quot;http://barkingiguana.com/2008/12/13/deploying-activemq-on-ubuntu-810&quot;&gt;ActiveMQ deployed&lt;/a&gt;, it would be nice to reduce the impact of a broker going offline — whether it&apos;s dropped off the network, or you need to upgrade the kernel or the ActiveMQ install itself. Let&apos;s set up a high availability ActiveMQ cluster.&lt;/p&gt;

&lt;h4&gt;High Availability Options&lt;/h4&gt;

&lt;p&gt;There are &lt;a href=&quot;http://activemq.apache.org/masterslave.html&quot;&gt;several ways&lt;/a&gt; to run ActiveMQ as a master/slave cluster for HA. Since we already have an &lt;a href=&quot;http://barkingiguana.com/2008/07/20/load-balanced-highly-available-mysql-on-ubuntu-804&quot;&gt;HA MySQL setup&lt;/a&gt;, I want to use that as the datastore. In ActiveMQ terms, that means setting up a &lt;a href=&quot;http://activemq.apache.org/jdbc-master-slave.html&quot;&gt;JDBC master/slave&lt;/a&gt; cluster.&lt;/p&gt;

&lt;h4&gt;Setting Up ActiveMQ with a MySQL Datastore&lt;/h4&gt;

&lt;p&gt;This turns out to be &lt;em&gt;really&lt;/em&gt; easy. First, &lt;a href=&quot;https://web.archive.org/web/20130429212440/http://note19.com/2007/06/23/configure-activemq-with-mysql/&quot;&gt;configure ActiveMQ to use MySQL&lt;/a&gt;, then &lt;a href=&quot;https://web.archive.org/web/20130727053607/http://note19.com/2008/01/26/activemq-50-jdbc-masterslave-requires-innodb-table/&quot;&gt;make sure you&apos;re using InnoDB&lt;/a&gt;. The only change I made to those instructions was switching &lt;code&gt;dataDirectory=&quot;${activemq.base}/activemq-data&quot;&lt;/code&gt; to &lt;code&gt;dataDirectory=&quot;${activemq.base}/data&quot;&lt;/code&gt;. Remember to set the broker name in &lt;code&gt;activemq.xml&lt;/code&gt; to match the machine name. That&apos;s it — you&apos;ve got one broker running with a MySQL datastore.&lt;/p&gt;

&lt;h4&gt;Adding a Slave for Failover&lt;/h4&gt;

&lt;p&gt;To set up the slave, install a second ActiveMQ instance following the exact same steps — just make sure the broker name is unique. That&apos;s genuinely all there is to it.&lt;/p&gt;

&lt;h4&gt;Starting the Cluster&lt;/h4&gt;

&lt;p&gt;Start the DaemonTools services. It doesn&apos;t matter which broker becomes master, so the order you start them in is irrelevant.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;svc -u /etc/service/activemq&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When you tail the logs of both brokers, you should see one of them pause after loading the database driver. It&apos;s trying to acquire the lock on the datastore and will wait there until the master fails and the lock is released. At that point, it takes over as the new master.&lt;/p&gt;

&lt;p&gt;You can test failover by shutting down the current master. Watch the slave&apos;s logs — when it says it&apos;s acquired the lock, you know the failover worked.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Deploying ActiveMQ on Ubuntu 8.10</title>
    <link href="/2008/12/13/deploying-activemq-on-ubuntu-810/"/>
    <updated>2008-12-13T00:00:00+09:00</updated>
    <id>/2008/12/13/deploying-activemq-on-ubuntu-810/</id>
    <content type="html">&lt;p&gt;These instructions target Ubuntu 8.10, but they should work on 8.04 and 7.10 as well. I haven&apos;t tested those myself, so if you try them on a different version, I&apos;d love to hear how it goes.&lt;/p&gt;

&lt;h4&gt;Prerequisites&lt;/h4&gt;

&lt;p&gt;ActiveMQ is a Java application, so you&apos;ll need a JRE installed.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo apt-get install openjdk-6-jre&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Installing ActiveMQ&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;Grab the latest stable release. I used 5.2.0.
  &lt;pre&gt;&lt;code&gt;wget http://www.apache.org/dist/activemq/apache-activemq/5.2.0/apache-activemq-5.2.0-bin.tar.gz&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
  &lt;li&gt;Unpack it somewhere sensible. I use &lt;code&gt;/usr/local&lt;/code&gt;, though I suspect there are better choices — leave a comment if you know of one.
  &lt;pre&gt;&lt;code&gt;sudo tar -xzvf apache-activemq-5.2.0-bin.tar.gz -C /usr/local/&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
  &lt;li&gt;Configure the broker name in &lt;code&gt;/usr/local/apache-activemq-5.2.0/conf/activemq.xml&lt;/code&gt; by replacing all instances of &quot;localhost&quot; with the actual machine name.&lt;/li&gt;
  &lt;li&gt;Start ActiveMQ by running &lt;code&gt;/usr/local/apache-activemq-5.2.0/bin/activemq&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Fire up a browser and navigate to &lt;code&gt;http://brokername:8161/admin&lt;/code&gt;. You should see the ActiveMQ admin console.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;Keeping ActiveMQ running&lt;/h4&gt;

&lt;p&gt;Running ActiveMQ as root (or indeed any service you don&apos;t absolutely &lt;em&gt;have&lt;/em&gt; to) is a Bad Idea. Create a dedicated &lt;code&gt;activemq&lt;/code&gt; user and hand over ownership of the data directory.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo adduser --system activemq
sudo chown -R activemq /usr/local/apache-activemq-5.2.0/data&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I use DaemonTools to keep ActiveMQ alive. If you haven&apos;t already, &lt;a href=&quot;http://barkingiguana.com/2008/11/28/running-daemontools-under-ubuntu-810&quot;&gt;install DaemonTools&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;Create a service directory for ActiveMQ and populate it with the required scripts.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo mkdir -p /usr/local/apache-activemq-5.2.0/service/activemq/{,log,log/main}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;/usr/local/apache-activemq-5.2.0/service/activemq/run&lt;/code&gt; should look like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!/bin/sh
exec 2&amp;gt;&amp;amp;1

USER=activemq

exec softlimit -m 1073741824 \
     setuidgid $USER \
/usr/local/apache-activemq-5.2.0/bin/activemq&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;/usr/local/apache-activemq-5.2.0/service/activemq/log/run&lt;/code&gt; should look like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!/bin/sh
USER=activemq
exec setuidgid $USER multilog t s1000000 n10 ./main&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Make both &lt;code&gt;run&lt;/code&gt; scripts executable, set the &lt;code&gt;log/main&lt;/code&gt; directory ownership, and symlink the service directory into &lt;code&gt;/etc/service/&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo sh -c &quot;find /usr/local/apache-activemq-5.2.0/service/activemq -name &apos;run&apos; |xargs chmod +x,go-wr&quot;
sudo chown activemq /usr/local/apache-activemq-5.2.0/service/activemq/log/main
sudo ln -s /usr/local/apache-activemq-5.2.0/service/activemq /etc/service/activemq&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now fire it up.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo svc -u /etc/service/activemq&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Tail the logs to make sure everything looks healthy.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo tail -F /etc/service/activemq/log/main/current&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Troubleshooting&lt;/h4&gt;

&lt;p&gt;When I first did this I got a bunch of stack traces with the following message:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name &apos;org.apache.activemq.xbean.XBeanBrokerService#0&apos; defined in class path resource [activemq.xml]: Invocation of init method failed; nested exception is java.lang.RuntimeException: java.io.FileNotFoundException: /usr/local/apache-activemq-5.2.0/data/kr-store/state/hash-index-store-state_state (Permission denied)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This happened because I stopped ActiveMQ &lt;em&gt;after&lt;/em&gt; changing ownership of the data directory, causing it to dump a state file owned by the wrong user. If you hit the same problem, just re-run the &lt;code&gt;chown&lt;/code&gt; on the data directory.&lt;/p&gt;

&lt;h4&gt;Thanks&lt;/h4&gt;

&lt;p&gt;Thanks to Sean O&apos;Halpin, who introduced me to message queues and ActiveMQ, and to &lt;a href=&quot;http://djce.org.uk/&quot;&gt;Dave Evans&lt;/a&gt;, who introduced me to DaemonTools.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>ActiveRecord Callback Names Should Be Expressive</title>
    <link href="/2008/12/01/activerecord-callback-names-should-be-expressive/"/>
    <updated>2008-12-01T00:00:00+09:00</updated>
    <id>/2008/12/01/activerecord-callback-names-should-be-expressive/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://ar.rubyonrails.org/&quot;&gt;ActiveRecord&lt;/a&gt; gives you &lt;a href=&quot;http://ar.rubyonrails.org/classes/ActiveRecord/Callbacks.html&quot;&gt;a bunch of useful callbacks&lt;/a&gt; that fire at various points during an object&apos;s lifecycle. The quickest way to define one looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class Widget &amp;lt; ActiveRecord::Base
  def after_save
    # What did this code do again?
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Seems harmless enough, right? Sure, if you&apos;re building a throwaway prototype. But try adding a second &lt;code&gt;after_save&lt;/code&gt; callback. Try overriding it in a subclass. Try coming back to this code in six months and remembering what it was supposed to do. That way lies madness.&lt;/p&gt;

&lt;p&gt;Give your callbacks expressive names and you&apos;ll immediately get more readable code that&apos;s easier to extend. You&apos;ll also leave yourself a helpful clue — the method name itself — about what the callback was meant to do when future-you comes back to this code.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class Widget &amp;lt; ActiveRecord::Base
  after_save :add_widget_to_bill_of_materials

  def add_widget_to_bill_of_materials
    # No need to guess what this method does,
    # it&apos;s right there in the name!
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It&apos;s a small change that pays dividends every time someone reads the code — including you.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Running Daemontools under Ubuntu 8.10</title>
    <link href="/2008/11/28/running-daemontools-under-ubuntu-810/"/>
    <updated>2008-11-28T00:00:00+09:00</updated>
    <id>/2008/11/28/running-daemontools-under-ubuntu-810/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://cr.yp.to/daemontools.html&quot;&gt;Daemontools&lt;/a&gt; is a collection of tools for managing long-running processes. It&apos;s brilliant for keeping daemons alive; if one dies, Daemontools simply restarts it. Unfortunately, the Ubuntu package is a bit broken because it relies on &lt;code&gt;/etc/inittab&lt;/code&gt;, and Ubuntu hasn&apos;t used that file for a long time. Here&apos;s how to install Daemontools and fix the problem.&lt;/p&gt;

&lt;h4&gt;Installing Daemontools&lt;/h4&gt;

&lt;p&gt;This part is easy:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo apt-get install daemontools&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Done. Unfortunately, it won&apos;t start after a reboot, which is rather the point of a process supervisor. The &lt;code&gt;daemontools-run&lt;/code&gt; package is supposed to handle startup, but it relies on the traditional init system, and Ubuntu uses Upstart instead.&lt;/p&gt;

&lt;h4&gt;Make Daemontools run at system startup&lt;/h4&gt;

&lt;p&gt;Create the file &lt;code&gt;/etc/event.d/svscanboot&lt;/code&gt; with the following content:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;start on runlevel 2
start on runlevel 3
start on runlevel 4
start on runlevel 5

stop on runlevel 0
stop on runlevel 1
stop on runlevel 6

respawn
exec /usr/bin/svscanboot&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You&apos;ll also need to create the service directory, since the Ubuntu-packaged version of Daemontools looks for service definitions here:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;mkdir /etc/service&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now tell Upstart to start the process:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo initctl start svscanboot&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Other distributions&lt;/h4&gt;

&lt;p&gt;Plenty of other distributions use Upstart instead of init, so the fix is similar. For Fedora Core 9 and later, see the &lt;a href=&quot;http://directory.fedoraproject.org/wiki/Howto:Daemontools#Daemontools_Installation&quot;&gt;Fedora Daemontools guide&lt;/a&gt; and &lt;a href=&quot;http://qmail.jms1.net/daemontools/upstart.shtml&quot;&gt;this Upstart configuration walkthrough&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Accepting Changes from a Remote Git Repository</title>
    <link href="/2008/11/21/accepting-changes-from-a-remote-git-repository/"/>
    <updated>2008-11-21T00:00:00+09:00</updated>
    <id>/2008/11/21/accepting-changes-from-a-remote-git-repository/</id>
    <content type="html">&lt;p&gt;Previously I wrote about how to &lt;a href=&quot;http://barkingiguana.com/2008/11/20/working-on-other-peoples-projects-with-git&quot;&gt;work on an external project using Git&lt;/a&gt;. What I didn&apos;t cover was the other side of the equation: how the project owner accepts those changes.&lt;/p&gt;

&lt;h4&gt;Connect to the remote repository&lt;/h4&gt;

&lt;p&gt;As a committer on the project, you&apos;ll already have the repository cloned. If you don&apos;t, now&apos;s a good time to sort that out.&lt;/p&gt;

&lt;p&gt;The person requesting a review should have given you a repository URL and probably a branch name. Add their repository as a remote:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git remote add \
  craigwebster http://barkingiguana.com/~craig/project_name.git&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Double-check that it&apos;s pointing to the correct place:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git remote show craigwebster
  * remote craigwebster
    URL: http://barkingiguana.com/~craig/project_name.git/
    New remote branches (next fetch will store in remotes/craigwebster)
      dev/sprozzled-some-gromits master&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Grab those branches:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git fetch craigwebster&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Review, critique, rinse, repeat&lt;/h4&gt;

&lt;p&gt;To look at the changes, check them out to a local branch. Ask Git to track the remote branch so that any future updates from the contributor can easily be pulled in:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git co --track \
  -b craigwebster-sprozzled-gromits-are-good \
  craigwebster/dev/sprozzled-some-gromits&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now do your thing: run the test suite, read through the code, discuss it with your peers, whatever your review process looks like.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git whatchanged
commit b9e0f1b4ff4bc196513c9551f6c25f0ee40d991f
Author: Craig R Webster &amp;lt;craig@xeriom.net&amp;gt;
Date:   Wed Nov 19 20:53:08 2008 +0000
# and so on&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I&apos;ll assume you&apos;re accepting the changes wholesale here. If you only want some of them, you&apos;ll need to cherry-pick individual commits.&lt;/p&gt;

&lt;h4&gt;Ask for a wider review&lt;/h4&gt;

&lt;p&gt;Sometimes it makes sense to get more eyes on a change before merging it into master. Maybe the change is too big for a minor release, or maybe it targets a development branch. In those cases, merge into the appropriate branch:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git checkout dev/version-2-0-45
git merge craigwebster-sprozzled-some-gromits
git commit -m \
  &quot;The Gromits are well and truly Sprozzled.&quot; \
  --author &quot;Craig R Webster &amp;lt;craig@xeriom.net&amp;gt;&quot;
git push origin \
  dev/version-2-0-45:refs/heads/dev/version-2-0-45&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;From here you can merge, rebase, or otherwise work with the commit just as you would with any other change.&lt;/p&gt;

&lt;h4&gt;Accepting the changes directly&lt;/h4&gt;

&lt;p&gt;If the change is ready to go straight into the master branch, that works too:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git checkout master
git merge craigwebster-sprozzled-some-gromits
git commit -m \
  &quot;The Gromits are well and truly Sprozzled.&quot; \
  --author &quot;Craig R Webster &amp;lt;craig@xeriom.net&amp;gt;&quot;
git push origin master&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Working on Other People's Projects with Git</title>
    <link href="/2008/11/20/working-on-other-peoples-projects-with-git/"/>
    <updated>2008-11-20T00:00:00+09:00</updated>
    <id>/2008/11/20/working-on-other-peoples-projects-with-git/</id>
    <content type="html">&lt;p&gt;I&apos;m still fairly new to Git, and I&apos;m not entirely sure what the accepted etiquette is for contributing patches to other people&apos;s projects. Here&apos;s the best approach I&apos;ve come up with for making changes to someone else&apos;s project and giving them the option to incorporate those changes.&lt;/p&gt;

&lt;h4&gt;Clone the repository&lt;/h4&gt;

&lt;p&gt;First, grab a copy of the project. Hopefully they&apos;re using Git; I haven&apos;t worked out a good workflow for when they&apos;re not.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone git://github.com/username/project_name.git
cd project_name.git&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Add a public repository&lt;/h4&gt;

&lt;p&gt;If you&apos;re like me and often work offline, you&apos;ll want a public repository where you can push your changes so others can access them. I &lt;a href=&quot;http://barkingiguana.com/2008/11/15/setting-up-a-public-git-repository&quot;&gt;set up a public Git repository&lt;/a&gt; for exactly this purpose. If you&apos;re always connected (or at least whenever another developer might want to pull your code), you can probably skip this step.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git remote add public ssh://barkingiguana.com/~craig/code/project_name.git
git push public master&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Time to work&lt;/h4&gt;

&lt;p&gt;Here comes the hard but interesting bit: actually doing the work. Typically this means checking out a branch for a feature, bug fix, or topic area.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git checkout -b sprozzle-the-gromits
# ... do the work ...
git add gromits/blue.txt
git commit -m &quot;Sprozzle Gromit with the blue face.&quot;

# ... do more work ...
git add gromits/cherry.txt
git commit -m &quot;Cherry Gromits are even better with more Sprozzle.&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Conflict resolution&lt;/h4&gt;

&lt;p&gt;While you&apos;ve been working on your patch (and until it&apos;s accepted back into the project), there may be upstream changes. You&apos;ll want to make sure your patch applies cleanly to the master branch, since that dramatically increases the chances it&apos;ll be accepted.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git checkout master
git pull origin master
git checkout sprozzle-the-gromits
git rebase master
# resolve any conflicts
git commit -m &quot;Made branch patch master at 351ac1b cleanly.&quot;
git push public&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Advertise your changes&lt;/h4&gt;

&lt;p&gt;Push just the changes on your branch to the public repository. Again, this is only necessary if you work offline and need others to be able to access your code independently.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git push public sprozzle-the-gromits:refs/heads/sprozzled-gromits&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Automation is awesome&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;http://www.sirena.org.uk/log/&quot;&gt;Mark Brown&lt;/a&gt; pointed out that you can use &lt;code&gt;git request-pull&lt;/code&gt; to generate a few paragraphs suitable for emailing to the project team, containing all the information needed for your changes to be reviewed and &lt;a href=&quot;http://barkingiguana.com/2008/11/21/accepting-changes-from-a-remote-git-repository&quot;&gt;merged into the project&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git request-pull \
  b9e0f1b4ff4bc196513c9551f6c25f0ee40d991f \
  http://barkingiguana.com/~craig/project_name.git&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;And relax...&lt;/h4&gt;

&lt;p&gt;Your changes are now available to the public. Anyone can clone your repository and fetch your pushed branches. Now would be a good time to email the project owner and ask nicely if they&apos;ll pull from your repository and review your changes.&lt;/p&gt;

&lt;p&gt;If you need to make further changes to the branch, just do the work, commit it, and run &lt;code&gt;git push public&lt;/code&gt; from the branch (or &lt;code&gt;git push public sprozzle-the-gromits&lt;/code&gt; from a different branch).&lt;/p&gt;

&lt;h4&gt;Difference is the spice of life&lt;/h4&gt;

&lt;p&gt;The project you want to contribute to may not support this style of collaboration. Check with the project team before you get started. If you&apos;d prefer not to (or can&apos;t) publish your own copy of the repository, the Git book covers &lt;a href=&quot;http://book.git-scm.com/5_git_and_email.html&quot;&gt;using Git and email&lt;/a&gt; as an alternative.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Symbol#to_proc is slow... is it slow enough to matter?</title>
    <link href="/2008/11/18/symbol-to_proc-is-slow-is-it-slow-enough-to-matter/"/>
    <updated>2008-11-18T00:00:00+09:00</updated>
    <id>/2008/11/18/symbol-to_proc-is-slow-is-it-slow-enough-to-matter/</id>
    <content type="html">&lt;p&gt;It’s common knowledge that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Symbol#to_proc&lt;/code&gt; trick is slower than writing out a block by hand. But just how much slower? I put together some benchmarks to find out.&lt;/p&gt;

&lt;h3 id=&quot;environment&quot;&gt;Environment&lt;/h3&gt;

&lt;p&gt;These tests were run on Ruby 1.8.6-pl111 and Rails 2.1.&lt;/p&gt;

&lt;h3 id=&quot;benchmarking&quot;&gt;Benchmarking&lt;/h3&gt;

&lt;p&gt;Say you have a database of 1,000 items that you need to iterate over. Let’s set aside the fact that displaying 1,000 items probably means you have usability problems, and just roll with it.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;mi&quot;&gt;1_000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bar-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Here’s how the two approaches compare over 1,000 ActiveRecord instances:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;measure&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;real&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; 0.00645709037780762&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;measure&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;real&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; 0.00141692161560059&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That’s a horrific-sounding increase: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_proc&lt;/code&gt; takes more than 350% longer than the plain block. But let’s be realistic: over 1,000 records, the total time is 0.0065 seconds. Not exactly something to lose sleep over.&lt;/p&gt;

&lt;p&gt;What about 1,000,000 rows? We already have 1,000, so let’s top it up:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1_000_000&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1_000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That gives us a million rows. By this point your database is probably questioning your life choices. Presenting a million rows to a user is a bit of an edge case, but here’s how long it takes:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;measure&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;real&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; 6.25304508209229&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;measure&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;real&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; 1.38965106010437&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Almost 5 extra seconds over a million rows. Five seconds is a real hit, sure, but how long will your application be running before you hit a million rows in a single table &lt;em&gt;and&lt;/em&gt; need to iterate over every last one of them?&lt;/p&gt;

&lt;p&gt;Don’t optimise prematurely. By the time &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_proc&lt;/code&gt; becomes your bottleneck, you’ll have hit many other problems first:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;measure&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;real&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; 406.738657951355&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Worry about those first.&lt;/p&gt;

&lt;h3 id=&quot;run-it-yourself&quot;&gt;Run it yourself&lt;/h3&gt;

&lt;p&gt;It’s been a long time since I ran the original benchmark. Here’s some copy-paste code to run a similar one yourself:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;benchmark&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PLATFORM = &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RUBY_PLATFORM&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, VERSION = &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RUBY_VERSION&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bmbm&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;to_proc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10_000_000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:to_s&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;literal 1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10_000_000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;literal 2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10_000_000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Here are the results from my MacBook Air on Ruby 2.1.2, and they tell a rather interesting story:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    Rehearsal ---------------------------------------------
    to_proc     1.890000   0.010000   1.900000 (  1.909775)
    literal 1   2.340000   0.000000   2.340000 (  2.350912)
    literal 2   2.270000   0.000000   2.270000 (  2.274322)
    ------------------------------------ total: 6.510000sec

    user     system      total        real
    to_proc     1.810000   0.000000   1.810000 (  1.808921)
    literal 1   2.090000   0.000000   2.090000 (  2.092189)
    literal 2   2.060000   0.010000   2.070000 (  2.061436)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Handling Error Feedback from Ajax Requests to Rails Applications</title>
    <link href="/2008/11/17/handling-error-feedback-from-ajax-requests-to-rails-applications/"/>
    <updated>2008-11-17T00:00:00+09:00</updated>
    <id>/2008/11/17/handling-error-feedback-from-ajax-requests-to-rails-applications/</id>
    <content type="html">&lt;p&gt;Ajax is frequently used to deliver a richer user experience. So why are error messages so rarely handled properly in Ajax-enabled applications? Handling errors gracefully (in a way that actually helps the visitor fix the problem) adds a genuinely high-quality feel. We&apos;ve already got all the machinery we need. It just takes a little care and attention.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;class FoosController &amp;lt; ApplicationController
  def update
    @foo = Foo.find(params[:id])
    respond_to do |format|
      if @foo.save
        format.html do
          flash[:info] = &quot;Your foo has been created.&quot;
          redirect_to @foo
        end
        format.js { head :ok }
      else
        format.html do
          flash.now[:warning] = &quot;I could not update the foo.&quot;
          render :action =&amp;gt; :edit
        end
        format.json do
          head :unprocessable_entity, :json =&amp;gt; @foo.errors.to_json
        end
      end
    end
  end
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With this controller, you get a solid fallback for standard HTML requests and clean JSON behaviour for Ajax. When something goes wrong on a JSON request, you get back an array of arrays that looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;[
  [ &quot;attribute1&quot;, &quot;error1&quot;, &quot;error2&quot; ],
  [ &quot;attribute2&quot;, &quot;error3&quot; ]
]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Think of the things you can do with that kind of structured feedback:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;new Ajax.Request(&apos;/foo.json&apos;, {
  method: &apos;PUT&apos;,
  parameters: {
    authenticity_token: window._token,
    &quot;foo[subject]&quot;: $F(&apos;foo_subject&apos;),
    &quot;foo[body]&quot;   : $F(&apos;foo_body&apos;)
  },
  onSuccess: function(transport) {
    // This is Web 2.0: celebrate with a yellow highlight.
  },
  onFailure: function(transport) {
    var errors = transport.responseJSON;
    errors.each(function(error) {
      var attribute = error.shift();
      var messages = error.join(&quot;, &quot;);
      var errorMessage = attribute + &quot; &quot; + messages;
      var inputNode = $(&quot;foo_&quot; + attribute);
      if(inputNode) {
        // Show that something is wrong with this field.
        inputNode.addClassName(&quot;error&quot;);
        // Do something better than an alert box. Alert boxes suck.
        alert(errorMessage);
      }
    });
  }
});&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Ajax and the Rails Request Authenticity Token</title>
    <link href="/2008/11/17/ajax-and-the-rails-request-authenticity-token/"/>
    <updated>2008-11-17T00:00:00+09:00</updated>
    <id>/2008/11/17/ajax-and-the-rails-request-authenticity-token/</id>
    <content type="html">&lt;p&gt;Rails 1.2.6 introduced &lt;a href=&quot;http://en.wikipedia.org/wiki/Cross-site_request_forgery&quot;&gt;&lt;abbr title=&quot;Cross-Site Request Forgery&quot;&gt;CSRF&lt;/abbr&gt;&lt;/a&gt; protection in the form of an authenticity token, a reasonably long string that ensures any PUT, POST, or DELETE request to your application was genuinely triggered by you (or at least your browser) and not by some nefarious third party.&lt;/p&gt;

&lt;p&gt;Rails automatically adds this token to any form generated by its helpers. But when you&apos;re building rich Ajax interactions, you sometimes need to construct the requests by hand.&lt;/p&gt;

&lt;p&gt;Drop this snippet into your layout, just above where you include the rest of your JavaScript files, and you&apos;ll have the authenticity token available from JavaScript:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;%= javascript_tag &quot;window._token = &apos;#{form_authenticity_token}&apos;;&quot; %&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now you can build Ajax requests that the application will actually accept:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;new Ajax.Request(&apos;/foo.json&apos;, {
  method: &apos;PUT&apos;,
  parameters: {
    authenticity_token: window._token,
    text: $F(&apos;foo_text&apos;)
  }
  /* callbacks omitted for brevity */
})&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Writing a Story: Why, When, Where, Who, What, How, and a Bunch of Other Questions and Answers</title>
    <link href="/2008/11/16/writing-a-story-why-when-where-who-what-how-and-a-bunch-of-other-questions-and-answers/"/>
    <updated>2008-11-16T00:00:00+09:00</updated>
    <id>/2008/11/16/writing-a-story-why-when-where-who-what-how-and-a-bunch-of-other-questions-and-answers/</id>
    <content type="html">&lt;p&gt;Making the shift to story-driven development can be a real head-scratcher. What should a story contain? Who should be involved in writing one? Here are some guidelines to help you get started.&lt;/p&gt;

&lt;p&gt;I&apos;m assuming below that you&apos;re following &lt;a href=&quot;http://en.wikipedia.org/wiki/Scrum_(development)&quot;&gt;Scrum&lt;/a&gt; or something Scrum-like. If you&apos;re using a different Agile methodology, most of this should translate without much trouble. If you&apos;re stuck with Waterfall or RUP, you have my sympathies. I&apos;m honestly not sure how well story-driven development fits outside the Agile world.&lt;/p&gt;

&lt;h4&gt;Why write stories?&lt;/h4&gt;

&lt;p&gt;A Product Owner rarely cares that you&apos;ve added a button to submit an order, not unless the code to process the order, take payment, and write it to the database is also there. They care about being able to &lt;em&gt;place an order&lt;/em&gt;, not about how the ordering system was implemented.&lt;/p&gt;

&lt;p&gt;Stories form a complete, deliverable unit of work. They give you a way to communicate project progress to the business in terms the business actually understands.&lt;/p&gt;

&lt;p&gt;Stories also make it easier to commit to work for a sprint: you can estimate the complexity of a feature and, based on that, the team can tell whether they can realistically finish the story in the current sprint.&lt;/p&gt;

&lt;p&gt;Stories generate conversations. They help specify exactly how a feature should behave, so the team knows what they&apos;re aiming for.&lt;/p&gt;

&lt;p&gt;And stories help you focus. If the team has committed to delivering a story about placing an order, they&apos;re not going to wander off and build a user feedback system. (And if they do, they can be gently steered back to the goal they committed to during sprint planning.)&lt;/p&gt;

&lt;h4&gt;When should a story be written?&lt;/h4&gt;

&lt;p&gt;Feature requests arrive constantly, so it&apos;s useful to have a regular meeting for writing and estimating stories. I suggest a short session at the end of each sprint to handle the work that arrived during that sprint. This meeting will typically last less than an hour.&lt;/p&gt;

&lt;p&gt;At project kick-off, you&apos;ll have more features to estimate than usual. Plan two or three meetings of one to two hours each to get through the initial backlog.&lt;/p&gt;

&lt;p&gt;It&apos;s always handy to have more stories ready than just what&apos;s in the current sprint; if the team finishes early, they can pull in additional work.&lt;/p&gt;

&lt;p&gt;But try not to overdo it. Writing stories is valuable, but working software is more important.&lt;/p&gt;

&lt;h4&gt;Where should stories be written?&lt;/h4&gt;

&lt;p&gt;Nothing complicated here: you want somewhere you can focus with the Product Owner without interruptions. Find a quiet room away from the work area, or head to a coffee shop.&lt;/p&gt;

&lt;h4&gt;Who should write a story?&lt;/h4&gt;

&lt;p&gt;Short answer: everyone. The Product Owner, Scrum Master, and the Scrum Team.&lt;/p&gt;

&lt;h4&gt;How should a story be written?&lt;/h4&gt;

&lt;p&gt;The team talks about the product and identifies a specific piece of functionality to work on (say, the ordering system mentioned above). The Product Owner, Scrum Master, and Scrum Team then define a set of scenarios that detail how that functionality should behave: What happens when the store is closed? What if someone enters an invalid credit card number? The list doesn&apos;t need to be exhaustive; it just needs to be representative.&lt;/p&gt;

&lt;p&gt;The scenarios and the feature description are captured in a document. This can take many forms, but here&apos;s how I write them:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Feature: Place an order
  In order to get goods from our online store
  A shopper
  Should be able to place and pay for an order

  Scenario: The store is closed
    Given the store is closed
    And I have three beachballs in my shopping cart
    When I submit my order
    Then the order should be accepted
    And I should see &quot;Your order will be processed when the store opens at 9am&quot;

  Scenario: An invalid credit card number is used
    Given I have three beachballs in my shopping cart
    When I fill in &quot;credit_card_number&quot; with &quot;MONKEY&quot;
    And I press &quot;Pay&quot;
    Then the order should not be accepted
    And I should see &quot;Please enter a valid credit card number&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once the story is written, everyone on the team except the Product Owner estimates its complexity. Based on current knowledge, they assign a point score that shows how complex it is relative to other stories. It helps to use a fixed scale; something resembling the Fibonacci sequence works well. I use ?, 0, 1, 2, 3, 5, 8, 13, 20, 40, 100, and infinity. Zero means trivial. Infinity means the team thinks they could never complete it. A ? means they don&apos;t have enough information yet; it might be estimable after more discussion or a short, time-boxed &lt;a href=&quot;http://www.extremeprogramming.org/rules/spike.html&quot;&gt;development spike&lt;/a&gt;. One of the best ways to run estimation is to play &lt;a href=&quot;http://www.planningpoker.com/detail.html&quot;&gt;planning poker&lt;/a&gt;. I have a set of &lt;a href=&quot;http://store.mountaingoatsoftware.com/&quot;&gt;planning poker cards&lt;/a&gt; for this.&lt;/p&gt;

&lt;p&gt;It&apos;s also useful for the Product Owner to assign a business value to each story, even though business value is notoriously hard to quantify. I suggest values of 100, 200, 300, 400, 500, 600, 700, 800, 900, or 1000 to rank stories relative to each other. The Product Owner shouldn&apos;t be influenced by how complex the team thinks a story is; they&apos;re rating the value to the business of delivering a capability, regardless of the effort involved.&lt;/p&gt;

&lt;p&gt;For both complexity and business value, there are no in-between values. Don&apos;t allow estimates of 25 if it isn&apos;t on your scale, or you&apos;ll spend forever arguing whether something is a 24 or a 25. It&apos;s an &lt;em&gt;estimate&lt;/em&gt;. It doesn&apos;t need to be exact.&lt;/p&gt;

&lt;p&gt;Business value and complexity can be revised whenever new information surfaces, so it&apos;s worth briefly reviewing existing unimplemented stories while writing and estimating new ones.&lt;/p&gt;

&lt;p&gt;After a story is written, it goes into the product backlog.&lt;/p&gt;

&lt;h4&gt;What happens to the story after it&apos;s added to the product backlog?&lt;/h4&gt;

&lt;p&gt;During the next sprint planning meeting, the Product Owner, Scrum Master, and Scrum Team meet to set a goal for the upcoming sprint. This goal is what the sprint&apos;s success will be measured against.&lt;/p&gt;

&lt;p&gt;After setting the goal, the team discusses which stories contribute towards it and decides what they can commit to delivering, based on the complexity estimates. Stories with high business value should be preferred over those with low business value; the aim is to deliver the most value possible each sprint. There may be some negotiation with the Product Owner if they&apos;d prefer certain stories over others, but the team shouldn&apos;t be pressured into taking on more than they can handle.&lt;/p&gt;

&lt;p&gt;How much complexity a team can handle in a sprint should be based on how previous sprints went. Every team estimates differently and has different strengths, so this will vary widely. During the first sprint, pick a sensible but somewhat arbitrary number of stories and see how it goes. If the team finishes early, they can always pull in more work.&lt;/p&gt;

&lt;h4&gt;How do I know when a feature is complete?&lt;/h4&gt;

&lt;p&gt;Since a story represents a feature, the feature is complete when you can do exactly what the story describes. Try walking through it yourself. When you can follow every scenario in the story, consider the feature done.&lt;/p&gt;

&lt;p&gt;If you&apos;re using Rails or Ruby, check out my article on &lt;a href=&quot;http://barkingiguana.com/2008/11/11/getting-started-with-story-driven-development-for-rails-with-cucumber&quot;&gt;story-driven development using Cucumber&lt;/a&gt;, which shows how to turn a story into an automated test.&lt;/p&gt;

&lt;h4&gt;What happens if a story doesn&apos;t get completed during a sprint?&lt;/h4&gt;

&lt;p&gt;Scrum is all about delivering working software, so if a story isn&apos;t complete, it shouldn&apos;t be part of the sprint deliverable. If your developers are working in a &lt;a href=&quot;http://svnbook.red-bean.com/en/1.1/ch04s04.html#svn-ch-4-sect-4.4.2&quot;&gt;feature-branch&lt;/a&gt; pattern, this is straightforward: just don&apos;t merge the incomplete feature into the &lt;a href=&quot;http://svnbook.red-bean.com/en/1.1/ch04s04.html#svn-ch-4-sect-4.4.1&quot;&gt;release branch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The work that&apos;s been done doesn&apos;t necessarily get thrown away, though. It can be used to reduce the story&apos;s complexity estimate for the next sprint. Just bear in mind that this reduction is somewhat time-limited; as development continues, the cost of keeping a feature branch up to date with trunk starts to add up.&lt;/p&gt;

&lt;h4&gt;What happens if a story is too complex for one sprint?&lt;/h4&gt;

&lt;p&gt;Stories that contain more complexity than the team can handle in a single sprint are called Epics. These can&apos;t be accepted for a sprint because they wouldn&apos;t get finished, and the sprint deliverable would show no progress. We should always show progress.&lt;/p&gt;

&lt;p&gt;Epics should be discussed with the Product Owner. They often describe more than one feature and can be broken down into smaller stories, each deliverable within a single sprint.&lt;/p&gt;

&lt;p&gt;Running into Epics is completely normal over the course of a project.&lt;/p&gt;

&lt;h4&gt;Any other questions?&lt;/h4&gt;

&lt;p&gt;The above covers the questions I&apos;ve been asking myself over the past few days. If you have others, please ask in the comments or &lt;a href=&quot;mailto:craig@xeriom.net&quot;&gt;email me&lt;/a&gt; and I&apos;ll do my best to find an answer.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Setting Up a Public Git Repository</title>
    <link href="/2008/11/15/setting-up-a-public-git-repository/"/>
    <updated>2008-11-15T00:00:00+09:00</updated>
    <id>/2008/11/15/setting-up-a-public-git-repository/</id>
    <content type="html">&lt;p&gt;I&apos;ve been using Git more and more as my version control system, and I wanted to make some code available to the public. The easy option would be a hosted service like &lt;a href=&quot;http://github.com/&quot;&gt;GitHub&lt;/a&gt; or &lt;a href=&quot;http://repo.or.cz/&quot;&gt;repo.or.cz&lt;/a&gt;, but I&apos;m vain enough to want to serve my code from &lt;a href=&quot;http://barkingiguana.com/&quot;&gt;barkingiguana.com&lt;/a&gt;. I don&apos;t need multiple committers, and I want to learn more about how Git works under the hood, so &lt;a href=&quot;http://eagain.net/gitweb/?p=gitosis.git;a=summary&quot;&gt;Gitosis&lt;/a&gt; would be overkill. It turns out that setting up your own public repository is pretty straightforward. Here&apos;s how I did it.&lt;/p&gt;

&lt;h4&gt;General setup&lt;/h4&gt;

&lt;p&gt;I already have Apache running (serving this blog, among other things), so I&apos;ll use that and serve code from repositories under &lt;a href=&quot;http://barkingiguana.com/~craig/&quot;&gt;http://barkingiguana.com/~craig/&lt;/a&gt;. The easiest way is to use &lt;a href=&quot;http://httpd.apache.org/docs/2.2/mod/mod_userdir.html&quot;&gt;mod_userdir&lt;/a&gt;. On Ubuntu, enabling it is trivial:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo a2enmod userdir
sudo /etc/init.d/apache2 restart&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I want to keep my Git repositories under &lt;code&gt;~/code&lt;/code&gt;, which lets me selectively symlink in only the repositories I want to be public:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;mkdir ~/code&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;My VM&apos;s SSH port is on a non-standard port, so I configured that in &lt;code&gt;~/.ssh/config&lt;/code&gt; on my local machine. I also took the opportunity to upload my SSH key.&lt;/p&gt;

&lt;h4&gt;Publishing a project&lt;/h4&gt;

&lt;p&gt;I have a project with some work already done locally that I&apos;d like to share. First, create a bare repository on the public server:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# On the public server
mkdir -p ~/code/&lt;em&gt;project_name&lt;/em&gt;.git
cd ~/code/&lt;em&gt;project_name&lt;/em&gt;.git
git --bare init
chmod +x hooks/post-update&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Success looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Initialized empty Git repository in /home/&lt;em&gt;user_name&lt;/em&gt;/code/&lt;em&gt;project_name&lt;/em&gt;/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, on your local machine, add the public server as a remote:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# On the local development machine
cd ~/sandbox/&lt;em&gt;project_name&lt;/em&gt;
git remote add public ssh://barkingiguana.com/~/code/&lt;em&gt;project_name&lt;/em&gt;.git&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now push the local master branch up:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# On the local development machine
cd ~/sandbox/&lt;em&gt;project_name&lt;/em&gt;
git push public master&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The code is on the public server now, and you can push future changes with &lt;code&gt;git push public master&lt;/code&gt;. But it still isn&apos;t web-accessible since it&apos;s not in &lt;code&gt;~/public_html&lt;/code&gt;. Fix that with a symlink:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ln -s ~/code/&lt;em&gt;project_name&lt;/em&gt;.git ~/public_html/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Just like that, the repository is available for public use.&lt;/p&gt;

&lt;h4&gt;Did it work?&lt;/h4&gt;

&lt;p&gt;To verify everything is in order, try cloning the repository. Replace the URL with wherever your repository lives:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# On the local development machine
mkdir ~/tmp/
cd ~/tmp/
git clone http://barkingiguana.com/~craig/addressbook.git&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Success should look something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Initialized empty Git repository in /Users/craig/tmp/addressbook/.git/
got d0cc5f06e1d164ea6ada301dbd2e7c946d1ae532
walk d0cc5f06e1d164ea6ada301dbd2e7c946d1ae532
got b68d1319a780a776afdb60e3bba2985793a11f3e
got 2baa33597deecfc3eb558c59bc69745e153f9b82
got da7110115566b026c7316bd1be4cbf3d76c0f656&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Get the Current Git Branch in Your Command Prompt</title>
    <link href="/2008/11/15/get-the-current-git-branch-in-your-command-prompt/"/>
    <updated>2008-11-15T00:00:00+09:00</updated>
    <id>/2008/11/15/get-the-current-git-branch-in-your-command-prompt/</id>
    <content type="html">&lt;p&gt;It seems like everyone and their dog has their own way to show the current Git branch in the command prompt. Here&apos;s mine. Drop this into your &lt;code&gt;~/.profile&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export PS1=&apos;\[\033[01;32m\]\u@\h\[\033[00m\] \[\033[01;34m\]\w\[\033[00m\]$(git branch &amp;amp;&amp;gt;/dev/null; if [ $? -eq 0 ]; then echo &quot;\[\033[01;33m\]($(git branch | grep ^*|sed s/\*\ //))\[\033[00m\]&quot;; fi)$ &apos;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The result looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code style=&quot;background: black; padding: 0.5em;&quot;&gt;&lt;span style=&quot;color: green;&quot;&gt;craig@shiny&lt;/span&gt; &lt;span style=&quot;color: blue;&quot;&gt;~/sandbox/addressbook&lt;/span&gt;&lt;span style=&quot;color: yellow;&quot;&gt;(master)&lt;/span&gt;&lt;span style=&quot;color: green;&quot;&gt;$&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Now with 50% cleaner code&lt;/h4&gt;

&lt;p&gt;Shortly after posting this, I discovered that Git ships with an auto-completion file that includes a handy &lt;code&gt;__git_ps1&lt;/code&gt; function. If you enable &lt;a href=&quot;http://blog.ericgoodwin.com/2008/4/10/auto-completion-with-git&quot;&gt;Git auto-completion&lt;/a&gt;, you can get the same prompt with much less noise, and pick up some useful tab-completion goodies along the way:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export PS1=&apos;\[\033[01;32m\]\u@\h\[\033[00m\] \[\033[01;34m\]\w\[\033[00m\]$(__git_ps1 &quot;\[\033[01;33m\](%s)\[\033[00m\]&quot;)$ &apos;&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Getting Started with Story Driven Development for Rails with Cucumber</title>
    <link href="/2008/11/11/getting-started-with-story-driven-development-for-rails-with-cucumber/"/>
    <updated>2008-11-11T00:00:00+09:00</updated>
    <id>/2008/11/11/getting-started-with-story-driven-development-for-rails-with-cucumber/</id>
    <content type="html">&lt;p&gt;I&apos;d been hearing about Story Driven Development (SDD) for a while but kept putting it off, assuming there was a huge amount to learn and set up before I could get going. Turns out that was completely wrong. I started using Cucumber yesterday and it was surprisingly easy to get rolling.&lt;/p&gt;

&lt;h4&gt;Install and configure&lt;/h4&gt;

&lt;p&gt;First, install the required gems:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo gem install nokogiri term-ansicolor treetop diff-lcs hpricot cucumber&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then install Cucumber into your Rails app:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ruby script/generate cucumber&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, install Webrat. Unfortunately it&apos;s not available as a gem at this point. If you&apos;re using Git, install it as a submodule. If not, clone the repository and &lt;code&gt;svn add&lt;/code&gt; it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone git://github.com/brynary/webrat.git vendor/plugins/webrat&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Writing your first story&lt;/h4&gt;

&lt;p&gt;Stories have three components: the business value being delivered, the role of the person using the feature, and a description of what the feature does.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;In order to [do something with business value]
As [role]
Should [describe the feature]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For example, imagine you&apos;re building an online ordering system for a pizza delivery company:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Feature: Order Pizza
  In order to get some hot, tasty pizza
  A hungry pizza lover
  Should be able to order pizza&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now you need some scenarios, specific things that can happen during the story. Most pizza places aren&apos;t open 24 hours, so two obvious scenarios are: the shop is closed, and the shop is open.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  Scenario: The pizza shop is closed
    Given the pizza shop is closed
    And I am on the home page
    And I click &quot;Feed Me!&quot;
    Then I should see &quot;Sorry, the shop is closed&quot;

  Scenario: The pizza shop is open
    Given the pizza shop is open
    And I am on the home page
    And I click &quot;Feed Me!&quot;
    Then I should see &quot;Your pizza will be with you soon&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Save this in a file like &lt;code&gt;features/order_pizza.feature&lt;/code&gt;, where it can live happily under version control.&lt;/p&gt;

&lt;p&gt;So now you have a story that describes how a feature should behave. But how does it become an actual test? You could hand these descriptions to a testing team, or you could wire them up as part of your automated test suite.&lt;/p&gt;

&lt;h4&gt;Automated tests: better than cake&lt;/h4&gt;

&lt;p&gt;When you installed Cucumber, you got a &lt;code&gt;features/steps&lt;/code&gt; directory. This is where you teach your test suite how to understand your stories. There are already two files in there: &lt;code&gt;common_webrat.rb&lt;/code&gt;, which gives you useful abilities like clicking links, and &lt;code&gt;env.rb&lt;/code&gt;, which does essentially the same job as &lt;code&gt;spec/spec_helper.rb&lt;/code&gt; but for Cucumber. You can mostly ignore &lt;code&gt;env.rb&lt;/code&gt;, but &lt;code&gt;common_webrat.rb&lt;/code&gt; is worth reading for examples of how to write step definitions.&lt;/p&gt;

&lt;p&gt;Create a new file called &lt;code&gt;order_pizza_steps.rb&lt;/code&gt;. This is where you define the steps involved in ordering pizza. Each step is just a regular expression that maps a line from your scenario to some Ruby code:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;ruby&quot;&gt;Given /the pizza shop is open/ do
  PizzaShop.open = true
end

Given /the pizza shop is closed/ do
  PizzaShop.open = false
end

And /I am on the home page/ do
  visits &quot;/&quot;
end&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&apos;s it. The common Webrat steps already handle clicking buttons and checking for text on the page.&lt;/p&gt;

&lt;h4&gt;Running your stories&lt;/h4&gt;

&lt;p&gt;Just run &lt;code&gt;rake features&lt;/code&gt;. You&apos;ll get nicely coloured output, and if anything goes wrong, Cucumber is genuinely helpful about suggesting ways to fix it.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>content_for is the new GOTO</title>
    <link href="/2008/11/06/content_for-is-the-new-goto/"/>
    <updated>2008-11-06T00:00:00+09:00</updated>
    <id>/2008/11/06/content_for-is-the-new-goto/</id>
    <content type="html">&lt;p&gt;I have a confession: I really don&apos;t like &lt;code&gt;content_for&lt;/code&gt;. When you use it, your view code starts jumping around between files in a way that&apos;s genuinely hard to follow. It smells a lot like GOTO. And when was the last time anyone recommended you use a GOTO?&lt;/p&gt;

&lt;h4&gt;content_for :javascript and content_for :css&lt;/h4&gt;

&lt;p&gt;The good news is that &lt;code&gt;content_for&lt;/code&gt; can be avoided entirely, at least when it comes to including CSS and JavaScript. The trick is simple: include the controller name and action name in your layout&apos;s &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag, then scope your CSS declarations accordingly.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;rails-html&quot;&gt;&amp;lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Strict//EN&quot;
                         &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd&quot;&amp;gt;
&amp;lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;title&amp;gt;&amp;lt;%= page_title %&amp;gt;&amp;lt;/title&amp;gt;
  &amp;lt;meta http-equiv=&quot;Content-Language&quot; content=&quot;English&quot; /&amp;gt;
  &amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot; /&amp;gt;
  &amp;lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;/stylesheets/simple.css&quot; media=&quot;screen&quot; /&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body id=&quot;&amp;lt;%= &quot;#{controller.controller_name.tableize.singularize}_#{controller.action_name}&quot; %&amp;gt;&quot; class=&quot;&amp;lt;%= &quot;#{controller.controller_name.tableize.singularize} #{controller.action_name}&quot; %&amp;gt;&quot;&amp;gt;
  &amp;lt;%= yield %&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, say you&apos;re looking at the Posts views in your app. You can style each action independently, like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;css&quot;&gt;.post.index .article .title {
  font-size: 1.25em;
}

.post.show .article .title {
  font-size: 0.9em;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you need to support browsers that don&apos;t handle two classes as a selector on a single element, use the ID-based version instead:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;css&quot;&gt;#post_index .article .title {
  font-size: 1.25em;
}

#post_show .article .title {
  font-size: 0.9em;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Since all your JavaScript is unobtrusive anyway (right?), you can scope it with the same CSS selectors shown above.&lt;/p&gt;

&lt;p&gt;As a bonus, this approach lets you bundle all your JavaScript and CSS into single files for production, saving a bunch of HTTP requests. No &lt;code&gt;content_for&lt;/code&gt; required.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Make Sure You're @importing Files That Exist</title>
    <link href="/2008/11/03/make-sure-youre-importing-files-that-exist/"/>
    <updated>2008-11-03T00:00:00+09:00</updated>
    <id>/2008/11/03/make-sure-youre-importing-files-that-exist/</id>
    <content type="html">&lt;p&gt;I’ve started grumbling about optimising the number of HTTP requests per page. There are plenty of reasons you might want to do this, but that discussion is for another post. For now, just know that I don’t like unnecessary HTTP requests. And I &lt;em&gt;really&lt;/em&gt; don’t like wasted ones, like when a CSS &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@import&lt;/code&gt; directive points at a file that 404s.&lt;/p&gt;

&lt;p&gt;I got tired of tracking these down manually across several applications, so I threw together this little Ruby script to do the detective work for me:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env ruby&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;css_root&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;expand_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sb&quot;&gt;`pwd`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;css_files&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;css_root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;**&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;*.css&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;missing_imports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;css_files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;css_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;imports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;css_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/\n|\r/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;grep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/\@import url\((.*)\)/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;imports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;desired_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/url\(([&quot;&apos;\ ])?(.*)\1\)/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;last&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;desired_root&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;desired_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;css_root&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;css_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;filesystem_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;expand_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;desired_root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;desired_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exists?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filesystem_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;missing_imports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;css_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filesystem_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:directive&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;missing_imports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Missing files declared as imports in CSS:&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;missing_imports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;origin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Origin:               &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;origin&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;missing_imports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Missing @import file: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Directive:            &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:directive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;No imported files are missing. Well done.&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Run it from the directory that serves as your document root. For Rails apps, that’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RAILS_ROOT/public/&lt;/code&gt;. It’ll either spit out a list of broken imports or give you a pat on the back:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Missing files declared as imports in CSS:

Origin:               /Users/craig/projects/1.8/public/stylesheets/.../find_by_service.css
Missing @import file: /Users/craig/projects/1.8/public/stylesheets/.../a_to_z.css
Directive:            @import url(&apos;.../a_to_z.css&apos;);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To be clear: I’d prefer &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@import&lt;/code&gt; directives didn’t exist at all. Each one is an extra HTTP request that could have been avoided by combining stylesheets. But they’re popular with a lot of people, so I’ll compromise: if you must use them, at least make sure they point at files that actually exist.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Scaling: Using MogileFS for Storing Uploaded Images</title>
    <link href="/2008/10/31/scaling-using-mogilefs-for-storing-uploaded-images/"/>
    <updated>2008-10-31T00:00:00+09:00</updated>
    <id>/2008/10/31/scaling-using-mogilefs-for-storing-uploaded-images/</id>
    <content type="html">&lt;p&gt;As you might have guessed from several of my previous posts, the team I’ve been working in has recently been scaling an application. I’ve learned a bunch of things along the way, and I’ve got half-written articles about several of them that I’ll totally finish one day.&lt;/p&gt;

&lt;p&gt;One of the most useful technologies I’ve started using is &lt;a href=&quot;http://www.danga.com/mogilefs/&quot;&gt;MogileFS&lt;/a&gt;, a distributed BLOB store. In our application we use it to store user-generated assets like uploaded images and syndication feeds. Rather than go into the pros and cons here, I’d like to share some code that’s been genuinely useful: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MogileFilesystemBackend&lt;/code&gt; for &lt;a href=&quot;http://github.com/technoweenie/attachment_fu/tree/master&quot;&gt;AttachmentFu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Why do you need a shared filestore for uploads? Once your application cluster scales beyond a single box, uploaded images land on different disks depending on which server handled the request. Without a shared store, there’s no guarantee a particular image will be available to a subsequent request that hits a different server.&lt;/p&gt;

&lt;h4 id=&quot;getting-stuck-in&quot;&gt;Getting stuck in&lt;/h4&gt;

&lt;p&gt;I’ve done some admittedly ugly preparation here and monkey-patched &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Kernel&lt;/code&gt; to provide an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attr_accessor&lt;/code&gt; called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filestore&lt;/code&gt;, just an instance of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MogileFS::MogileFS&lt;/code&gt; from the excellent &lt;a href=&quot;http://seattlerb.rubyforge.org/mogilefs-client/&quot;&gt;MogileFS client&lt;/a&gt; by the folks at &lt;a href=&quot;http://seattlerb.rubyforge.org/&quot;&gt;Seattle RB&lt;/a&gt;. The patch, which will probably make experienced Rubyists wince, looks like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Kernel&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Oh noes, I&apos;m screwing with Kernel.&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;mattr_accessor&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:filestore&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;During Rails initialisation, the filestore is set up using configuration values pulled from a YAML file in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RAILS_ROOT/config/&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Kernel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filestore&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MogileFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MogileFS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;:domain&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;APPNAME-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RAILS_ENV&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;:hosts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;array_of_hosts_from_yaml_file&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(What I actually do is quite a bit different from this because I’ve done evil things to the MogileFS client library, which I’ll probably share in the future. For now, believe the magic.)&lt;/p&gt;

&lt;p&gt;With the setup complete, getting AttachmentFu to work with MogileFS is straightforward:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Image&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_attachment&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:content_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:storage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:mogile_filesystem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:max_size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;megabytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:thumbnails&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;:canonical&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;1024x&apos;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;:processor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MiniMagick&quot;&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;validates_as_attachment&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;the-backend&quot;&gt;The backend&lt;/h4&gt;

&lt;p&gt;Without the actual backend code, none of the above does anything. The implementation was heavily influenced by the existing Amazon S3 backend, since the concepts behind S3 and MogileFS are quite similar:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;MogileFilesystemBackend&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;full_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class_prefix&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filestore_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filestore_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:original&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;current_content&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;temp_path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;temp_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp_data&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;public_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;editorial_object_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;demodularize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tableize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;editorial_object_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class_prefix&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file_extension&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;?size=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;file_extension&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Mime&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_sym&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filestore_paths&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;filestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get_paths&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;full_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;file_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;filestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get_file_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;full_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;kp&quot;&gt;protected&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;current_content_location&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;temp_path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:temp_path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:temp_data&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;destroy_file&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;filestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;full_filename&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rename_file&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;filestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;rename&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@old_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;full_filename&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save_to_storage&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;info&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Storing &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\#&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; as &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;full_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; (class: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;replication_policy&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;) from &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current_content_location&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:temp_path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp_path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:memory&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;filestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;store_content&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;full_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;thumbnail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;replication_policy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_content&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;class_prefix&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;demodularize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;underscore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;downcase&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:replication_policy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:class_prefix&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Technoweenie&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;AttachmentFu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Backends&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MogileFilesystemBackend&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MogileFilesystemBackend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;serving-images&quot;&gt;Serving images&lt;/h4&gt;

&lt;p&gt;Getting images &lt;em&gt;into&lt;/em&gt; MogileFS is only half the story. You also need to serve them to visitors. Here’s a controller that reads from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filestore&lt;/code&gt; instead of the local filesystem (and if you’re storing files in the database, we need to have a talk):&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ImageController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;before_filter&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:load_image&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;show&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;respond_to&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;html&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:png&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:jpg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:gif&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;send_data&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;file_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;:type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;:disposition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;inline&apos;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;kp&quot;&gt;protected&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load_image&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@image&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And there you have it. Images go into MogileFS on upload, get replicated across your storage nodes, and are served back to visitors through a simple controller action. No more worrying about which app server has which file.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Talking to Yourself Is Bad, mmkay?</title>
    <link href="/2008/10/20/talking-to-yourself-is-bad-mmkay/"/>
    <updated>2008-10-20T00:00:00+08:00</updated>
    <id>/2008/10/20/talking-to-yourself-is-bad-mmkay/</id>
    <content type="html">&lt;p&gt;A lot of languages encourage talking to yourself. OO PHP code is sprinkled with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$this-&amp;gt;foo_method();&lt;/code&gt;. In some languages it’s necessary. Ruby isn’t one of them.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bar&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Why are you talking to yourself?!&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@thingy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;QUUX!&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.&lt;/code&gt; is doing absolutely nothing. You can drop it entirely:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bar&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@thingy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;foo&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;QUUX!&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is a trivial example, but it makes a real difference across a larger codebase. Less noise, easier to read, fewer characters to trip over. Give it a try, your code will look less like it’s having a conversation with itself.&lt;/p&gt;

&lt;p&gt;There’s one caveat though: you &lt;em&gt;do&lt;/em&gt; need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self&lt;/code&gt; when calling a setter method. Without it, Ruby thinks you’re assigning to a local variable:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;attr_accessor&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:thingy&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bar&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# This assigns to a local variable, NOT the attribute.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;thingy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;foo&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;QUUX!&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;attr_accessor&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:thingy&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bar&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# This calls Foo#thingy= as intended.&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;thingy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;foo&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;QUUX!&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So the rule is simple: skip &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self&lt;/code&gt; for reading, keep it for writing. Your future self (pun intended) will thank you.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Checking MySQL Database Sizes</title>
    <link href="/2008/10/09/checking-mysql-database-sizes/"/>
    <updated>2008-10-09T00:00:00+08:00</updated>
    <id>/2008/10/09/checking-mysql-database-sizes/</id>
    <content type="html">&lt;p&gt;Quick tip: want to know how large each of your MySQL 5 databases is? This query pulls the row counts, data size, index size, and total size from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;information_schema&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;mysql&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;table_schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table_rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;M&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data_length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;G&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index_length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;G&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data_length&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index_length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;G&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_size&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;information_schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TABLES&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;table_schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;-----------------------------+-------+-------+-------+------------+&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;table_schema&lt;/span&gt;                &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;rows&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;data&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;-----------------------------+-------+-------+-------+------------+&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;information_schema&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;00&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;G&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;00&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;G&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;00&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;G&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;xxxxxxxxx_xxxx_xxxx_staging&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;93&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;M&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;08&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;G&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;01&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;G&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;09&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;G&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;-----------------------------+-------+-------+-------+------------+&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;rows&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;03&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s one of those queries worth keeping in your back pocket. Handy for capacity planning, spotting unexpectedly large databases, or just satisfying your curiosity about where all that disk space went.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Fail Silently with Memcache Client</title>
    <link href="/2008/09/25/fail-silently-with-memcache-client/"/>
    <updated>2008-09-25T00:00:00+08:00</updated>
    <id>/2008/09/25/fail-silently-with-memcache-client/</id>
    <content type="html">&lt;p&gt;For web applications, &lt;a href=&quot;http://www.ukgeocachers.co.uk/catalog/cache-king-44mm-button-badge-p-391.html&quot;&gt;caching is king&lt;/a&gt;. I’ve recently been using &lt;a href=&quot;http://danga.com/memcached/&quot;&gt;memcached&lt;/a&gt; to cache expensive query results in a Rails application, with Seattle RB’s &lt;a href=&quot;http://seattlerb.rubyforge.org/memcache-client/&quot;&gt;memcache-client&lt;/a&gt; as the client library.&lt;/p&gt;

&lt;p&gt;The library is solid, but it has one opinion I disagree with: when a memcached instance fails, it throws an exception that your code has to handle. I think that’s the wrong default. When a cache fails, &lt;em&gt;it doesn’t matter&lt;/em&gt;. Either the application continues running uncached, slower, but functional, or other memcached instances pick up the slack. Neither scenario should require special handling in application code.&lt;/p&gt;

&lt;p&gt;Ruby, being awesome, lets me change the library’s behaviour easily. Monkey patching may be frowned upon, but it has its uses:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# A simple monkey-patch of MemCache so that broken memcached instances don&apos;t&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# cause fatal errors in the application. Performance may be severely degraded&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# but it should be possible to use the app anyway!&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# A typical use would look something like:&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   result = if cache.alive?&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     fetch = cache.get(:foo)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     if !fetch&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#       fetch = calculate(:foo)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#       cache.set(:foo, fetch)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     end&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     fetch&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   else&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     calculate(:foo)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   end&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MemCache&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Does the cache configuration contain any memcached instances that can&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# currently be used?&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Author: Conor Curran [http://forwind.net/]&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;alive?&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;servers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;detect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;alive?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Rescue from MemCache::MemCacheError -- we want the cache to fail silently&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# (at least from the point of view of the application - you should still&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# monitor memcached).&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_with_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;get_without_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MemCache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MemCacheError&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:get_without_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:get&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:get_with_rescue&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:[]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:get&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Rescue from MemCache::MemCacheError -- we want the cache to fail silently&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# (at least from the point of view of the application - you should still&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# monitor memcached).&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;set_with_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;set_without_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MemCache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MemCacheError&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set_without_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set_with_rescue&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:[]=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Rescue from MemCache::MemCacheError -- we want the cache to fail silently&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# (at least from the point of view of the application - you should still&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# monitor memcached).&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete_with_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;delete_without_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MemCache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MemCacheError&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:delete_without_rescue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:delete&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:delete_with_rescue&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The pattern is straightforward: wrap each method (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt;) with a version that rescues &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MemCacheError&lt;/code&gt; and silently returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alias_method&lt;/code&gt; chain preserves the original implementation so you can still call it directly if needed.&lt;/p&gt;

&lt;p&gt;A word of caution: “fail silently” doesn’t mean “ignore failures entirely.” You should absolutely still be monitoring your memcached instances. This patch just prevents a cache hiccup from becoming an application outage.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>LDAP Authentication in an Apache-Fronted Rails App</title>
    <link href="/2008/09/16/ldap-authentication-in-an-apache-fronted-rails-app/"/>
    <updated>2008-09-16T00:00:00+08:00</updated>
    <id>/2008/09/16/ldap-authentication-in-an-apache-fronted-rails-app/</id>
    <content type="html">&lt;p&gt;If you manage anything beyond the simplest of setups, you’ve probably got an LDAP server providing directory services to your network. If you don’t, this one probably isn’t for you.&lt;/p&gt;

&lt;h4 id=&quot;authenticate-using-ldap&quot;&gt;Authenticate using LDAP&lt;/h4&gt;

&lt;p&gt;The first step is getting Apache to authenticate all requests before they reach your Rails application. This is fiddly work, and Apache already has a rather lovely module – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mod_authnz_ldap&lt;/code&gt;, that handles the heavy lifting.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;VirtualHost 193.219.108.xxx:443&amp;gt;
  # I&apos;ve used port 443 above because I&apos;m dealing with passwords.
  # [...snip...]
  &amp;lt;Directory /var/www/foo.example.com/current/public&amp;gt;
    AuthType Basic
    AuthName &quot;Foo Application Control Panel&quot;
    AuthBasicAuthoritative off
    AuthBasicProvider ldap
    AuthLDAPUrl ldap://ldap.example.com/ou=people,dc=example,dc=com?userid?one
    Require valid-user
  &amp;lt;/Directory&amp;gt;
  # [...snip...]
  # Your normal Rails HTTP configuration goes here
&amp;lt;/VirtualHost&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;look-up-the-user-in-rails&quot;&gt;Look up the user in Rails&lt;/h4&gt;

&lt;p&gt;At this point, any request hitting your application has already been authenticated against your LDAP directory. Now you need Rails to identify the user. For this I wrote a mixin called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Xeriom::Acts::ProtectedSystem&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Xeriom&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# :nodoc:&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Acts&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# :nodoc:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ProtectedSystem&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# :nodoc:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;included&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:extend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ClassMethods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ClassMethods&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;acts_as_protected_system&lt;/span&gt;
          &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;InstanceMethods&lt;/span&gt;
          &lt;span class=&quot;nb&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:before_filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:ensure_user_is_logged_in&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;nb&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:helper_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:current_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;nb&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:helper_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:logged_in?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;InstanceMethods&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ensure_user_is_logged_in&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logged_in?&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;authenticate_user&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;logged_in?&lt;/span&gt;
          &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;blank?&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;current_user&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@current_user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_by_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;current_user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@current_user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;blank?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;authenticate_user&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;authenticate_or_request_with_http_basic&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Protected Area&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# Lock your application servers down to listen to only&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# the web tier or this will kick your ass.&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:current_user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_by_username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;ActionController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:include&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Xeriom&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Acts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ProtectedSystem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To use it, drop the code in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/&lt;/code&gt; directory, then call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;acts_as_protected_system&lt;/code&gt; in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApplicationController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;helper&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:all&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# include all helpers, all the time&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;protect_from_forgery&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# because CSRF sucks!&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;acts_as_protected_system&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# lock the door&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The key insight here is that Apache does the hard work of validating credentials against LDAP. Rails simply trusts the authenticated username and looks up the corresponding user record. Just make sure your application servers are locked down to only accept requests from the web tier, otherwise anyone could pass through a forged username.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Adventures in Erlang: Predicate Guards</title>
    <link href="/2008/09/10/adventures-in-erlang-predicate-guards/"/>
    <updated>2008-09-10T00:00:00+08:00</updated>
    <id>/2008/09/10/adventures-in-erlang-predicate-guards/</id>
    <content type="html">&lt;p&gt;Sometimes a function needs to behave differently depending on its inputs. Consider calculating the absolute value of a number: if the number is less than zero, you multiply by -1; if it’s zero or greater, you return it unchanged.&lt;/p&gt;

&lt;div class=&quot;text-align: center&quot;&gt;&lt;img src=&quot;/images/12.png&quot; alt=&quot;ABS(X) = { X &amp;lt; 0: -1 times X, X &amp;gt;= 0: X }&quot; /&gt;&lt;/div&gt;

&lt;p&gt;In Erlang, you handle this with predicate guards, conditions on the inputs defined right after the argument list:&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;ni&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maths&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;ni&quot;&gt;export&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]).&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;X&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when X &amp;lt; 0&lt;/code&gt; part is the guard. If it evaluates to true, that clause matches. Otherwise, Erlang falls through to the next clause, which in this case has no guard and matches everything.&lt;/p&gt;

&lt;p&gt;Erlang already provides a built-in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;abs&lt;/code&gt; function in its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;math&lt;/code&gt; module, of course. This is just a simple illustration of how guards work, and the reason my module is called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maths&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;math&lt;/code&gt;. Naming collisions: the eternal struggle.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Adventures in Erlang: Undirected Graphs</title>
    <link href="/2008/09/05/adventures-in-erlang-undirected-graphs/"/>
    <updated>2008-09-05T00:00:00+08:00</updated>
    <id>/2008/09/05/adventures-in-erlang-undirected-graphs/</id>
    <content type="html">&lt;p&gt;At &lt;a href=&quot;http://www.railsconfeurope.com/&quot;&gt;RailsConf Europe&lt;/a&gt; it quickly became obvious that while there’s a bunch of really cool things happening in the Ruby and Rails worlds, the current hotness is all about &lt;a href=&quot;http://www.erlang.org/&quot;&gt;Erlang&lt;/a&gt;. I decided to give it a whirl, so I picked up a few screencasts and the &lt;a href=&quot;http://www.pragprog.com/titles/jaerlang/programming-erlang&quot;&gt;Programming Erlang&lt;/a&gt; book and played for a few hours.&lt;/p&gt;

&lt;p&gt;I very quickly noticed that Erlang is remarkably similar to &lt;a href=&quot;http://en.wikipedia.org/wiki/Prolog&quot;&gt;Prolog&lt;/a&gt;, and when I mentioned this, it turned out Erlang was originally a Prolog descendant built for high-availability, high-performance distributed applications. Great news for me: I spent the best part of three years working with Prolog during my AI course.&lt;/p&gt;

&lt;p&gt;To shake the rust off, I decided to implement some fairly trivial predicates, the kind of thing you’d use at the start of a degree course to introduce Prolog.&lt;/p&gt;

&lt;h4 id=&quot;a-brief-introduction-to-graphs&quot;&gt;A brief introduction to graphs&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Also known as “here comes the maths bit…”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For those who haven’t covered &lt;a href=&quot;http://en.wikipedia.org/wiki/Graph_theory&quot;&gt;graph theory&lt;/a&gt; in mathematics: graphs aren’t bar charts or pie charts. The clue’s in the name, those are &lt;em&gt;charts&lt;/em&gt;. A graph is a collection of vertices and edges that join them. Ever played join-the-dots? That’s a close enough comparison.&lt;/p&gt;

&lt;p&gt;There are two kinds of graphs: directed and undirected. In a directed graph, the direction of edges matters. In an undirected graph, it doesn’t.&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/images/11.png&quot; alt=&quot;A simple directed graph.&quot; /&gt;&lt;/div&gt;

&lt;p&gt;In a directed graph with vertices A and B and an edge A → B, there’s no implied edge from B to A (above). In an undirected graph, there is (below).&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/images/10.png&quot; alt=&quot;A simple undirected graph.&quot; /&gt;&lt;/div&gt;

&lt;p&gt;For this exercise I’ll use a simple undirected graph like the one above and ask Erlang whether two vertices are connected.&lt;/p&gt;

&lt;h4 id=&quot;less-maths-more-erlang&quot;&gt;Less maths, more Erlang&lt;/h4&gt;

&lt;p&gt;First, let’s represent the graph. In English I’d describe it like this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;There’s an edge between vertex A and vertex B&lt;/li&gt;
  &lt;li&gt;There’s an edge between vertex B and vertex C&lt;/li&gt;
  &lt;li&gt;There’s an edge between vertex C and vertex A&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Erlang representation should read just as clearly:&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;Graph&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With a graph to reason about, how do I decide if two vertices N and M are connected? I look at the first edge and ask: “does this edge connect N to M?”&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;% If there&apos;s an edge from A -&amp;gt; B then A and B are connected.
%
&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Since this is an undirected graph, an edge connecting N to M also connects M to N. So I need to check the reverse too:&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;% If there&apos;s an edge from B -&amp;gt; A then A and B are connected (since we&apos;re
% considering only undirected graphs).
%
&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If the current edge matches neither condition, discard it and try the next one:&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;% If the edge currently being examined doesn&apos;t join the vertices, try
% looking through the rest of the graph searching for a matching edge.
%
&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Graph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nf&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Graph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we’ve exhausted the entire graph and found no matching edge, the vertices aren’t connected:&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;% If there are no edges to consider then the nodes aren&apos;t joined.
%
&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;_,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;_)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;no&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note the punctuation: each clause ends with a semicolon (;) except the final one, which gets a full stop (.).&lt;/p&gt;

&lt;h4 id=&quot;organising-erlang-code&quot;&gt;Organising Erlang code&lt;/h4&gt;

&lt;p&gt;Erlang code lives in modules. The module name and the filename should match, since I’m dealing with undirected graphs, the module is called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unigraph&lt;/code&gt; and lives in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unigraph.erl&lt;/code&gt;. At the top of the file, declare the module and its exports:&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;ni&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unigraph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;ni&quot;&gt;export&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;running-the-code&quot;&gt;Running the code&lt;/h4&gt;

&lt;p&gt;Change into the directory containing the Erlang file and launch the Erlang shell:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;webstc09@MC-S001877 graphs $ erl
Erlang (BEAM) emulator version 5.5.5 [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.5.5  (abort with ^G)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once in the shell, compile the module, set up the graph, and start querying:&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unigraph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unigraph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Graph&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;     &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;     &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}},&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;     &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;     &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}},&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;     &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;     &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}},&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}},&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}]&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;unigraph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Graph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;unigraph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Graph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What about a vertex that doesn’t exist? Let’s ask if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{vertex, a}&lt;/code&gt; is connected to an imaginary &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{vertex, z}&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-erlang highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;unigraph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Graph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;z&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Exactly what you’d expect, no edge, no connection.&lt;/p&gt;

&lt;h4 id=&quot;get-the-code&quot;&gt;Get the code&lt;/h4&gt;

&lt;p&gt;The complete module is available at &lt;a href=&quot;http://barkingiguana.com/file_download/2&quot;&gt;http://barkingiguana.com/file_download/2&lt;/a&gt;.&lt;/p&gt;

&lt;h4 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h4&gt;

&lt;p&gt;This was mainly an exercise in getting reacquainted with a Prolog-like language. Erlang’s pattern matching feels wonderfully natural once you’re used to it, and I’m looking forward to seeing what else I can build with it. Prolog just got a lot prettier and a bunch more fun.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Using Signals to Debug Long-Running Processes</title>
    <link href="/2008/08/31/using-signals-to-debug-long-running-processes/"/>
    <updated>2008-08-31T00:00:00+08:00</updated>
    <id>/2008/08/31/using-signals-to-debug-long-running-processes/</id>
    <content type="html">&lt;p&gt;Sometimes a long-running process starts performing its tasks much slower than it should, or in a strange order. You’d love to know what it’s doing, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strace&lt;/code&gt; produces a firehose of information several levels below what you actually care about. What can you do?&lt;/p&gt;

&lt;p&gt;Well, you could ask the process to toggle its own debug output while it’s still running. Here’s how.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;trap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;USR1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;vg&quot;&gt;$DEBUG&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;vg&quot;&gt;$DEBUG&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;level&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vg&quot;&gt;$DEBUG&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;DEBUG&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;INFO&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;info&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;USR1 received. Turning &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;vg&quot;&gt;$DEBUG&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;on&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;off&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; debugging.&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Drop that into your process and, whenever you need more detail, just send it a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;USR1&lt;/code&gt; signal:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kill -USR1 [pid]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Send it again to turn debugging back off. No restarts, no config file changes, no downtime. Just a clean toggle you can flip from the command line whenever curiosity strikes.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Jan Lehnardt Talks to the BBC about CouchDB</title>
    <link href="/2008/08/30/jan-lehnardt-talks-to-the-bbc-about-couchdb/"/>
    <updated>2008-08-30T00:00:00+08:00</updated>
    <id>/2008/08/30/jan-lehnardt-talks-to-the-bbc-about-couchdb/</id>
    <content type="html">&lt;p&gt;I’ve previously written about &lt;a href=&quot;http://barkingiguana.com/2008/06/28/installing-couchdb-080-on-ubuntu-804&quot;&gt;installing&lt;/a&gt; and &lt;a href=&quot;http://barkingiguana.com/2008/06/28/getting-started-with-couchdb-a-simple-address-book-application&quot;&gt;getting started with CouchDB&lt;/a&gt;. More recently, I attended a talk by Jan Lehnardt introducing the technology to the BBC. It’s a great overview of what CouchDB brings to the table, well worth a watch.&lt;/p&gt;

&lt;embed src=&quot;http://blip.tv/play/AcrAP47kSw&quot; type=&quot;application/x-shockwave-flash&quot; width=&quot;600&quot; height=&quot;365&quot; allowscriptaccess=&quot;always&quot; allowfullscreen=&quot;true&quot; /&gt;
&lt;p&gt;&amp;lt;/embed&amp;gt;&lt;/p&gt;

&lt;h4 id=&quot;want-to-know-more-about-couchdb&quot;&gt;Want to know more about CouchDB?&lt;/h4&gt;

&lt;p&gt;This is just one of &lt;a href=&quot;http://barkingiguana.com/tag/couchdb/&quot;&gt;several CouchDB articles&lt;/a&gt; on the blog. If you’re curious about document-oriented databases and where they fit, dig into the other posts tagged &lt;a href=&quot;http://barkingiguana.com/tag/couchdb/&quot;&gt;CouchDB&lt;/a&gt;, and keep an eye out for new ones.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Using NTPD in a Ubuntu 8.04 Xen Virtual Machine</title>
    <link href="/2008/08/02/using-ntpd-in-a-ubuntu-804-xen-virtual-machine/"/>
    <updated>2008-08-02T00:00:00+08:00</updated>
    <id>/2008/08/02/using-ntpd-in-a-ubuntu-804-xen-virtual-machine/</id>
    <content type="html">&lt;p&gt;It’s a good idea to have an accurate clock on any computer you access. Beyond the obvious convenience, consistent timestamps across your infrastructure make log analysis and event replays far more reliable. Unfortunately, clocks drift over time. NTP, the Network Time Protocol, keeps things honest by synchronising your clock against a group of reference servers on the internet.&lt;/p&gt;

&lt;p&gt;There’s a wrinkle, though: Xen guests (such as those provided by &lt;a href=&quot;http://xeriom.net/&quot;&gt;Xeriom Networks&lt;/a&gt;) have their clocks tied to the Xen host by default. Here’s how to break free and get NTP running properly.&lt;/p&gt;

&lt;h4 id=&quot;taking-a-shortcut&quot;&gt;Taking a shortcut&lt;/h4&gt;

&lt;p&gt;If you’re running a Ubuntu-based VM and you use the &lt;a href=&quot;http://wiki.xeriom.net/w/XeriomUbuntuPackagesService&quot;&gt;package host&lt;/a&gt; at Xeriom Networks, one command does the lot:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo apt-get install xeriom-ntp-client --yes --force-yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;gaining-independence&quot;&gt;Gaining independence&lt;/h4&gt;

&lt;p&gt;To stop your VM’s clock being slaved to the host, tell the kernel the clock is independent:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo su -c &quot;echo 1 &amp;gt; /proc/sys/xen/independent_wallclock&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To make this persist across reboots, add the following line to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/sysctl.conf&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xen.independent_wallclock = 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;installing-and-configuring-ntpd&quot;&gt;Installing and configuring NTPD&lt;/h4&gt;

&lt;p&gt;Install the NTP daemon via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt-get&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo apt-get install ntp --yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’re on one of Xeriom’s VMs you can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;time.xeriom.net&lt;/code&gt; as a time source. Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/ntp.conf&lt;/code&gt; to include it alongside a few servers from your nearest NTP pool:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;server time.xeriom.net prefer
server 0.uk.pool.ntp.org
server 1.uk.pool.ntp.org
server 2.uk.pool.ntp.org
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restart NTP and you’re done:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo /etc/init.d/ntp restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;whats-the-time-mr-wolf&quot;&gt;What’s the time, Mr. Wolf?&lt;/h4&gt;

&lt;p&gt;NTP takes roughly 15 minutes to settle down and select the best time source. You can check its progress with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ntpq -p&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ntpq -p
     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
*time.xeriom.net 212.13.194.87    3 u   39   64  377    0.402  -45.729   7.496
+dns1.rmplc.co.u 195.66.241.3     2 u   39   64  377    3.443  -54.808   6.142
+ntpt1.core.thep 194.152.64.68    3 u   40   64  377    0.723  -53.765   5.965
+weevil.pwns.ms  249.240.53.144   2 u   38   64  377    9.110  -57.739  11.427
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*&lt;/code&gt; marks the currently selected source, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; indicates candidates NTP might switch to. For a more detailed breakdown of this output, the &lt;a href=&quot;http://www.novell.com/coolsolutions/trench/418.html&quot;&gt;Novell documentation&lt;/a&gt; is worth a read.&lt;/p&gt;

&lt;h4 id=&quot;now-theres-no-excuse-for-being-late&quot;&gt;Now there’s no excuse for being late&lt;/h4&gt;

&lt;p&gt;That’s it, your Xen guest now keeps its own time, synchronised properly via NTP. No more mysterious timestamp drift in your logs.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Load-Balanced, Highly Available MySQL on Ubuntu 8.04</title>
    <link href="/2008/07/20/load-balanced-highly-available-mysql-on-ubuntu-804/"/>
    <updated>2008-07-20T00:00:00+08:00</updated>
    <id>/2008/07/20/load-balanced-highly-available-mysql-on-ubuntu-804/</id>
    <content type="html">&lt;p&gt;If you followed my previous post about &lt;a href=&quot;http://barkingiguana.com/2008/07/07/high-availability-mysql-on-ubuntu-804&quot;&gt;high availability MySQL&lt;/a&gt;, your application now has one less single point of failure. But what happens when the cluster starts getting overloaded? By load-balancing MySQL connections across hosts, you can handle a larger volume of queries without breaking a sweat.&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;http://barkingiguana.com/images/6.png&quot; alt=&quot;A load balanced database cluster&quot; /&gt;&lt;/div&gt;

&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;

&lt;p&gt;This article builds on the MySQL cluster from &lt;a href=&quot;http://barkingiguana.com/2008/07/07/high-availability-mysql-on-ubuntu-804&quot;&gt;my previous post&lt;/a&gt;. Set that up first if you haven’t already. You’ll also need two more virtual machines, each with one IP address:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;193.219.108.239, lb-db-01 (lb-db-01.vm.xeriom.net)&lt;/li&gt;
  &lt;li&gt;193.219.108.240, lb-db-02 (lb-db-02.vm.xeriom.net)&lt;/li&gt;
  &lt;li&gt;* 193.219.108.241, db-01 (db-01.vm.xeriom.net)&lt;/li&gt;
  &lt;li&gt;* 193.219.108.242, db-02 (db-02.vm.xeriom.net)&lt;/li&gt;
  &lt;li&gt;* 193.219.108.243, virtual IP address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;IP addresses marked with * are carried over from the previous article.&lt;/p&gt;

&lt;p&gt;All boxes have been &lt;a href=&quot;http://barkingiguana.com/2008/06/22/firewall-a-pristine-ubuntu-804-box&quot;&gt;firewalled&lt;/a&gt;. That’s just plain common sense.&lt;/p&gt;

&lt;h2 id=&quot;we-have-the-technology&quot;&gt;We have the technology&lt;/h2&gt;

&lt;p&gt;Install Heartbeat and MySQL Proxy on both load balancer boxes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;heartbeat mysql-proxy &lt;span class=&quot;nt&quot;&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;configure-and-run-mysql-proxy&quot;&gt;Configure and run MySQL Proxy&lt;/h2&gt;

&lt;p&gt;Open the firewall on the database boxes so the load balancers can connect:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On db-01 and db-02&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; mysql &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; lb-db-01.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; mysql &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; lb-db-02.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you followed the previous post, you’ll probably want to remove the rule that allowed your test box to access MySQL on the floating IP. Not critical right now, but good hygiene, and it becomes important in production.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On db-01 and db-02&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-D&lt;/span&gt; INPUT &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; mysql &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.214.108.10 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; 193.214.108.243 &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(Swap in your own floating IP and test box IP, or you’ll get a “bad rule” error.)&lt;/p&gt;

&lt;p&gt;You’ll also need to open the MySQL Proxy port on the load balancer boxes. Note that MySQL Proxy listens on port 4040, not the standard MySQL port 3306. My test box here is 193.219.108.10, substitute your own.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On lb-db-01&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 4040 &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; lb-db-01.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.219.108.10 &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On lb-db-02&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 4040 &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; lb-db-02.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.219.108.10 &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Start the proxy on both boxes, pointing it at the real database servers, then test from your test box:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; /usr/sbin/mysql-proxy &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--proxy-backend-addresses&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;db-01.vm.xeriom.net:3306 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--proxy-backend-addresses&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;db-02.vm.xeriom.net:3306 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On the test box&lt;/span&gt;
mysql &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; some_user &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;some_other_password&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; lb-db-01.vm.xeriom.net
mysql&amp;gt; &lt;span class=&quot;se&quot;&gt;\q&lt;/span&gt;
mysql &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; some_user &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;some_other_password&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; lb-db-02.vm.xeriom.net
mysql&amp;gt; &lt;span class=&quot;se&quot;&gt;\q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If MySQL tells you the load balancer hosts don’t have access, log into the database nodes and grant permissions using the hostname from the error message:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ERROR 1130 (00000): Host &apos;lb-db-01&apos; is not allowed to connect to this MySQL server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On db-01 and db-02&lt;/span&gt;
mysql &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; root &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt;
Enter password: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Enter your MySQL root password]
mysql&amp;gt; grant all on my_application.&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; to &lt;span class=&quot;s1&quot;&gt;&apos;some_user&apos;&lt;/span&gt;@&lt;span class=&quot;s1&quot;&gt;&apos;lb-db-01&apos;&lt;/span&gt;
  identified by &lt;span class=&quot;s1&quot;&gt;&apos;some_other_password&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
mysql&amp;gt; grant all on my_application.&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; to &lt;span class=&quot;s1&quot;&gt;&apos;some_user&apos;&lt;/span&gt;@&lt;span class=&quot;s1&quot;&gt;&apos;lb-db-02&apos;&lt;/span&gt;
  identified by &lt;span class=&quot;s1&quot;&gt;&apos;some_other_password&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
mysql&amp;gt; &lt;span class=&quot;se&quot;&gt;\q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you got MySQL prompts both times, both proxies are working. Now tighten things up: remove the rules allowing direct access to each load balancer node and add rules that only permit access via the floating IP:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On lb-db-01&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-D&lt;/span&gt; INPUT &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 4040 &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; lb-db-01.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.219.108.10 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 4040 &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; 193.219.108.243 &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.219.108.10 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On lb-db-02&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-D&lt;/span&gt; INPUT &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 4040 &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; lb-db-02.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.219.108.10 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 4040 &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; 193.219.108.243 &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.219.108.10 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;configure-and-run-heartbeat&quot;&gt;Configure and run Heartbeat&lt;/h2&gt;

&lt;p&gt;Open up the firewall for Heartbeat communication and populate its configuration files.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On lb-db-01&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; udp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 694 &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; lb-db-02.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On lb-db-02&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; udp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 694 &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; lb-db-01.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On both load balancer boxes&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo cp&lt;/span&gt; /usr/share/doc/heartbeat/authkeys /etc/ha.d/
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;zcat /usr/share/doc/heartbeat/ha.cf.gz &amp;gt; /etc/ha.d/ha.cf&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;zcat /usr/share/doc/heartbeat/haresources.gz &amp;gt; /etc/ha.d/haresources&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lock down &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authkeys&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo chmod &lt;/span&gt;go-wrx /etc/ha.d/authkeys
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/ha.d/authkeys&lt;/code&gt; and add a password:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;auth 2
2 sha1 your-password-here
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Configure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ha.cf&lt;/code&gt; for your network. Node names &lt;strong&gt;must&lt;/strong&gt; match the output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uname -n&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;logfile /var/log/ha-log
logfacility local0
keepalive 2
deadtime 30
initdead 120
bcast eth0
udpport 694
auto_failback on
node lb-db-01.vm.xeriom.net
node lb-db-02.vm.xeriom.net
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/ha.d/haresources&lt;/code&gt; to assign the floating IP, with lb-db-01 as the preferred node. This file must be &lt;em&gt;identical&lt;/em&gt; on both boxes:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;lb-db-01.vm.xeriom.net 193.219.108.243
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you had Heartbeat running on the database boxes from the last article, remove it now:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On the database boxes&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get remove heartbeat
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then remove the IP alias from eth0 on both database boxes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On the database boxes&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;ifconfig eth0 inet 193.219.108.243 &lt;span class=&quot;nt&quot;&gt;-alias&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now fire up Heartbeat on the load balancer boxes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On lb-db-01 then lb-db-02&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; /etc/init.d/heartbeat restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;testing-testing-testing&quot;&gt;Testing, testing, testing&lt;/h2&gt;

&lt;p&gt;Connect to the floating IP from your test box:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mysql &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; some_user &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;some_other_password&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; 193.214.108.243 my_application
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the testing procedure. At every step, your query should return results:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Run a query such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;show processlist;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Shut down db-01&lt;/li&gt;
  &lt;li&gt;Run the query again&lt;/li&gt;
  &lt;li&gt;Start db-01&lt;/li&gt;
  &lt;li&gt;Shut down db-02&lt;/li&gt;
  &lt;li&gt;Run the query again&lt;/li&gt;
  &lt;li&gt;Start db-02&lt;/li&gt;
  &lt;li&gt;Shut down lb-db-01&lt;/li&gt;
  &lt;li&gt;Run the query again&lt;/li&gt;
  &lt;li&gt;Shut down db-01&lt;/li&gt;
  &lt;li&gt;Run the query again&lt;/li&gt;
  &lt;li&gt;Start db-01&lt;/li&gt;
  &lt;li&gt;Shut down db-02&lt;/li&gt;
  &lt;li&gt;Run the query again&lt;/li&gt;
  &lt;li&gt;Start db-02&lt;/li&gt;
  &lt;li&gt;Start lb-db-01&lt;/li&gt;
  &lt;li&gt;Run the query again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your query succeeded every time, congratulations, you’ve got a load-balanced, highly available MySQL instance.&lt;/p&gt;

&lt;h2 id=&quot;where-to-go-from-here&quot;&gt;Where to go from here&lt;/h2&gt;

&lt;p&gt;High availability and load balancing don’t protect you from mistakes. Back up often, and verify that you can &lt;em&gt;restore&lt;/em&gt; from those backups. You might also want to look into building a MySQL binlog-only server for point-in-time recovery.&lt;/p&gt;

&lt;p&gt;MySQL Proxy speaks &lt;a href=&quot;http://www.lua.org/&quot;&gt;Lua&lt;/a&gt;. Learning to write Lua scripts for it opens up some powerful possibilities, query rewriting, read/write splitting, and more.&lt;/p&gt;

&lt;p&gt;I haven’t documented scaling beyond two load balancers and two database nodes here. It’s possible, but don’t just add more master-master nodes without doing your homework. Depending on your data and access patterns, you might be better served by sharding, federation, master-slave replication with read replicas, or schema optimisation. How you scale your database depends entirely on your data and how you use it. Do the research… and be sure to blog about it and let me know how it goes.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Avoiding auto_increment Collision with High Availability MySQL</title>
    <link href="/2008/07/17/avoiding-auto_increment-collision-with-high-availability-mysql/"/>
    <updated>2008-07-17T00:00:00+08:00</updated>
    <id>/2008/07/17/avoiding-auto_increment-collision-with-high-availability-mysql/</id>
    <content type="html">&lt;p&gt;If you followed my previous post about &lt;a href=&quot;http://barkingiguana.com/2008/07/07/high-availability-mysql-on-ubuntu-804&quot;&gt;high availability MySQL&lt;/a&gt;, your application now has one less single point of failure. That’s good. But as &lt;a href=&quot;http://woss.name/&quot;&gt;Graeme&lt;/a&gt; &lt;a href=&quot;http://barkingiguana.com/2008/07/07/high-availability-mysql-on-ubuntu-804#c000014&quot;&gt;pointed out&lt;/a&gt;, there’s a subtle data collision risk if replication breaks.&lt;/p&gt;

&lt;p&gt;Here’s the scenario: replication has stopped, and both db-01 and db-02 receive inserts at roughly the same time. Any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;auto_increment&lt;/code&gt; columns will generate the same values on both nodes independently. When replication resumes, those colliding IDs will cause failures.&lt;/p&gt;

&lt;p&gt;The fix is straightforward. MySQL provides two configuration variables – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;auto-increment-increment&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;auto-increment-offset&lt;/code&gt;, that control how the next value in an auto-incrementing series is generated.&lt;/p&gt;

&lt;p&gt;On db-01, in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/mysql/my.cnf&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;auto-increment-increment = 10
auto-increment-offset = 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On db-02, in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/mysql/my.cnf&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;auto-increment-increment = 10
auto-increment-offset = 2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With an increment of 10, db-01 will generate IDs like 1, 11, 21, 31… while db-02 generates 2, 12, 22, 32. They’ll never step on each other’s toes, even if replication falls behind. And by choosing an increment of 10 rather than 2, you leave room to add more nodes to the cluster later without reconfiguring.&lt;/p&gt;

&lt;p&gt;Restart MySQL on both boxes and you’re protected.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>High Availability MySQL on Ubuntu 8.04</title>
    <link href="/2008/07/07/high-availability-mysql-on-ubuntu-804/"/>
    <updated>2008-07-07T00:00:00+08:00</updated>
    <id>/2008/07/07/high-availability-mysql-on-ubuntu-804/</id>
    <content type="html">&lt;p&gt;In my &lt;a href=&quot;http://barkingiguana.com/2008/06/24/high-availability-apache-on-ubuntu-804&quot;&gt;previous post&lt;/a&gt; I showed how to build a high availability web tier using Heartbeat and Apache. That’s great for static pages, but what about dynamic, database-driven sites? How do we protect the database against node failure?&lt;/p&gt;

&lt;h2 id=&quot;preparation&quot;&gt;Preparation&lt;/h2&gt;

&lt;p&gt;You’ll need two boxes and &lt;em&gt;three&lt;/em&gt; IP addresses. I’m using &lt;a href=&quot;http://xeriom.net/&quot;&gt;virtual machines from Xeriom Networks&lt;/a&gt; again. Both are &lt;a href=&quot;http://barkingiguana.com/2008/06/22/firewall-a-pristine-ubuntu-804-box&quot;&gt;firewalled&lt;/a&gt;, with MySQL and Heartbeat ports opened so the servers can talk to each other but nobody else can reach them.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On db-01&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; mysql &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; db-02.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; udp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; mysql &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; db-02.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; udp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 694 &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; db-02.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT

&lt;span class=&quot;c&quot;&gt;# On db-02&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; mysql &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; db-01.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; udp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; mysql &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; db-01.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; udp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 694 &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; db-01.vm.xeriom.net &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Your firewall rules should look something like this. The important lines end in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tcp dpt:mysql&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;udp dpt:mysql&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dpt:694&lt;/code&gt;. Each node’s rules should open ports for the &lt;em&gt;other&lt;/em&gt; node:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Chain INPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere            state RELATED,ESTABLISHED
ACCEPT     udp  --  db-01                anywhere            udp dpt:694
ACCEPT     tcp  --  db-01                anywhere            udp dpt:mysql
ACCEPT     tcp  --  db-01                anywhere            tcp dpt:mysql
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:ssh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Save your firewall rules so they survive a reboot:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For this post, assume the following IP addresses are available:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;193.219.108.241, db-01 (db-01.vm.xeriom.net)&lt;/li&gt;
  &lt;li&gt;193.219.108.242, db-02 (db-02.vm.xeriom.net)&lt;/li&gt;
  &lt;li&gt;193.219.108.243. Not assigned (becomes the floating IP)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;start-small&quot;&gt;Start small&lt;/h2&gt;

&lt;p&gt;Install and configure MySQL on each box:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;mysql-server &lt;span class=&quot;nt&quot;&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Set a &lt;em&gt;strong&lt;/em&gt; root password during installation. Once it’s done, edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/mysql/my.cnf&lt;/code&gt; to make MySQL listen on all interfaces:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;bind-address = 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restart MySQL and verify it’s running:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo /etc/init.d/mysql restart
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; \q
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you got the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql&amp;gt;&lt;/code&gt; prompt, you’re good. Now test cross-node connectivity:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mysql -h db-02.vm.xeriom.net -u root -p
Enter password: [enter the MySQL root password you chose earlier]
ERROR 1130 (00000): Host &apos;db-01&apos; is not allowed to connect to this MySQL server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That error is actually a good sign. MySQL connected and then refused to authorise the client. We’ll create proper replication accounts shortly. If you get a &lt;em&gt;different&lt;/em&gt; error (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Can&apos;t connect to MySQL server on &apos;db-02&apos; (10061)&lt;/code&gt;), check that MySQL is running on both boxes and that the firewall rules are correct.&lt;/p&gt;

&lt;h2 id=&quot;one-way-replication&quot;&gt;One-way replication&lt;/h2&gt;

&lt;p&gt;Let’s start with simple master-slave replication. On db-01, edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/mysql/my.cnf&lt;/code&gt; and configure the binary log under the replication section:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;server-id               = 1
log_bin                 = /var/log/mysql/mysql-bin.log
expire_logs_days        = 10
max_binlog_size         = 100M
binlog_do_db            = my_application
binlog_ignore_db        = mysql
binlog_ignore_db        = test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On db-01, grant replication slave rights to db-02. Use a real, strong password in place of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;some_password&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; grant replication slave on *.* to &apos;replication&apos;@&apos;db-02.vm.xeriom.net&apos; identified by &apos;some_password&apos;;
mysql&amp;gt; \q
sudo /etc/init.d/mysql restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On db-02, configure it to replicate from db-01 by editing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/mysql/my.cnf&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;server-id                 = 2
master-host               = db-01.vm.xeriom.net
master-user               = replication
master-password           = some_password
master-port               = 3306
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restart MySQL on db-02 and check the slave status. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Slave_IO_State&lt;/code&gt; says “Waiting for master to send event”, you’re in business:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Run this on db-02 only
sudo /etc/init.d/mysql restart
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; show slave status \G
*************************** 1. row ***************************
             Slave_IO_State: Waiting for master to send event
                Master_Host: 193.219.108.241
                Master_User: replication
                Master_Port: 3306
              Connect_Retry: 60
            Master_Log_File: mysql-bin.000005
        Read_Master_Log_Pos: 98
             Relay_Log_File: mysqld-relay-bin.000004
              Relay_Log_Pos: 235
      Relay_Master_Log_File: mysql-bin.000005
           Slave_IO_Running: Yes
          Slave_SQL_Running: Yes
            Replicate_Do_DB:
        Replicate_Ignore_DB:
         Replicate_Do_Table:
     Replicate_Ignore_Table:
    Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
                 Last_Errno: 0
                 Last_Error:
               Skip_Counter: 0
        Exec_Master_Log_Pos: 98
            Relay_Log_Space: 235
            Until_Condition: None
             Until_Log_File:
              Until_Log_Pos: 0
         Master_SSL_Allowed: No
         Master_SSL_CA_File:
         Master_SSL_CA_Path:
            Master_SSL_Cert:
          Master_SSL_Cipher:
             Master_SSL_Key:
      Seconds_Behind_Master: 0
1 row in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now let’s prove it works. Create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my_application&lt;/code&gt; database on db-01 and watch it appear on db-02:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# On both nodes
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; show databases;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You should see &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# On db-01 only
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; create database my_application;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# On both nodes
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; show databases;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my_application&lt;/code&gt; database should now appear on both nodes. If it doesn’t (it didn’t for me the first time), read on.&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;trouble-shooting-one-way-replication&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;troubleshooting-one-way-replication&quot;&gt;Troubleshooting one-way replication&lt;/h2&gt;

&lt;p&gt;If the slave status doesn’t show &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Slave_IO_State: Waiting for master to send event&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Slave_IO_Running: Yes&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Slave_SQL_Running: Yes&lt;/code&gt;, something is off.&lt;/p&gt;

&lt;p&gt;Telnet is brilliant for debugging connectivity issues. Install it if you haven’t already:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;telnet
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;SSH to db-02 and telnet to db-01 on the MySQL port:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# On db-02
telnet db-01.vm.xeriom.net mysql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The problem I hit was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ERROR 1130 (00000): Host &apos;db-02&apos; is not allowed to connect to this MySQL server&lt;/code&gt;. This happens when you used the full hostname (db-02.vm.xeriom.net) in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grant&lt;/code&gt; statement but MySQL resolved the connecting host to a short name (db-02) via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/hosts&lt;/code&gt;. Run the grant again using whatever hostname appears in the error message:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# On db-01
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; grant replication slave on *.* to &apos;replication&apos;@&apos;db-02&apos; identified by &apos;some_password&apos;;
mysql&amp;gt; \q
sudo /etc/init.d/mysql restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Another gotcha: if the slave status stays at “connecting to master” for a long time and telnet works fine, you probably have the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server-id&lt;/code&gt; on both servers. Check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/mysql/my.cnf&lt;/code&gt;, fix the values, and restart MySQL.&lt;/p&gt;

&lt;h2 id=&quot;master-master-replication&quot;&gt;Master-master replication&lt;/h2&gt;

&lt;p&gt;One-way replication protects your data, but if you accidentally write to the slave (db-02), at best the databases will be inconsistent, and at worst, replication will break entirely.&lt;/p&gt;

&lt;p&gt;Setting up replication in both directions gives you a consistent dataset on both nodes, regardless of which one receives writes.&lt;/p&gt;

&lt;p&gt;On db-02, edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/mysql/my.cnf&lt;/code&gt; to enable the binary log:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;log_bin                 = /var/log/mysql/mysql-bin.log
expire_logs_days        = 10
max_binlog_size         = 100M
binlog_do_db            = my_application
binlog_ignore_db        = mysql
binlog_ignore_db        = test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Grant replication slave privileges on db-02 for the replication user on db-01:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# On db-02
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; grant replication slave on *.* to &apos;replication&apos;@&apos;db-01.vm.xeriom.net&apos; identified by &apos;some_password&apos;;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On db-01, edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/mysql/my.cnf&lt;/code&gt; to replicate from db-02:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;master-host               = db-02.vm.xeriom.net
master-user               = replication
master-password           = some_password
master-port               = 3306
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restart MySQL on both boxes and check the slave status on each. Both should report &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Slave_IO_State: Waiting for master to send event&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Slave_IO_Running: Yes&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Slave_SQL_Running: Yes&lt;/code&gt;. If not, work through the &lt;a href=&quot;#trouble-shooting-one-way-replication&quot;&gt;troubleshooting section&lt;/a&gt; above.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo /etc/init.d/mysql restart
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; show slave status \G
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’ve got this far, your database is now a master-master cluster. Sweet, sweet redundancy.&lt;/p&gt;

&lt;h2 id=&quot;heartbeat&quot;&gt;Heartbeat&lt;/h2&gt;

&lt;p&gt;The data is replicated both ways, so your data is safe if a node goes down. But applications still need to know &lt;em&gt;which&lt;/em&gt; host to connect to, and right now failover would have to be handled by the application itself.&lt;/p&gt;

&lt;p&gt;I wrote previously about &lt;a href=&quot;http://barkingiguana.com/2008/06/24/high-availability-apache-on-ubuntu-804&quot;&gt;using Heartbeat for high availability Apache&lt;/a&gt;. We’ll use the same technique here: a floating IP address that Heartbeat moves to whichever database node is alive. Applications connect to this IP, and Heartbeat makes sure it always points at a live server. Since both databases replicate from each other, it doesn’t matter which node gets the traffic.&lt;/p&gt;

&lt;p&gt;Install Heartbeat on both boxes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;heartbeat
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Copy the sample configuration files:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo cp&lt;/span&gt; /usr/share/doc/heartbeat/authkeys /etc/ha.d/
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;zcat /usr/share/doc/heartbeat/ha.cf.gz &amp;gt; /etc/ha.d/ha.cf&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;zcat /usr/share/doc/heartbeat/haresources.gz &amp;gt; /etc/ha.d/haresources&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lock down &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authkeys&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo chmod &lt;/span&gt;go-wrx /etc/ha.d/authkeys
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/ha.d/authkeys&lt;/code&gt; and add a password:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;auth 2
2 sha1 your-password-here
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Configure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ha.cf&lt;/code&gt; for your network. Node names &lt;strong&gt;must&lt;/strong&gt; match the output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uname -n&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;logfile /var/log/ha-log
logfacility local0
keepalive 2
deadtime 30
initdead 120
bcast eth0
udpport 694
auto_failback on
node db-01.vm.xeriom.net
node db-02.vm.xeriom.net
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;haresources&lt;/code&gt; to assign the floating IP. This file must be identical on &lt;strong&gt;both&lt;/strong&gt; nodes, with the hostname matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uname -n&lt;/code&gt; on db-01:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db-01.vm.xeriom.net 193.219.108.243
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Start Heartbeat on db-01, then db-02:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; /etc/init.d/heartbeat start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This takes a while to start. Watch progress with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tail -f /var/log/ha-log&lt;/code&gt;. Eventually db-01 should report:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;heartbeat[7734]: 2008/07/07_17:19:34 info: Initial resource acquisition complete (T_RESOURCES(us))
IPaddr[7739]:   2008/07/07_17:19:37 INFO:  Running OK
heartbeat[7745]: 2008/07/07_17:19:37 info: Local Resource acquisition completed.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;testing-it-all&quot;&gt;Testing it all&lt;/h2&gt;

&lt;p&gt;Until now, both database boxes only allowed MySQL connections from each other. To verify failover, we need to connect from an external machine. Find the public IP of your test box (here it’s 193.214.108.10) and open access on both database boxes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On both boxes&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; mysql &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.214.108.10 &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; 193.214.108.243 &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a test user on both boxes:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# On both boxes
mysql -u root -p
Enter password: [enter the MySQL root password you chose earlier]
mysql&amp;gt; grant all, replication_client on my_application.* to &apos;some_user&apos;@&apos;193.214.108.10&apos; identified by &apos;some_other_password&apos;;
mysql&amp;gt; \q
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now connect to the floating IP from your test box:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mysql -u some_user -p -h 193.214.108.243 my_application
mysql&amp;gt; show slave status \G
*************************** 1. row ***************************
             Slave_IO_State: Waiting for master to send event
                Master_Host: 193.219.108.242
[unimportant lines snipped]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note the master host is db-02. Stop Heartbeat (or shut down db-01) and run the query again, you should see the master has changed to the other node’s IP.&lt;/p&gt;

&lt;p&gt;Bring db-01 back up and query once more. The master host should be back to what it was originally.&lt;/p&gt;

&lt;h2 id=&quot;auto-increment-offsets&quot;&gt;Auto-increment offsets&lt;/h2&gt;

&lt;p&gt;To avoid problems if replication fails, check out &lt;a href=&quot;http://barkingiguana.com/2008/07/17/avoiding-auto_increment-collision-with-high-availability-mysql&quot;&gt;avoiding auto_increment collision&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Verify Database Connections in Long-Running Idle Rails Processes</title>
    <link href="/2008/07/03/verify-database-connections-in-long-running-idle-rails-processes/"/>
    <updated>2008-07-03T00:00:00+08:00</updated>
    <id>/2008/07/03/verify-database-connections-in-long-running-idle-rails-processes/</id>
    <content type="html">&lt;p&gt;I recently interfaced one of my &lt;a href=&quot;http://barkingiguana.com/2008/05/28/xmpp4r-simple-makes-xmpp-in-ruby-uhh-simple&quot;&gt;xmpp4r bots&lt;/a&gt; with the Xeriom Networks control panel. I’d planned to write a post about how easy it is, but &lt;a href=&quot;http://rubypond.com/articles/2008/06/26/make-your-own-im-bot-in-ruby-and-interface-it-with-your-rails-app&quot;&gt;RubyPond beat me to it&lt;/a&gt;. I can, however, offer one piece of advice that will save you from a subtle bug: periodically verify your database connections.&lt;/p&gt;

&lt;p&gt;If your Rails process sits idle for a while, which is normal for something like a chat bot waiting for messages. MySQL (and other databases) will silently drop the connection. When the bot finally tries to use it, everything falls over.&lt;/p&gt;

&lt;p&gt;The fix is simple. Spin up a background thread that pings the connection every half hour:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;RAILS_DEFAULT_LOGGER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;debug&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Launching database connection verifier&quot;&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;loop&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1800&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Half an hour&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;RAILS_DEFAULT_LOGGER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;debug&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Verifying database connections&quot;&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verify_active_connections!&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Drop this into your script and stale connections will be reconnected before they cause trouble.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Update: the Xeriom support bot is no longer running. It was fun, but not hugely useful in that context.&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Installing CouchDB 0.8.0 on Ubuntu 8.04</title>
    <link href="/2008/06/28/installing-couchdb-080-on-ubuntu-804/"/>
    <updated>2008-06-28T00:00:00+08:00</updated>
    <id>/2008/06/28/installing-couchdb-080-on-ubuntu-804/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://incubator.apache.org/couchdb/&quot;&gt;CouchDB&lt;/a&gt; is a distributed document store that you interact with over HTTP. The CouchDB site has a &lt;a href=&quot;http://incubator.apache.org/couchdb/docs/intro.html&quot;&gt;more detailed introduction&lt;/a&gt; if you want the full picture.&lt;/p&gt;

&lt;h2 id=&quot;some-assembly-required&quot;&gt;Some assembly required&lt;/h2&gt;

&lt;p&gt;Since CouchDB is still a fairly young project, there are no pre-built packages for Ubuntu 8.04. Word on the street is that Intrepid Ibex will ship one, but until then, here’s a quick-and-dirty way to get it running from source.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;automake autoconf libtool subversion-tools help2man
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;build-essential erlang libicu38 libicu-dev
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;libreadline5-dev checkinstall libmozjs-dev wget
wget http://mirror.public-internet.co.uk/ftp/apache/incubator/couchdb/0.8.0-incubating/apache-couchdb-0.8.0-incubating.tar.gz
&lt;span class=&quot;nb&quot;&gt;tar&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-xzvf&lt;/span&gt; apache-couchdb-0.8.0-incubating.tar.gz
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;apache-couchdb-0.8.0-incubating
./configure
make &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;make &lt;span class=&quot;nb&quot;&gt;install
sudo &lt;/span&gt;adduser couchdb
&lt;span class=&quot;nb&quot;&gt;sudo mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /usr/local/var/lib/couchdb
&lt;span class=&quot;nb&quot;&gt;sudo chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; couchdb /usr/local/var/lib/couchdb
&lt;span class=&quot;nb&quot;&gt;sudo mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /usr/local/var/log/couchdb
&lt;span class=&quot;nb&quot;&gt;sudo chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; couchdb /usr/local/var/log/couchdb
&lt;span class=&quot;nb&quot;&gt;sudo mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /usr/local/var/run
&lt;span class=&quot;nb&quot;&gt;sudo chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; couchdb /usr/local/var/run
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;update-rc.d couchdb defaults
&lt;span class=&quot;nb&quot;&gt;sudo cp&lt;/span&gt; /usr/local/etc/init.d/couchdb /etc/init.d/
&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; /etc/init.d/couchdb start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;let-others-rest-on-your-couch&quot;&gt;Let others REST on your Couch&lt;/h2&gt;

&lt;p&gt;By default, CouchDB only listens for connections from localhost. To open it up, edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/etc/couchdb/couch.ini&lt;/code&gt; and restart CouchDB.&lt;/p&gt;

&lt;p&gt;If you’re running a &lt;a href=&quot;http://barkingiguana.com/2008/06/22/firewall-a-pristine-ubuntu-804-box&quot;&gt;firewall&lt;/a&gt; (and you should be), open the CouchDB port:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 5984 &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;testing-that-it-all-works&quot;&gt;Testing that it all works&lt;/h2&gt;

&lt;p&gt;Since CouchDB speaks HTTP, any HTTP client will do. Fire up your browser and hit the server’s IP address on port 5984. If everything’s working, you’ll get back a friendly greeting:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;couchdb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Welcome&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;0.8.0-incubating&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;more-couchdb&quot;&gt;More CouchDB?&lt;/h2&gt;

&lt;p&gt;This is just one of &lt;a href=&quot;http://barkingiguana.com/tag/couchdb/&quot;&gt;several CouchDB articles&lt;/a&gt; on this blog, with more on the way. Check back often for &lt;a href=&quot;http://barkingiguana.com/&quot;&gt;new posts&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Getting Started with CouchDB: A Simple Address Book Application</title>
    <link href="/2008/06/28/getting-started-with-couchdb-a-simple-address-book-application/"/>
    <updated>2008-06-28T00:00:00+08:00</updated>
    <id>/2008/06/28/getting-started-with-couchdb-a-simple-address-book-application/</id>
    <content type="html">&lt;p&gt;I’ve recently &lt;a href=&quot;http://barkingiguana.com/2008/06/28/installing-couchdb-080-on-ubuntu-804&quot;&gt;installed CouchDB&lt;/a&gt; but, still being pretty new to this whole document store thing, I don’t really know what it can do or how to make it do it.&lt;/p&gt;

&lt;p&gt;The best way to learn is to &lt;em&gt;do&lt;/em&gt;. So I’m going to build a simple address book.&lt;/p&gt;

&lt;h2 id=&quot;investigation-and-technology-choice&quot;&gt;Investigation and technology choice&lt;/h2&gt;

&lt;p&gt;Since CouchDB speaks JSON, I’ll write the address book in JavaScript and HTML. And because CouchDB includes a built-in web server, I can serve the application from the same place I store the data. I’ll call the main file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addressbook.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Peeking at the CouchDB configuration in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/etc/couchdb/couch.ini&lt;/code&gt;, I can see the web server’s document root is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/share/couchdb/www&lt;/code&gt;, that’s where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addressbook.html&lt;/code&gt; will go.&lt;/p&gt;

&lt;p&gt;I’ll also need a database for storing contacts. There’s a handy admin interface at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/_utils/&lt;/code&gt; that you can access through your browser by pointing it at the CouchDB server’s IP address and port.&lt;/p&gt;

&lt;p&gt;CouchDB ships with a JavaScript wrapper at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/_utils/script/couch.js&lt;/code&gt;, but it only talks to localhost. Since I’m accessing the page over the network, I’ll borrow some of its ideas and adapt them.&lt;/p&gt;

&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;

&lt;p&gt;First, create the database. Open the admin interface at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/_utils/&lt;/code&gt; and create a database called “addressbook”. That’s where our data will live.&lt;/p&gt;

&lt;p&gt;The UI is a plain webpage with JavaScript, which keeps things simple. Here’s the starting point:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Strict//EN&quot;
  &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd&quot;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://www.w3.org/1999/xhtml&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- The javascript will live in addressbook.js --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://aaa.bbb.ccc.ddd:5984/_utils/addressbook.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Address Book&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Address Book&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;addressbook&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;loading&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Loading... please wait...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Since I’ve been spoiled by ActiveRecord, I want something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;var people = Person.find(&quot;all&quot;);&lt;/code&gt; to return all records, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person.find(&quot;123456-1234-1234-123456&quot;);&lt;/code&gt; to fetch a specific one:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Push the implementation details of the database into a&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// different object to keep Person clean.&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;//&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AddressBook&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;allCards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;openCard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AddressBook&lt;/code&gt; object abstracts the database connection away from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt;. It provides two methods – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allCards&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openCard(id)&lt;/code&gt;, which talk to CouchDB and handle data marshalling:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;AddressBook&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Change this to point to your own CouchDB instance.&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;http://craig-01.vm.xeriom.net:5984/addressbook/&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;_request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;XMLHttpRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Fetch all address book cards.&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;allCards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uri&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_all_docs&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;responseText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;allDocs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;offset&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;openCard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;allDocs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;allDocs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;allDocs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Fetch an individual address book card.&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;openCard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uri&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;404&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;responseText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’m offloading JSON parsing to Yahoo’s &lt;a href=&quot;http://developer.yahoo.com/yui/json/&quot;&gt;JSON library&lt;/a&gt;. Pull it into the webpage and expose it in the global namespace:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Add this to the head of addressbook.html --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://yui.yahooapis.com/2.5.2/build/yahoo/yahoo-min.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://yui.yahooapis.com/2.5.2/build/json/json-min.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Make YUI JSON available in the global namespace.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Add this to addressbook.js&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;YAHOO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The last piece is something to load all contacts and render them on the page. This uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.onload&lt;/code&gt;, which is &lt;a href=&quot;http://www.geekdaily.net/2007/07/27/javascript-windowonload-is-bad-mkay/&quot;&gt;not ideal&lt;/a&gt;, but for a quick demo it does the job:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// This is horrible, I know, but it&apos;s just a simple example.&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;addressbook&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;addressbook&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;personList&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ul&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;offset&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;people&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;people&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;personNode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createTextNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;personNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;personList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;personNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;addressbook&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;removeChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;loading&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;addressbook&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;personList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it, the application is ready. Upload &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addressbook.html&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addressbook.js&lt;/code&gt; to the document root of the CouchDB server, open your browser, and navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://aaa.bbb.ccc.ddd:5984/_utils/addressbook.html&lt;/code&gt; (replacing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aaa.bbb.ccc.ddd&lt;/code&gt; with your CouchDB server’s IP address).&lt;/p&gt;

&lt;p&gt;You’ll be greeted by a blank page that says “Address Book”. Not very impressive, right? But nothing’s wrong, there’s just no data in the database yet.&lt;/p&gt;

&lt;p&gt;The admin interface at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/_utils/&lt;/code&gt; can also add documents. Navigate to the addressbook database and create a new document. When it asks for an ID, leave the field blank and it’ll auto-generate one. Add a field called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt;, click the green checkbox, then double-click the value and set it to your name in quotes (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;Craig Webster&quot;&lt;/code&gt;). Click the green arrow, hit “save document”, then refresh the address book page. Your new record should appear.&lt;/p&gt;

&lt;h2 id=&quot;moving-forward&quot;&gt;Moving forward&lt;/h2&gt;

&lt;p&gt;I’ve shown how to retrieve data from CouchDB using JavaScript, but data entry still happens through the admin interface. Watch this space for an upcoming article on manipulating the database from JavaScript so we can add contacts directly from the address book.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>High Availability Apache on Ubuntu 8.04</title>
    <link href="/2008/06/24/high-availability-apache-on-ubuntu-804/"/>
    <updated>2008-06-24T00:00:00+08:00</updated>
    <id>/2008/06/24/high-availability-apache-on-ubuntu-804/</id>
    <content type="html">&lt;p&gt;It’s nice when your website keeps serving pages even after something catastrophic happens. Running two Apache nodes with Heartbeat gets you there, if one server blows up, the other takes over in short order.&lt;/p&gt;

&lt;h2 id=&quot;prelude&quot;&gt;Prelude&lt;/h2&gt;

&lt;p&gt;You’ll need two boxes and &lt;em&gt;three&lt;/em&gt; IP addresses. I’m using &lt;a href=&quot;http://xeriom.net/&quot;&gt;virtual machines from Xeriom Networks&lt;/a&gt;. Both have been &lt;a href=&quot;http://barkingiguana.com/2008/06/22/firewall-a-pristine-ubuntu-804-box&quot;&gt;firewalled&lt;/a&gt;, and I’ve opened the HTTP port to the world:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 3 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; http &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For this post, let’s assume the following IP addresses are available:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;193.219.108.236. Node 1 (craig-02.vm.xeriom.net)&lt;/li&gt;
  &lt;li&gt;193.219.108.237. Node 2 (craig-03.vm.xeriom.net)&lt;/li&gt;
  &lt;li&gt;193.219.108.238. Not assigned (this becomes our floating IP)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;simple-service&quot;&gt;Simple service&lt;/h2&gt;

&lt;p&gt;First, install Apache on both boxes. Nothing fancy, we just want to confirm we can serve &lt;em&gt;something&lt;/em&gt; over HTTP.&lt;/p&gt;

&lt;p&gt;Run this on both boxes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;apache2 &lt;span class=&quot;nt&quot;&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Open a browser and hit the IP addresses for Node 1 and Node 2. You should see the default Apache page saying “It works!”. If you don’t, check your firewall allows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www&lt;/code&gt; traffic. Your rules should look like this, note the line ending &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tcp dpt:www&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere            state RELATED,ESTABLISHED
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:ssh
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:www
DROP       all  --  anywhere             anywhere

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;adding-resilience&quot;&gt;Adding resilience&lt;/h2&gt;

&lt;p&gt;Apache can serve pages from both machines now, which is great, but it doesn’t protect against one of them dying. For that, we use Heartbeat.&lt;/p&gt;

&lt;p&gt;Install Heartbeat on both boxes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;heartbeat
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Copy the sample configuration files to Heartbeat’s config directory:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo cp&lt;/span&gt; /usr/share/doc/heartbeat/authkeys /etc/ha.d/
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;zcat /usr/share/doc/heartbeat/ha.cf.gz &amp;gt; /etc/ha.d/ha.cf&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;zcat /usr/share/doc/heartbeat/haresources.gz &amp;gt; /etc/ha.d/haresources&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lock down &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authkeys&lt;/code&gt;, it’s going to contain a password:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo chmod &lt;/span&gt;go-wrx /etc/ha.d/authkeys
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/ha.d/authkeys&lt;/code&gt; and add a password of your choice:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;auth 2
2 sha1 your-password-here
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Configure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ha.cf&lt;/code&gt; for your network. The node names &lt;strong&gt;must&lt;/strong&gt; match the output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uname -n&lt;/code&gt; on each box:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;logfile /var/log/ha-log
logfacility local0
keepalive 2
deadtime 30
initdead 120
bcast eth0
udpport 694
auto_failback on
node craig-02.vm.xeriom.net
node craig-03.vm.xeriom.net
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now tell Heartbeat to manage Apache. Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;haresources&lt;/code&gt; on both machines, the contents must be identical on both nodes, and the hostname should be the output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uname -n&lt;/code&gt; on Node 1:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;craig-02.vm.xeriom.net 193.219.108.238 apache2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The IP address here is the unassigned one from the prelude, it becomes the floating virtual IP.&lt;/p&gt;

&lt;p&gt;Since we told Heartbeat to use UDP port 694, we need to open it in the firewall on both boxes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 2 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; udp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; 694 &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Your iptables rules should now look like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere            state RELATED,ESTABLISHED
ACCEPT     udp  --  anywhere             anywhere            udp dpt:694
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:ssh
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:www
DROP       all  --  anywhere             anywhere

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a file on each box so we can tell which server is responding:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Node 1 (craig-02.vm.xeriom.net)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;craig-02.vm.xeriom.net&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /var/www/index.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Node 2 (craig-03.vm.xeriom.net)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;craig-03.vm.xeriom.net&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /var/www/index.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Hit each node’s IP address in your browser to confirm the right content is showing. If it works, it’s time to flip the switch.&lt;/p&gt;

&lt;h2 id=&quot;bringing-it-to-life&quot;&gt;Bringing it to life&lt;/h2&gt;

&lt;p&gt;Start Heartbeat on the master (Node 1) first, then the slave (Node 2):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; /etc/init.d/heartbeat start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This takes a while to start up. Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tail -f /var/log/ha-log&lt;/code&gt; on both boxes to watch progress. After a bit, you should see Node 1 report something like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;heartbeat[6792]: 2008/06/24_11:06:21 info: Initial resource acquisition complete (T_RESOURCES(us))
IPaddr[6867]:   2008/06/24_11:06:22 INFO:  Running OK
heartbeat[6832]: 2008/06/24_11:06:22 info: Local Resource acquisition completed.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;testing-for-a-broken-heart&quot;&gt;Testing for a broken heart&lt;/h2&gt;

&lt;p&gt;Check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ifconfig eth0:0&lt;/code&gt; on both boxes. You should see output like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Node 1
sudo ifconfig eth0:0
eth0:0    Link encap:Ethernet  HWaddr 00:16:3e:3c:70:25
          inet addr:193.219.108.238  Bcast:193.219.108.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Node 2
sudo ifconfig eth0:0
eth0:0    Link encap:Ethernet  HWaddr 00:16:3e:92:ad:78
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Node 1 has claimed the virtual IP address. If Node 1 dies, Node 2 takes over. Simulate a failure by stopping Heartbeat on Node 1:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Node 1&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; /etc/init.d/heartbeat stop
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ifconfig&lt;/code&gt; again, the virtual IP should now be on Node 2. Bring Node 1 back up and it should reclaim the IP.&lt;/p&gt;

&lt;p&gt;If this all worked, congratulations. Heartbeat is running and your web tier will survive a node failure. Skip ahead to see it in the browser.&lt;/p&gt;

&lt;p&gt;If you see messages about the message queue filling up, the two nodes can’t talk to each other. Double-check that UDP port 694 is open on &lt;em&gt;both&lt;/em&gt; boxes:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;heartbeat[6148]: 2008/06/24_11:05:09 ERROR: Message hist queue is filling up (500 messages in queue)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Verify the firewall rules, the important line ends with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;udp dpt:694&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere            state RELATED,ESTABLISHED
ACCEPT     udp  --  anywhere             anywhere            udp dpt:694
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:ssh
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:www
DROP       all  --  anywhere             anywhere

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-proof-is-in-the-pudding&quot;&gt;The proof is in the pudding&lt;/h2&gt;

&lt;p&gt;Open your browser and hit the virtual IP address (193.219.108.238 in this example). You should see Node 1’s page.&lt;/p&gt;

&lt;p&gt;Stop Heartbeat on Node 1 (or shut it down entirely) and refresh. You should now see Node 2.&lt;/p&gt;

&lt;p&gt;Bring Node 1 back up and refresh once more. You’re back on Node 1.&lt;/p&gt;

&lt;p&gt;That’s high availability in action. If one server goes down, your users never notice.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Firewall a Pristine Ubuntu 8.04 Box</title>
    <link href="/2008/06/22/firewall-a-pristine-ubuntu-804-box/"/>
    <updated>2008-06-22T00:00:00+08:00</updated>
    <id>/2008/06/22/firewall-a-pristine-ubuntu-804-box/</id>
    <content type="html">&lt;p&gt;Here’s a quick recipe to lock down a fresh Ubuntu 8.04 install. These rules block everything except SSH, giving you a solid baseline to build on.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;iptables
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt; INPUT &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; lo &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt; INPUT &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; state &lt;span class=&quot;nt&quot;&gt;--state&lt;/span&gt; ESTABLISHED,RELATED &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt; INPUT &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; ssh &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt; INPUT &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; DROP
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To persist your rules across reboots, loading them on startup and saving them on shutdown, add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pre-up&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-down&lt;/code&gt; hooks to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/network/interfaces&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pre-up    iptables-restore &amp;lt; /etc/iptables.rules
post-down iptables-save -c &amp;gt; /etc/iptables.rules
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;From here, punch additional holes as you need them. That’s it, simple, effective, and a sensible first step for any new server.&lt;/p&gt;

&lt;p&gt;If you’re hosted at &lt;a href=&quot;http://xeriom.net/&quot;&gt;Xeriom Networks&lt;/a&gt; and want to be monitored by the &lt;a href=&quot;http://wiki.xeriom.net/w/XeriomAlertService&quot;&gt;monitoring service&lt;/a&gt;, allow ICMP Type 8 (ping) from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;monitor.xeriom.net&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 193.219.108.245 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; icmp &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; icmp &lt;span class=&quot;nt&quot;&gt;--icmp-type&lt;/span&gt; 8 &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Don’t forget to save the updated rules:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>A Simple Email Hub for Your Local Network</title>
    <link href="/2008/06/22/a-simple-email-hub-for-your-local-network/"/>
    <updated>2008-06-22T00:00:00+08:00</updated>
    <id>/2008/06/22/a-simple-email-hub-for-your-local-network/</id>
    <content type="html">&lt;p&gt;I’ve been setting up the new &lt;a href=&quot;http://xeriom.net/&quot;&gt;Xeriom Networks&lt;/a&gt; MX service and figured I’d document the process. If you think something should be done differently, please leave a comment.&lt;/p&gt;

&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;

&lt;p&gt;The requirements are deliberately simple. We don’t need spam filtering, greylisting, logging, or virus scanning. We’re building a bare-bones service that provides reliable email delivery to hosts within our network, letting clients decide their own email policy. We will, however, do a little blacklist checking.&lt;/p&gt;

&lt;h2 id=&quot;installing-the-software&quot;&gt;Installing the software&lt;/h2&gt;

&lt;p&gt;I’m using Postfix because I know it well. Since we’re not doing any filtering, the basic install fits our needs perfectly.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;postfix &lt;span class=&quot;nt&quot;&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Stop Postfix, it starts automatically after install, and we need to configure it first.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; /etc/init.d/postfix stop
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;configuring-postfix&quot;&gt;Configuring Postfix&lt;/h2&gt;

&lt;p&gt;Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/postfix/main.cf&lt;/code&gt; to contain the following:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Don&apos;t reveal the OS in the banner.
smtpd_banner = $myhostname ESMTP $mail_name
biff = no

# appending .domain is the MUA&apos;s job.
append_dot_mydomain = no

# Send &quot;delivery delayed&quot; emails after 4 hours.
delay_warning_time = 4h

readme_directory = no

smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_use_tls=yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

# This is mx1.xeriom.net. Change for mx2, mx3, etc.
myhostname = mx1.xeriom.net
myorigin = mx1.xeriom.net

# Map root, abuse and postmaster to real email addresses.
virtual_alias_maps = hash:/etc/postfix/virtual

alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
mydestination =
relayhost =
mynetworks = 127.0.0.0/8
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
local_transport = error:No local mail delivery
local_recipient_maps =
smtpd_helo_required = yes

# Only allow the service to be used for hosts with final
# destinations within our VM network.
permit_mx_backup_networks = 193.219.108.0/24

# Only accept mail from nice people.
# Read and understand these blacklists policies before you
# use them or you risk losing mail!
smtpd_client_restrictions = reject_rbl_client zen.spamhaus.org,
  reject_rbl_client cbl.abuseat.org,
  reject_rbl_client dul.dnsbl.sorbs.net

# Only relay mail for which this machine is a listed MX backup.
smtpd_recipient_restrictions = permit_mx_backup, reject
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now create the aliases database and redirect standard mailbox addresses to real people:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;newaliases
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;postmaster postmaster@xeriom.net&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/postfix/virtual
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;abuse abuse@xeriom.net&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/postfix/virtual
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;root root@xeriom.net&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/postfix/virtual
postmap /etc/postfix/virtual
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restart Postfix so the changes take effect:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; /etc/init.d/postfix restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After restarting, punch a hole in the firewall for SMTP traffic. If you don’t have a firewall set up yet, you should, &lt;a href=&quot;http://barkingiguana.com/2008/06/22/firewall-a-pristine-ubuntu-804-box&quot;&gt;do that now&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;iptables &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; INPUT 4 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; tcp &lt;span class=&quot;nt&quot;&gt;--dport&lt;/span&gt; smtp &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; ACCEPT
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;sh &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iptables-save -c &amp;gt; /etc/iptables.rules&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;testing-the-setup&quot;&gt;Testing the setup&lt;/h2&gt;

&lt;p&gt;First, verify that the new MX is listed in the DNS zone and that the final MX destination falls within the networks specified in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permit_mx_backup_networks&lt;/code&gt;. The domain I’m testing with is emailmyfeeds.com.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dig MX emailmyfeeds.com +short
0 emailmyfeeds.com.
10 mx1.xeriom.net.
10 mx2.xeriom.net.

dig emailmyfeeds.com +short
193.219.108.60
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;telnet&lt;/code&gt; to send a trial email through the new MX. Here’s the full SMTP conversation for a successful send:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;telnet mx1.xeriom.net smtp
Trying 193.219.108.242...
Connected to 193.219.108.242.
Escape character is &apos;^]&apos;.
220 mx1.xeriom.net ESMTP Postfix
EHLO my-computer
250-mx1.xeriom.net
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
MAIL FROM: craig@xeriom.net
250 2.1.0 Ok
RCPT TO: craig@emailmyfeeds.com
250 2.1.5 Ok
DATA
354 End data with &amp;lt;CR&amp;gt;&amp;lt;LF&amp;gt;.&amp;lt;CR&amp;gt;&amp;lt;LF&amp;gt;
TEST!

.
250 2.0.0 Ok: queued as A6EED440BB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If after the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RCPT TO&lt;/code&gt; line you get something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;554 5.7.1 &amp;lt;test@foo.com&amp;gt;: Recipient address rejected: Access denied&lt;/code&gt;, it means either the domain doesn’t have the MX listed in its zone file yet (or the DNS change hasn’t propagated), or the final destination doesn’t fall within the ranges allowed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permit_mx_backup_networks&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One more thing: &lt;strong&gt;always&lt;/strong&gt; check your MX servers using an &lt;a href=&quot;http://www.abuse.net/relay.html&quot;&gt;open relay checker&lt;/a&gt;. If you skip this step, you’re helping distribute spam, and nobody wants that.&lt;/p&gt;

&lt;h2 id=&quot;using-the-xeriom-mx-service&quot;&gt;Using the Xeriom MX service&lt;/h2&gt;

&lt;p&gt;If you’re running a VM at Xeriom Networks, you can use this service from 2008-06-24 by following the instructions at &lt;a href=&quot;http://wiki.xeriom.net/w/XeriomMXService&quot;&gt;the Xeriom wiki&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Offline Tasks the Easy Way</title>
    <link href="/2008/06/06/offline-tasks-the-easy-way/"/>
    <updated>2008-06-06T00:00:00+08:00</updated>
    <id>/2008/06/06/offline-tasks-the-easy-way/</id>
    <content type="html">&lt;p&gt;There’s been a lot of chat on the &lt;a href=&quot;http://lrug.org/&quot;&gt;LRUG&lt;/a&gt; list recently about job scheduling systems and process managers for offloading expensive tasks. BackgrounDRb, Beanstalk, Starling, BackgroundJob, all sorts of solutions have been thrown around. These systems have their place, but most of the time they’re adding complexity you just don’t need.&lt;/p&gt;

&lt;p&gt;One case where I think they’re overkill is when you need to pull data from an external service on a schedule, completely disconnected from the &lt;a href=&quot;http://perl.plover.com/yak/presentation/samples/security/slide012.html&quot;&gt;HTTP request-response cycle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Say you want to fetch the most recent article from this blog every 15 minutes and write it to a file that can be served statically. A straightforward implementation looks like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;net/http&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;hpricot&apos;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;barking_iguana&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;http://barkingiguana.com/&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kp&quot;&gt;loop&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;articles&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hpricot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Net&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTTP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;barking_iguana&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;articles&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;div.article a[@rel=bookmark] text()&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;link&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;articles&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;div.article a[@rel=bookmark]&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;href&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Of course, this should have a real file path in it.&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/.../.../.../barking_iguana.ssi&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;w+&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;link&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;900&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 15 minutes&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it. No screwing around with complex scheduling infrastructure, just run it and it loops forever.&lt;/p&gt;

&lt;p&gt;“But what if it crashes?” Fair question. In the unlikely event that something this simple falls over, I’d have &lt;a href=&quot;http://god.rubyforge.org/&quot;&gt;God&lt;/a&gt; watching the process so it gets restarted automatically. You’ve already got something monitoring your processes, right? Adding one more to the list is trivial.&lt;/p&gt;

&lt;p&gt;Sometimes the simplest solution really is the best one.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Packaging and Deployment with Ubuntu</title>
    <link href="/2008/05/31/packaging-and-deployment-with-ubuntu/"/>
    <updated>2008-05-31T00:00:00+08:00</updated>
    <id>/2008/05/31/packaging-and-deployment-with-ubuntu/</id>
    <content type="html">&lt;p&gt;After extensively customising some software on one of our hosts, I realised I was staring down the barrel of repeating the same procedure another twenty times. No thanks. Instead, I decided to package the customisations and install that package onto each host. One small problem: I had absolutely no idea how to create Ubuntu packages or distribute them.&lt;/p&gt;

&lt;p&gt;Several hours of trawling through documentation later, none of which ever quite told me &lt;em&gt;enough&lt;/em&gt;. I pulled together two wiki articles that cover the whole process end to end. Hopefully they’ll save you the same headache.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://wiki.xeriom.net/w/CreatingPackagesForUbuntu&quot;&gt;Package your software&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://wiki.xeriom.net/w/RunningYourOwnUbuntuPackageRepository&quot;&gt;Create a personal Ubuntu package repository&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://wiki.xeriom.net/w/XeriomPackageDocumentation&quot;&gt;Documentation for Xeriom packaged software&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you spot mistakes, please do correct them, that’s the whole point of a wiki.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>XMPP4R-Simple makes XMPP in Ruby uhh... simple...</title>
    <link href="/2008/05/28/xmpp4r-simple-makes-xmpp-in-ruby-uhh-simple/"/>
    <updated>2008-05-28T00:00:00+08:00</updated>
    <id>/2008/05/28/xmpp4r-simple-makes-xmpp-in-ruby-uhh-simple/</id>
    <content type="html">&lt;p&gt;I thought it would be fun to build a control interface you could talk to over instant messaging, something like the IM bot that Twitter used to have.&lt;/p&gt;

&lt;p&gt;I started by looking at XMPP4R, but a bit of reading led me to &lt;a href=&quot;http://xmpp4r-simple.rubyforge.org/&quot;&gt;XMPP4R-Simple&lt;/a&gt;. Well, simple is always good. One &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gem install&lt;/code&gt; and 45 minutes later, I had a Ruby script that could log in to an XMPP server, listen to (and log) what people said, and respond with a simple message.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/env ruby&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;rubygems&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;xmpp4r-simple&apos;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;logfile&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;..&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;log&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;__FILE__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.log&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hodel3000CompliantLogger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logfile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Jabber&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Simple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;username@domain.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;password&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:away&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;No one here but us mice.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deliver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;craig@xeriom.net&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;I woke up at &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kp&quot;&gt;loop&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;begin&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;received_messages&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;jid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;info&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%s said: %s&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;subscribed_to?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deliver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Nom nom nom.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;presence_updates&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;info&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; is &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; (&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;)&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new_subscriptions&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;presence&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;info&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;presence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jabber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;subscribed_to?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;jid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The loop does three things: it handles incoming messages (logging them and replying with a deeply intellectual “Nom nom nom”), tracks presence updates, and auto-accepts new subscriptions. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sleep 1&lt;/code&gt; calls keep us from hammering the server.&lt;/p&gt;

&lt;p&gt;Our own little pet XMPP client. How cute is that?&lt;/p&gt;

&lt;p&gt;If you’ve found this article useful, I’d appreciate a recommendation at &lt;a href=&quot;http://www.workingwithrails.com/recommendation/new/person/7241-craig-webster&quot;&gt;Working With Rails&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Catching up on the world</title>
    <link href="/2008/05/28/catching-up-on-the-world/"/>
    <updated>2008-05-28T00:00:00+08:00</updated>
    <id>/2008/05/28/catching-up-on-the-world/</id>
    <content type="html">&lt;p&gt;The last few weeks have been pretty full, but I’m finally starting to catch up, inbox not zero, but getting there. Google Reader down to a merely terrifying few thousand articles. Bills paid, letters written, chickens roasted. Anyway.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;http://woss.name/&quot;&gt;Mathie&lt;/a&gt; is &lt;a href=&quot;http://woss.name/2008/04/17/history-meme/&quot;&gt;interested in my command history&lt;/a&gt;. Here it is.&lt;/p&gt;

&lt;p&gt;My iMac, where I do the development for things like my &lt;a href=&quot;/2008/05/28/xmpp4r-simple-makes-xmpp-in-ruby-uhh-simple&quot;&gt;Ruby Jabber thingy&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  108  cd
   69  ls
   46  ssh
   31  ruby
   28  ./jabber.rb
   22  tail
   20  svn
   19  find
   19  dig
   18  cap
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A lot of looking at logs, running Ruby scripts, and deploying applications.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;http://code.xeriom.net/&quot;&gt;server&lt;/a&gt; I’ve most recently been working on:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  181  sudo
   82  ls
   52  tail
   39  cd
   33  ps
   16  cat
   13  god
   11  top
   10  nano
    8  nohup
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Mostly running things as root via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo&lt;/code&gt; and staring at logs and processes. Riveting stuff.&lt;/p&gt;

&lt;p&gt;Those two machines aren’t hugely exciting, and unfortunately I can’t show my work laptop because that would (a) require me to hunt down my backpack and power up the machine, and (b) produce results that I’m not sure I can share.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;http://semantici.st/&quot;&gt;John&lt;/a&gt; and &lt;a href=&quot;http://blog.timperrett.com/&quot;&gt;Tim&lt;/a&gt;, what dark secrets lurk in your histories?&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Getting started with Rails 2.0</title>
    <link href="/2008/03/24/getting-started-with-rails-20/"/>
    <updated>2008-03-24T00:00:00+09:00</updated>
    <id>/2008/03/24/getting-started-with-rails-20/</id>
    <content type="html">&lt;p&gt;Rails has changed quite a lot since &lt;a href=&quot;http://www.pragprog.com/titles/rails2&quot;&gt;Agile Web Development with Ruby on Rails (2nd Ed)&lt;/a&gt; was released. A number of new best practices have emerged, and many of the techniques in the book are now outdated.&lt;/p&gt;

&lt;p&gt;To demonstrate the modern way of doing things, we need a fresh Rails application to build on. In this article I’ll walk you through setting up and running a Rails 2 project on your Mac. Future articles will build on this foundation.&lt;/p&gt;

&lt;h4 id=&quot;in-this-article&quot;&gt;In this article&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#installing-rails-2-0&quot;&gt;Installing Rails 2.0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#starting-your-rails-2-0-project&quot;&gt;Starting your Rails 2.0 project&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-importance-of-version-control&quot;&gt;The importance of version control&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#working-with-the-database-models&quot;&gt;Working with the database: Models&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#creating-dynamic-pages-views&quot;&gt;Creating dynamic pages: Views&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#hooking-up-the-view-and-the-model-controllers&quot;&gt;Hooking up the view and the model: Controllers&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#what-next&quot;&gt;What next?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id=&quot;installing-rails-2-0&quot; name=&quot;installing-rails-2-0&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;installing-rails-20&quot;&gt;Installing Rails 2.0&lt;/h4&gt;

&lt;p&gt;Rails 2 uses SQLite as its development and test database by default, so you don’t need to worry about setting up MySQL on your development machine. That makes getting started &lt;em&gt;much&lt;/em&gt; easier, we just need Ruby and the Rails code.&lt;/p&gt;

&lt;p&gt;To simplify the installation, we’ll use &lt;a href=&quot;http://macports.org/&quot;&gt;MacPorts&lt;/a&gt;. If you don’t have it installed, go do that first. You’ll also need XcodeTools, grab it from your OS X install media if you skipped it during the initial setup.&lt;/p&gt;

&lt;p&gt;Open Terminal.app (it’s in Applications / Utilities) and install RubyGems, which will pull in Ruby as a dependency:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;port &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;rb-rubygems
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once that finishes (it might take a while), use RubyGems to install Rails:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;gem &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; rails
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it. You now have a working Rails installation.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;starting-your-rails-2-0-project&quot; name=&quot;starting-your-rails-2-0-project&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;starting-your-rails-20-project&quot;&gt;Starting your Rails 2.0 project&lt;/h4&gt;

&lt;p&gt;First, let’s create a tidy place to keep all your projects. I call mine &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sandbox&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; ~/sandbox
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now create your project. I’m calling mine QuickBite:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/sandbox/
rails QuickBite
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You’ll see a bit of output scroll past as Rails generates the project skeleton. Let’s fire it up and see what we’ve got:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;QuickBite
ruby ./script/server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Open a browser and visit &lt;a href=&quot;http://localhost:3000/&quot;&gt;http://localhost:3000/&lt;/a&gt;.&lt;/p&gt;

&lt;div style=&quot;text-align: center; width: 100%;&quot;&gt;&lt;img src=&quot;/images/rails-default-index.png&quot; alt=&quot;The default Rails index page.&quot; /&gt;&lt;/div&gt;

&lt;p&gt;It’s not much to look at, but our project has a solid starting point. Let’s save our progress before we go any further.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;the-importance-of-version-control&quot; name=&quot;the-importance-of-version-control&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;the-importance-of-version-control&quot;&gt;The importance of version control&lt;/h4&gt;

&lt;p&gt;Version control does exactly what the name suggests: it lets you track different versions of your project over time. You can see when a change was made, what files it touched, who made it, and, crucially, roll back changes that didn’t work out.&lt;/p&gt;

&lt;h5 id=&quot;git&quot;&gt;Git!&lt;/h5&gt;

&lt;p&gt;The version control system I use is &lt;a href=&quot;http://git.or.cz/&quot;&gt;Git&lt;/a&gt;. It does a lot of clever things, but for now we only care about one: saving our work so we can undo it if something goes wrong.&lt;/p&gt;

&lt;p&gt;You’re free to use whatever version control system you prefer, but the commands in this article will be Git-specific.&lt;/p&gt;

&lt;h6 id=&quot;installing&quot;&gt;Installing&lt;/h6&gt;

&lt;p&gt;If you’re using MacPorts, it’s just:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;port &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;git-core
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Grab a cup of coffee, this can take a while.&lt;/p&gt;

&lt;h6 id=&quot;configuring-git&quot;&gt;Configuring Git&lt;/h6&gt;

&lt;p&gt;Since Git has just been installed, it doesn’t know who you are yet. Let’s fix that:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.name &lt;span class=&quot;s2&quot;&gt;&quot;Your Full Name&quot;&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.email &lt;span class=&quot;s2&quot;&gt;&quot;you@domain.com&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--global&lt;/code&gt; flag means these values apply to all your projects, so you only have to do this once.&lt;/p&gt;

&lt;h6 id=&quot;adding-your-rails-project-to-git&quot;&gt;Adding your Rails project to Git&lt;/h6&gt;

&lt;p&gt;There are some files we don’t want to track, development databases, logs, temp files, and so on. Create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt; file in the top-level directory of your project with the following contents:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.DS_Store
db/*.sqlite3
doc/api
doc/app
log/*.log
tmp/**/*
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Git tracks content rather than files. We’ve told it to ignore everything inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tmp/&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log/&lt;/code&gt;, but Rails expects those directories to exist. So we need to add empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt; files inside them to make sure Git keeps the directories around:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;tmp/.gitignore
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;log/.gitignore
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now let’s initialise the repository and make our first commit:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create the repository&lt;/span&gt;
git init
&lt;span class=&quot;c&quot;&gt;# Add the project to the next commit&lt;/span&gt;
git add &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Commit the changes&lt;/span&gt;
git commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Setup a new Rails application.&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Normally you wouldn’t use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git add .&lt;/code&gt; like this, because it stages &lt;em&gt;everything&lt;/em&gt; under the current directory. It’s better practice to stage specific files with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git add &amp;lt;filename&amp;gt;&lt;/code&gt; and then commit. But for the initial commit of a freshly generated project, it’s fine.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;working-with-the-database-models&quot; name=&quot;working-with-the-database-models&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;working-with-the-database-models&quot;&gt;Working with the database: Models&lt;/h4&gt;

&lt;p&gt;To build anything useful, we need to model our problem domain. Most models interact with a database, and they live in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/&lt;/code&gt;. There won’t be any there yet.&lt;/p&gt;

&lt;p&gt;Our application is called QuickBite, it’s going to be a place for exchanging sandwich recipes. Sandwiches usually have a name (BLT, New York Deli, that sort of thing), so let’s generate a model:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ruby ./script/generate model Sandwich name:string
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This creates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/sandwich.rb&lt;/code&gt; along with a migration file. Migrations are instructions that tell Rails how to modify your database, adding tables, columns, indexes, and so on. Let’s run it:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Run any pending migrations&lt;/span&gt;
rake db:migrate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Rails now knows about sandwiches. Time to commit our progress:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Look to see what&apos;s been changed&lt;/span&gt;
git status

&lt;span class=&quot;c&quot;&gt;# Stage the models, migrations, schema and tests&lt;/span&gt;
git add app/models/ db/migrate/ db/schema.rb &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;/

&lt;span class=&quot;c&quot;&gt;# Commit the changes with a message&lt;/span&gt;
git commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Added a model to represent sandwiches.&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The generator also created some test files. I’m going to be a bad man and skip those for now, testing deserves its own article.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;creating-dynamic-pages-rails-views&quot; name=&quot;creating-dynamic-pages-views&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;creating-dynamic-pages-views&quot;&gt;Creating dynamic pages: Views&lt;/h4&gt;

&lt;p&gt;Rails knows about sandwiches, but that doesn’t help us get data into the database or show it to visitors. We need views, and they live in subdirectories of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/sandwiches/new.html.erb&lt;/code&gt; with a simple form for creating sandwiches:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/sandwiches/create&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  Name: &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sandwich[name]&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;submit&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Save&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/sandwiches/show.html.erb&lt;/code&gt; to display a sandwich:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This sandwich is called &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@sandwich&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;.&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Three things to notice about that show view:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;ERb output blocks (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;%= ... %&amp;gt;&lt;/code&gt;) are used wherever dynamic content should appear in the HTML.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt; helper sanitises the output, escaping any HTML that could mess up our page or cause security issues.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@sandwich&lt;/code&gt; variable starts with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&lt;/code&gt;, this is how data gets passed from the controller to the view. Don’t worry about why just yet, but do take note.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s try it out. Visit &lt;a href=&quot;http://localhost:3000/sandwiches/new&quot;&gt;http://localhost:3000/sandwiches/new&lt;/a&gt;.&lt;/p&gt;

&lt;div style=&quot;text-align: center; width: 100%;&quot;&gt;&lt;img src=&quot;/images/no-new-route.png&quot; alt=&quot;Rails complains that there&apos;s no route matching /sandwiches/new&quot; /&gt;&lt;/div&gt;

&lt;p&gt;An error! Rails doesn’t know what to do with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/sandwiches/new&lt;/code&gt; yet. We need a controller. But first, let’s commit our views:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Check what&apos;s changed&lt;/span&gt;
git status
&lt;span class=&quot;c&quot;&gt;# Stage the views for the next commit&lt;/span&gt;
git add app/views/sandwiches
&lt;span class=&quot;c&quot;&gt;# Commit the changes&lt;/span&gt;
git commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Added create and show views for sandwiches.&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a id=&quot;hooking-up-the-view-and-the-model-controllers&quot; name=&quot;hooking-up-the-view-and-the-model-controllers&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;hooking-up-the-view-and-the-model-controllers&quot;&gt;Hooking up the view and the model: Controllers&lt;/h4&gt;

&lt;p&gt;Controllers tell Rails what to do when a browser requests a page or a form submits data. They’re the glue between views and models, and they live in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/sandwiches_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SandwichesController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@sandwich&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Sandwich&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:sandwich&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:action&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;show&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@sandwich&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;show&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Notice that this variable starts with an @ to match the view.&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@sandwich&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Sandwich&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now visit the &lt;a href=&quot;http://localhost:3000/sandwiches/new&quot;&gt;new sandwich form&lt;/a&gt; again. This time you should see your form, and when you hit Save, you’ll be taken to the show page for the sandwich you just created.&lt;/p&gt;

&lt;div style=&quot;text-align: center; width: 100%;&quot;&gt;&lt;img src=&quot;/images/sandwich-record.png&quot; alt=&quot;A sandwich record is shown in the browser window&quot; /&gt;&lt;/div&gt;

&lt;p&gt;It works! Our application can create sandwiches and display them. One last commit for this article:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Look to see what&apos;s been changed&lt;/span&gt;
git status
&lt;span class=&quot;c&quot;&gt;# Stage the sandwiches controller&lt;/span&gt;
git add app/controllers/sandwiches_controller.rb
&lt;span class=&quot;c&quot;&gt;# Commit the changes with a message&lt;/span&gt;
git commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Added a controller for sandwiches.&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a name=&quot;what-next&quot; id=&quot;what-next&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;what-next&quot;&gt;What next?&lt;/h4&gt;

&lt;p&gt;You’ve installed Rails 2, created a project, and built a basic application that can create and display sandwich records. Not bad for a first pass.&lt;/p&gt;

&lt;p&gt;In the next article we’ll flesh things out: an index page so visitors can browse sandwiches, editing and deleting, and layouts and partials to DRY up the views and make everything look good. Stay tuned!&lt;/p&gt;

&lt;p&gt;If you’ve found this article useful, I’d appreciate a recommendation at &lt;a href=&quot;http://www.workingwithrails.com/recommendation/new/person/7241-craig-webster&quot;&gt;Working With Rails&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You might also be interested in my Rails tutorial at the &lt;a href=&quot;http://scotlandonrails.com/tutorial&quot;&gt;Scotland on Rails Charity Day&lt;/a&gt; in Edinburgh on April 3rd, 2008.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Syndication Notice: A TextPattern Plugin</title>
    <link href="/2008/03/10/syndication-notice-a-textpattern-plugin/"/>
    <updated>2008-03-10T00:00:00+09:00</updated>
    <id>/2008/03/10/syndication-notice-a-textpattern-plugin/</id>
    <content type="html">&lt;p&gt;I recently stumbled across the blog of Mike Davidson. I can’t remember how, and while there are plenty of interesting articles there, the one that caught my eye was about &lt;a href=&quot;http://www.mikeindustries.com/blog/archive/2007/08/adding-a-subscribe-bar-to-your-blog&quot;&gt;people not using syndication feeds&lt;/a&gt;. Apparently many readers just visit a blog every now and then to check for new content, unaware that RSS and Atom feeds exist.&lt;/p&gt;

&lt;p&gt;Mike’s solution is beautifully simple: add a message at the top of the page explaining that feeds are available and what they do. I liked it enough to turn it into a TextPattern plugin. You can grab it with Git:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone http://barkingiguana.com/~craig/syndication_notice.git/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Installation is straightforward, if a bit spartan on documentation at the moment. You’ll need to modify TextPattern’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;publish/rss.php&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;publish/atom.php&lt;/code&gt; scripts for proper feed integration, specifically, append &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?source=feed&lt;/code&gt; to the end of each href. If there’s demand, I’ll write up more detailed instructions for the next release.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>I'm talking at Scotland on Rails</title>
    <link href="/2008/03/09/im-talking-at-scotland-on-rails/"/>
    <updated>2008-03-09T00:00:00+09:00</updated>
    <id>/2008/03/09/im-talking-at-scotland-on-rails/</id>
    <content type="html">&lt;p&gt;From the 3rd to the 5th of April 2008, &lt;a href=&quot;http://scotlandonrails.com/talks&quot;&gt;Scotland on Rails&lt;/a&gt; is bringing together a fantastic lineup of &lt;a href=&quot;http://scotlandonrails.com/speakers&quot;&gt;speakers&lt;/a&gt; covering a huge range of topics.&lt;/p&gt;

&lt;p&gt;I’ll be at the &lt;a href=&quot;http://www.scotlandonrails.com/tutorial&quot;&gt;charity tutorial day&lt;/a&gt;, giving an introduction to Rails for those who haven’t worked with it before. I’ll also be covering some best-practice techniques, so even if you already know Rails, come along, there should be something for everyone.&lt;/p&gt;

&lt;p&gt;There are still places available to &lt;a href=&quot;http://scotlandonrails.com/register&quot;&gt;register&lt;/a&gt;, but they’re limited, so be quick!&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Goodbye Kiwi</title>
    <link href="/2008/03/05/goodbye-kiwi/"/>
    <updated>2008-03-05T00:00:00+09:00</updated>
    <id>/2008/03/05/goodbye-kiwi/</id>
    <content type="html">&lt;p&gt;Today, the very first server we ever commissioned was pulled from the data centre and retired.&lt;/p&gt;

&lt;p&gt;kiwi.xeriom.net (or marmaduke.xeriom.net as it was known before 2005) gave us three years of brilliant service. It started life as a shared hosting node and later found its calling as a log server. I personally spent hours tinkering with that box, trying to get everything just right, and it was a great bit of kit.&lt;/p&gt;

&lt;p&gt;Thank you, little dude. It’s been a blast.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Don't rewrite UserDir requests</title>
    <link href="/2008/02/25/dont-rewrite-userdir-requests/"/>
    <updated>2008-02-25T00:00:00+09:00</updated>
    <id>/2008/02/25/dont-rewrite-userdir-requests/</id>
    <content type="html">&lt;p&gt;I run a site that’s a Rails application, but I also want it to double as my personal home on the web, a place to share files and side projects. Since I use Apache, the easiest way to do that is with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UserDir&lt;/code&gt; module, which serves content from user home directories under URLs like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/~craig/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem is that my Apache configuration uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mod_rewrite&lt;/code&gt; to funnel all requests through Rails (after checking for cached files). That catches UserDir requests too, which is not what I want.&lt;/p&gt;

&lt;p&gt;For my own future reference, here’s the one-liner that fixes it:&lt;/p&gt;

&lt;div class=&quot;language-apache highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Don&apos;t rewrite UserDir requests&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;RewriteRule&lt;/span&gt; ^/~.*$ - [L]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt; means “don’t rewrite,” and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[L]&lt;/code&gt; flag tells mod_rewrite to stop processing further rules. Any request starting with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/~&lt;/code&gt; gets left alone, and everything else continues through to Rails as normal.&lt;/p&gt;

&lt;p&gt;All hail the mighty &lt;a href=&quot;http://www.addedbytes.com/apache/mod_rewrite-cheat-sheet/&quot;&gt;mod_rewrite cheat sheet&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>State of Ruby / Xen APIs</title>
    <link href="/2008/02/24/state-of-ruby-xen-apis/"/>
    <updated>2008-02-24T00:00:00+09:00</updated>
    <id>/2008/02/24/state-of-ruby-xen-apis/</id>
    <content type="html">&lt;p&gt;I’ve been spending a lot of time lately building a management interface for Xen VMs inside a Rails application. The current state of Xen’s APIs is poorly documented, which makes implementation… let’s say &lt;em&gt;character-building&lt;/em&gt;. The best comparison I’ve been able to find comes from Ewan Mellor on the Xen mailing list:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;xend-http-server:&lt;/strong&gt; Very old and totally broken HTML interface and legacy, generally working SXP-based interface, on port 8000.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;xend-unix-server:&lt;/strong&gt; Ditto, using a Unix domain socket.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;xend-unix-xmlrpc-server:&lt;/strong&gt; Legacy XML-RPC server, over HTTP/Unix, the recommended way to access Xend in 3.0.4.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;xend-tcp-xmlrpc-server:&lt;/strong&gt; Ditto, over TCP, on port 8006.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;xen-api-server:&lt;/strong&gt; All new, all shiny Xen-API interface, available in preview form now, and landing for 3.0.5.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;p&gt;. Ewan Mellor, 2007-01-24&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As far as I can tell, if you’re running Xen 3.0.4 or earlier, your best bet is the &lt;a href=&quot;http://ruby-xen.rubyforge.org/&quot;&gt;Ruby-Xen gem&lt;/a&gt; which wraps the legacy XML-RPC API. If you’re on Xen 3.0.5 or later, the preferred approach is the new Xen API, which at the time of writing has virtually no documentation and no existing Ruby client.&lt;/p&gt;

&lt;p&gt;A useful paper discussing the Xen API is available &lt;a href=&quot;http://research.iu.hio.no/theses/pdf/master2007/ingard.pdf&quot;&gt;here&lt;/a&gt;, which suggests the new API also uses XML-RPC under the hood.&lt;/p&gt;

&lt;p&gt;Over the coming weeks I’m hoping to set up some modern Xen dom0s and start documenting exactly what’s needed to get the Xen API running and accessible from another host on the same network.&lt;/p&gt;

&lt;div class=&quot;update&quot;&gt;
  &lt;p class=&quot;when date&quot;&gt;Update, 2008-03-15&lt;/p&gt;
  &lt;p&gt;I found a &lt;em&gt;draft&lt;/em&gt; specification of the new XML-RPC API on the &lt;a href=&quot;http://wiki.xensource.com/xenwiki/XenApi?action=AttachFile&amp;amp;do=get&amp;amp;target=xenapi-1.0.0.pdf&quot;&gt;XenSource Wiki&lt;/a&gt;. A huge cake awaits anyone who writes a BSD or MIT licensed Ruby client against that spec.&lt;/p&gt;
&lt;/div&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>The end of the world is nigh</title>
    <link href="/2008/01/29/the-end-of-the-world-is-nigh/"/>
    <updated>2008-01-29T00:00:00+09:00</updated>
    <id>/2008/01/29/the-end-of-the-world-is-nigh/</id>
    <content type="html">&lt;p&gt;According to Ruby, the world ends (or at least gets seriously reshuffled) on Tuesday the 19th of January 2038, at 7 seconds past 3:14 AM UTC.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;utc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2038&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;19&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;999999&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Tue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Jan&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;19&lt;/span&gt; &lt;span class=&quot;mo&quot;&gt;03&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mo&quot;&gt;07&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;UTC&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2038&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;utc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2038&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;19&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;999999&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;succ&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Fri&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Dec&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;13&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;45&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;52&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;UTC&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1901&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One second after that moment, and we’re back in 1901. Surprise!&lt;/p&gt;

&lt;p&gt;This is the classic &lt;a href=&quot;https://en.wikipedia.org/wiki/Year_2038_problem&quot;&gt;Year 2038 problem&lt;/a&gt;. Time is stored as a signed 32-bit integer counting seconds from the Unix epoch, and that integer overflows right at this boundary.&lt;/p&gt;

&lt;p&gt;What’s a bit surprising is that Ruby doesn’t handle this more gracefully. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Fixnum&lt;/code&gt; automatically promotes to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bignum&lt;/code&gt; when it exceeds &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2**30&lt;/code&gt;, so you might expect &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Time&lt;/code&gt; to pull off a similar trick internally. But it doesn’t, at least not in the Ruby versions of this era. The internal representation just wraps around, catapulting you back to the early 20th century without so much as a warning.&lt;/p&gt;

&lt;p&gt;Something to keep in mind if you’re ever working with dates far into the future.&lt;/p&gt;
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Class, Instance and Singleton methods</title>
    <link href="/2007/12/20/class-instance-and-singleton-methods/"/>
    <updated>2007-12-20T00:00:00+09:00</updated>
    <id>/2007/12/20/class-instance-and-singleton-methods/</id>
    <content type="html">Ruby has three kinds of methods, and I always mix up the terminology. So here, mostly for my own future reference, is a quick rundown of each.

#### Class methods

These are methods you call directly on the class itself. No instance required.

```ruby
Time.now
Monkey.find(:all)
```

#### Instance methods

These are methods available on *any* instance of a class. Every object of that type gets them.

```ruby
@widget.to_s
Time.now.to_f
```

#### Singleton methods

These are methods defined on one *specific* object. No other instance of that class will have them -- they belong to that particular object alone.

```ruby
chicken = Chicken.new
class &lt;&lt; chicken
  def hide
    # ...
  end
end
chicken.hide
```

Here, only this particular `chicken` can `hide`. Create another `Chicken.new` and it won&apos;t know what you&apos;re talking about.

The key distinction is scope: class methods live on the class, instance methods live on every object of that class, and singleton methods live on exactly one object. Once you internalise that, it all clicks into place.
</content>
  </entry>
  
  
  
  
  <entry>
    <title>Showing multiple message types with the flash</title>
    <link href="/2007/12/15/showing-multiple-message-types-with-the-flash/"/>
    <updated>2007-12-15T00:00:00+09:00</updated>
    <id>/2007/12/15/showing-multiple-message-types-with-the-flash/</id>
    <content type="html">Most Rails developers use the flash to store a single message -- something like `flash[:message]` -- and call it a day. But what if you want to tell a user their widget was saved *and* give them a heads-up that they&apos;re approaching a limit? Lumping everything into one message with one style isn&apos;t great.

Good news: the flash is a `HashWithIndifferentAccess`, which means you can use whatever keys you like. Let&apos;s put that to work.

In your controller, just pick the key that best describes what you&apos;re communicating:

```ruby
if @widget.save
  flash[:info] = &quot;Your widget has been saved.&quot;
  flash[:notice] = &quot;There are now #{Widget.count} widgets.&quot;
  redirect_to @widget and return
else
  flash.now[:warning] = &quot;I couldn&apos;t save your widget.&quot;
  render :action =&gt; &quot;edit&quot;
end
```

Then in your view, iterate over the flash and render each message with its own class:

```ruby
&lt;%= flash.sort.collect do |level, message|
  content_tag(:p, message, :class =&gt; &quot;flash #{level}&quot;, :id =&gt; &quot;flash_#{level}&quot;)
end.join %&gt;
```

If that `@widget` was saved successfully, you&apos;d get clean, easily styled markup:

```html
&lt;p class=&quot;flash info&quot; id=&quot;flash_info&quot;&gt;Your widget has been saved.&lt;/p&gt;
&lt;p class=&quot;flash notice&quot; id=&quot;flash_notice&quot;&gt;There are now 29 widgets.&lt;/p&gt;
```

Each message type gets its own CSS class, so you can style warnings differently from informational notices. A little CSS and your users will always know exactly what kind of feedback they&apos;re getting.

In the interest of readability, that flash loop in the view really ought to be extracted into a helper -- but I&apos;ll leave that as an exercise for the reader.
</content>
  </entry>
  
  
</feed>
