Pre-Shipment: You Sell a Promise, You Ship a Reality
· tech
#war-story#live-commerce#fulfillment
📑 Contents
- Two stock books: you sell a promise, what arrives is reality
- Allocation: mapping the promise onto reality
- The system’s boundary: it ends at the shipping note
- The rebuild: three steps to split fulfilment out
- Reflections
- A system’s boundary is drawn where your responsibility ends
- Mechanism to the system, policy to people — that’s three times now
- The best architectural evolution turns a seam that already exists into an interface
The money is in; time to ship. This chapter is where the system meets the physical world — comments, stock and payments all live in a database, but goods are real boxes, a real warehouse, a real delivery driver. Which makes it the chapter where the system’s boundary is drawn most deliberately: what the system owns and what is handed to people. The answer we had was smarter than I remembered.
Two stock books: you sell a promise, what arrives is reality
Get the sequence straight first: during the stream the host agrees with the supplier on “how much there is” — that number becomes the cap in the stock chapter, and the system uses it to guard against overselling. But the actual purchasing only starts after the stream ends; purchasing itself isn’t in the system, and the next fact the system sees is operations entering what physically arrived as a goods-in record — appended one at a time, so every stock adjustment has a log.
So this system really has two stock books, and they’re two separate columns on the inventory table, each with its own job:
- Sales stock (the promise): the number the host called. Its job is guarding “sold ≤ cap” at the instant of ordering, and it lives in millisecond transactions.
- Physical stock (reality): what the goods-in records accumulate. Its job is honestly recording what’s actually in the warehouse, and it lives on logistics’ daily rhythm.
The two books drift by nature — 100 agreed and 80 delivered (a short shipment), or damage, or the wrong spec. That isn’t a bug, it’s the normal condition of the personal-shopping business. There’s only one question: once they’ve drifted, whose orders have goods and whose have to wait?
Allocation: mapping the promise onto reality
The answer we had was an allocation system: assigning the stock that actually arrived to the orders that were promised.
Three designs in the allocation system, each worth a pause:
- The allocation log is append-only — stock movements can be rebuilt from the full set of events. Goods-in is an event, allocation is an event, and the complete set of facts can replay the current state at any time: drifted books have something to reconcile against, complaints have evidence, stock counts have a baseline. That log is the anchor on the physical side.
- Mechanism belongs to the system, policy to people. What the allocation system provides is a mechanism that lets you assign however you like, plus a record of every assignment; how to assign — who gets sacrificed on a short shipment, who ships first — is operations’ commercial judgment. The system doesn’t overreach into making policy for people, but it turns every policy decision into a traceable fact. That’s the third landing of the same philosophy as the state machine getting ripped out and refunds going through the bank.
- The rule is simple enough not to go wrong: a shipping note is created only when the order is fully allocated, and the items go together. Nothing is split, so there’s no “half-split” intermediate state. When to ship is mostly decided by operations based on how allocation is going, and an impatient customer calls support — mechanism and policy again.
The system’s boundary: it ends at the shipping note
The most instructive thing here is what the system chose not to do: there were in fact several warehouses and the system didn’t manage them; how picking is done, whether there are barcodes, what happens on a shortfall — none of the system’s business. The system’s responsibility ends at “order becomes shipping note”, and after that — convenience-store pickup through an API (the customer picks a store on the web) and home delivery through a hand-exported CSV to the courier — the rest is operations’ world. Shipping fees and their conditions are configured at the round layer, in the same category as coupons and settlement; rounds prove themselves this system’s natural configuration boundary for the third time.
There’s logic to where that line sits: it falls exactly on the boundary between the information flow and the physical flow. Get the information wrong and you oversell or take the wrong money — high cost of breach, invisible to the eye, so the system must guard it. Get the physical wrong (wrong item picked, a box short) and it’s visible on site and fixable on the spot — people guard that better than a system. Six engineers’ complexity budget, spent where it cuts, again.
The rebuild: three steps to split fulfilment out
The shape we had worked, but there’s one structural awkwardness: the cart chapter said an order is split by round and is the unit of fulfilment — which means payment already spans rounds while shipping is still bound to one. A customer buys from three rounds and wants one box? The structure doesn’t cooperate. The rebuild takes three steps, each deeper than the last:
- Devolve the unit of fulfilment to the order item. Whichever item is allocated becomes eligible to ship — “wait for everything” turns from a structural limit into an operational policy: wait for the set, or ship what’s arrived, is only a decision about whether to include that item when grouping. The order falls back to being pure accounting grouping (round coupons and reconciliation unchanged), and accounting doesn’t move at all — an order item was always the frozen-amount accounting unit, so devolving fulfilment actually puts fulfilment and accounting at the same granularity.
- Promote shipment to a first-class entity, grouped by “recipient + address”. Cross-round merged shipping happens naturally and partial arrivals split naturally, with no special logic. There are three costs, and writing them down is the honest thing to do: shipping-fee policy has to be redefined (rules stay at the round layer, calculation moves to the shipment — a parcel is what actually generates a fee); the address is the grouping key, so it has to be frozen at the moment of grouping — the commitment-point principle for the second time (the first was freezing the price at sale); and the customer’s unit of enquiry moves from order to shipment, so the storefront’s and support’s narrative has to follow.
- Split the whole fulfilment leg into a “shipping system”. The commerce system ends at exporting fulfillable orders; the shipping system consumes orders and owns goods-in, allocation, shipping notes and all courier integration, with dispatch and returns flowing back to commerce as events — and commerce follows its usual rules of landing facts and deriving status. The cost has to be written down too: the two stock books formally separate — sales stock stays in commerce to guard overselling, physical stock follows the shipping system, and the two reconcile through the events flowing back. That step sounds radical, but two things from back then support it: the hand-exported CSV to the courier was the manual prototype of exactly this interface — that boundary had already run on human power for years; and the project’s endgame split the monolith into commerce, bidding and purchasing services using the same instinct about boundaries, so shipping is amply qualified to be the fourth. The real payoff is at the end: once the interface is “import orders”, the shipping system becomes a replaceable consumer — run it yourself, switch to a 3PL, or mix, with commerce not moving a line. The compound interest of a good boundary is that even “should we build this ourselves?” becomes a decision you can reverse at any time.
Reflections
A system’s boundary is drawn where your responsibility ends
No warehouse management, no picking system, shipping by hand-exported CSV — younger me would have filed all of that under “technical debt”. Now I think it’s self-knowledge: the system guards the information flow (getting it wrong means overselling or wrong money, invisible to the eye), and people guard the physical flow (getting it wrong is visible and quickly fixed). Force the system into the warehouse and you’re taking on barcode hardware, stock-count discrepancies and how staff actually work — every one a new source of complexity, in exchange for correctness people were already holding. A boundary isn’t the limit of your capability, it’s the limit of your responsibility: draw it where you can answer for the mistakes, not where the technology could reach.
Mechanism to the system, policy to people — that’s three times now
The state machine ripped out by hosts, refunds going through the bank, allocation done “the way they want it” — the same philosophy landing three times in this system, and not one of them a compromise. Each is the correct division: systems are good at recording and holding invariants, people are good at judgment and absorbing exceptions. The cleverest thing about the allocation system is that it doesn’t try to turn a commercial judgment like “who gets sacrificed on a short shipment” into an algorithm, while turning every judgment into a traceable event in the allocation log — people free, books clear. That’s more honest than a “fully automatic allocation engine” and more reliable than “sorting it out in a spreadsheet”; it sits in the sweet spot between the two.
The best architectural evolution turns a seam that already exists into an interface
“Splitting fulfilment into a shipping system” sounds like a big refactor, but look closely: that interface — a batch of fulfillable orders — already existed as a hand-made CSV, and it ran steadily. The rebuild only formalises a cut that human beings had already worn into the system. This is what I believe most about architectural evolution: good boundaries aren’t designed, they’re observed. All those long-lived manual processes — the report exported every week, the file operations always sends to the same person — are interfaces nobody has admitted to yet. If you want to know where a system should split, don’t book a whiteboard session; go and look at where people are already handing things over.