Stories · Season 1 — Pemberton & Crumb · Episode 01

The Spreadsheet That Ran the Warehouse

In which a six-month-old codebase becomes harder to change than the building it ships from.

The story

Pemberton & Crumb sell artisanal pickles by subscription. Wildly, improbably, they’re doing well. Dana — the entire engineering department — built the first version of the store in a heroic three-week sprint, and it worked. Orders came in. Pickles went out. Everyone was happy.

Then came feature number forty-one.

“It’s a small one,” said Crumb, who handled the business side and believed all features were small. “When someone orders more than ten jars, give them free shipping and send the warehouse a little note to pack it in the big box.”

Dana opened OrderService.cs. It was 2,400 lines long. Somewhere in there was the logic for placing an order — but it was tangled up with pricing, inventory, the email receipts, a tax calculation someone added in a panic last quarter, and a method ominously named DoTheThing(). To add free shipping, Dana had to understand all of it, because all of it ran every time an order was placed.

The “small one” took four days and broke the receipt emails twice.

Why this is hard the traditional way

The classic way to build a service feels great for about three months. You have controllers, and they call “service” classes, and the service classes call the database. Clean!

But services are a trap with no natural edges. There’s no rule that says OrderService can only be about placing one order, so it slowly becomes about everything that has ever touched an order. Each new feature adds a method, each method adds a dependency, and soon the file is a god-object that five other features lean on. Touch it for free shipping and you risk the receipt emails, because they live three feet away and share the same plumbing.

And the plumbing itself gets copy-pasted everywhere: open a transaction, log the call, check the user’s permission, validate the input — the same five lines wrapped around every meaningful one. When that plumbing needs to change, you change it in three hundred places and miss four.

How Relay changes the ending

Relay flips the unit of code from “a service that does many things” to one message, one handler, one job. Placing an order is a PlaceOrderCommand with exactly one handler. Free shipping is its own small slice. They don’t share a 2,400-line file, so they can’t break each other by accident.

// One command. One handler. Everything about "place an order" lives right here.
public sealed record PlaceOrderCommand(Guid CustomerId, IReadOnlyList<Line> Lines)
    : ICommand<OrderConfirmation>;

public sealed class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, OrderConfirmation>
{
    public async Task<OrderConfirmation> Handle(PlaceOrderCommand cmd, CancellationToken ct)
    {
        // just the business decision — no transaction boilerplate,
        // no logging, no auth checks cluttering the logic.
    }
}

The plumbing — transactions, logging, validation, authorization — moves out of the handler and into a pipeline that wraps every message the same way. You write it once. The forty-first feature is exactly as safe as the first, because it runs through the same battle-tested pipeline without you lifting a finger.

Dana’s free-shipping feature becomes a new handler in its own folder. Nobody has to open DoTheThing() ever again. (Dana deleted DoTheThing(). It was a good day.)

What it costs you to ignore this

  • Velocity decays. The team gets slower over time, not faster, as every change requires understanding more of the system.
  • Every release is a gamble. Unrelated features share code, so “small” changes cause surprise outages somewhere else.
  • Onboarding takes months. New hires can’t safely touch anything until they’ve mentally mapped the whole OrderService.
  • The cross-cutting basics rot. Auth, audit logging, and transactions get applied inconsistently because they’re copy-pasted, not centralized.