← Writing

Case study

Ship the factory, not the service

What happens when you stop hand-writing one backend service per capability and instead build a factory that generates the plumbing from a spec, moves the business logic into skills a domain expert can author, and gates the whole thing on evals. The engineering job moves from writing services to building the thing that lets other people write them safely.

RolePlatform architecture and build
When2026
Stack
TypeScriptMCPspec-driven codegeneval-gated CIOpenTelemetry

Every backend service is two things wearing one coat. Underneath is the business logic, the part a domain expert would recognize as the actual work: when to refer this, what counts as complete, which exception overrides which rule. On top of that is the machinery, the part no domain expert should ever have to think about: how the service boots, how it authenticates, which queue it drains, what event it emits, how it deploys. For most of the history of backend work those two things ship together, in the same repository, written by the same person, and the person is an engineer. So every time the business wants to do something new, an engineer has to build a whole service to carry a small amount of logic, and every time the logic needs to change, it waits in an engineer's queue.

Past a certain size that arrangement stops scaling, and not for the reason people expect. The bottleneck isn't compute or even headcount. It's translation. The person who knows the work and the person who can make the computer do the work are different people, and every change has to cross that gap twice, once to explain it and once to verify the engineer understood. This is a case study about trying to close the gap by attacking the coat itself: separating the logic from the machinery, generating the machinery, and letting the people who own the logic author it directly. It worked more than I expected and broke in ways I should have expected, and the interesting part is the guardrails, because the guardrails are the whole thing.

The old shape

The starting point was a codebase that had grown the way successful codebases do: one service at a time, each one reasonable in isolation, the collection quietly becoming a problem. Dozens of services, each with its own slightly different take on the same problems, because they'd been written by different people at different times under different deadlines. Auth done four ways. Eventing done three ways. The same domain concept modeled subtly differently in five places, so that moving data between them meant a translation layer nobody wanted to own.

None of that was anyone's fault. It's what you get when the unit of work is "write a service" and the person doing it is optimizing for shipping the feature in front of them, not for the twentieth service that will look like this one. The cost doesn't show up in any single service. It shows up as a tax on the whole system: every new capability pays for the absence of a standard, and the domain experts who actually understand the work can't touch any of it without filing a ticket and waiting.

Two panels. The left panel, titled The old shape, shows a domain expert handing knowledge to an engineer, who writes a bespoke service that bundles business logic and machinery together; several such services sit side by side, each drawn slightly differently to show drift. An arrow labeled every change waits on an engineer loops from the service back to the engineer. The right panel, titled The factory, shows a domain expert authoring a skill directly on the left; a spec plus a seed feed a factory in the middle that emits uniform services with the machinery generated; the authored skills plug into those services as the logic, and a row of guardrails, eval gate, linter, sandbox with tracing, and approval gate, sits under the whole thing.
The move: stop making engineers write a whole service to carry a little logic. Generate the machinery from a spec, let domain experts author the logic as skills, and put the guardrails where the risk actually is.

The bet

The bet had three parts, and they only work together.

Plant one clean seed. Instead of writing services, define exactly one canonical shape for what a service is: how it boots, how it authenticates, how it talks to the event bus, how it exposes its tools, how it deploys. Wire the continuous-integration and delivery for that shape once, correctly, with the eval and safety gates built in from the first commit rather than bolted on later. Everything downstream grows from that seed, so the standard isn't a document people are supposed to follow. It's the only path that exists.

Generate the machinery from a spec. Describe each service declaratively: its data contracts, the events it consumes and emits, the tools it needs, its auth posture. A generator turns that spec into a running service built on the seed. The infrastructure is correct by construction because no human hand-assembled it. An engineer who wants a new service writes a spec and reviews what comes out, rather than writing the twentieth slightly-different version of boot-and-auth-and-queue by hand.

Move the logic into skills. The business logic, the part the domain expert owns, comes out of the compiled service entirely and becomes a skill: a declarative, legible unit that describes how a piece of work is actually done, in language close to how you'd explain it to a sharp new hire. The service is the body; the skill is the instinct. And critically, a skill is something a domain expert can read, and eventually author, without opening the service at all.

That third part is the one that changes who's in the room. The first two are good platform engineering. The third is a claim about who gets to change how the company behaves, and it's the one that made the guardrails non-negotiable.

The part that's actually hard

Letting someone who isn't an engineer change production behavior is either the best idea in the system or a catastrophe waiting for a Tuesday afternoon, and the difference between those two outcomes is entirely in the guardrails. Here the human-in-the-loop principle I keep coming back to stops being philosophy and becomes plumbing. Four things carried the weight.

Evals gate the merge. An authored skill doesn't ship because it looks right. It ships because it passes a suite of evaluations that encode what "right" means for that piece of work, run in CI like any other test. The author gets the same fast feedback an engineer gets from a failing unit test, in a language about the work rather than about the code. No green evals, no merge. This is the single most important sentence in the system: the gate is automated, so the safety doesn't depend on an engineer being available to eyeball every change.

A linter for authored logic. Before evals even run, a linter checks the skill itself: is it well-formed, does it reference tools that exist, does it stay inside the boundaries this author is allowed to touch, does it avoid the shapes that have burned us before. It's the spell-check for business logic, and like spell-check its value is that it catches the boring mistakes cheaply so the expensive review can focus on judgment.

Every run is legible. Nothing authored runs in the dark. Each execution lands in a sandbox first, and every step, every tool call, every decision, is traced and pullable after the fact. When something goes wrong you can replay exactly what the agent saw and did, which turns "the system did something weird" from a ghost story into a bug report. That legibility also feeds a loop: a failed trace becomes the seed of the next eval, so the system's understanding of "right" sharpens every time it's wrong.

Agents propose; humans dispose. The authored logic doesn't get to quietly commit consequential actions. It proposes them, and a human ratifies the ones that matter, the same conviction that agents earn autonomy by degrees, not by default. The approval isn't friction for its own sake. It's the load-bearing step, and the design work is making sure a human's attention lands on the decisions that deserve it and nowhere else.

A left-to-right pipeline titled from authored skill to production. A domain expert authors a skill. It passes through a linter, then an eval gate, either of which can send it back with an arrow labeled fails, back to the author. If it passes, it runs in a sandbox where every step is traced. Consequential actions reach an approval gate where a human approves or holds. Only approved actions commit. A dashed arrow labeled failed traces become new evals loops from the sandbox back to the eval gate.
The guardrails are the product. A non-engineer can author production logic only because the linter, the evals, the traced sandbox, and the approval gate stand between what they wrote and what actually happens.

Proving it before generalizing

The temptation with a platform like this is to build the whole thing in the abstract and unveil it. That's how you build a factory nobody's services fit through. Instead we took a single real service and drove it end to end through the new stack first: spec it, generate it from the seed, move its logic into skills, wire its evals, run it in the traced sandbox, put the approval gate where its consequential actions were. One service, all the way, with real work flowing through it.

That was the forcing function that made the abstractions honest. Half of what I thought the spec needed turned out to be unnecessary, and a few things I'd have called edge cases turned out to be the actual shape of the work. You find that out by pushing one real thing all the way through, not by designing the general case at a whiteboard. Only once one service lived entirely inside the new model did generalizing to the next one mean something, because now "the platform works" was a claim with a witness instead of a hope.

Buy the pieces that aren't your problem

One decision worth calling out because it cut against my instincts: for the hard, well-trodden infrastructure inside this, retrieval being the clearest case, the right move was to adopt a mature hosted platform rather than build our own. There are good companies whose entire existence is making that one problem excellent, and a factory's job is to compose the best available parts correctly, not to re-derive a solved problem worse. The engineering ego wants to build everything. The engineering judgment is knowing that the value is in the seed, the specs, the skills, and the guardrails, and that everything else should be bought and wired in. Adopt, don't rebuild, unless the thing you'd rebuild is the thing that's actually yours.

The honest residue

The redesign did what it was for. It did not make the system free, and a few of the costs are structural enough that leaving them out would be dishonest.

The abstraction tax is real and it lands early. Before the factory pays for itself, you're maintaining a generator, a spec format, a seed, a linter, and an eval harness, which is strictly more surface area than "write a service." The bet only wins on the curve, when the tenth and fiftieth service ride for nearly free, and there's a stretch in the middle where you've paid the cost and haven't collected the return, and you have to hold your nerve through it. I believe the curve is real. I have also seen platform bets die in exactly that stretch.

The spec is a new language, and every new language is a wall some people bounce off. The whole promise is that domain experts author logic directly, but "directly" is doing a lot of work in that sentence. If the spec is too expressive it's just programming with extra steps and the domain expert is back to needing an engineer. If it's too rigid it can't express the real work and people route around it. That balance is not a thing you get right once; it's a thing you tune forever, and getting it wrong in either direction quietly returns you to the world you were trying to leave.

The factory can become the bottleneck it was meant to remove. Centralize how every service is born and you've also centralized a single place that can block every service's birth. If changing the seed is slow, or the generator is finicky, or the eval harness is flaky, the tax you added is now levied on everyone at once. A factory that's hard to change is worse than the sprawl it replaced, because the sprawl at least failed independently.

What I'm still not sure about

I'm not sure where the line between generated and hand-written should sit. The clean story is that everything is a spec and a skill, but real systems have a service or two whose needs are strange enough that forcing them through the factory costs more than it saves. I don't have a principled test for which those are. Right now I know it when I see it, and "I know it when I see it" is exactly the kind of judgment a platform is supposed to make unnecessary.

I'm not sure domain experts will author at the scale the whole bet depends on. A handful of motivated people authoring skills is not the same as an organization defaulting to it, and the failure mode is quiet: people keep filing tickets because the ticket is the path they trust, and the authoring surface sits there unused while the gap it was built to close stays open. The technology can be perfect and the habit can still not form. I think the habit is the actual deliverable, and it's the one I have the least control over.

And I'm not sure I've drawn the autonomy line in the right place. Every approval gate is a small admission that the system won't fully close the loop, and there's constant pressure, from me as much as anyone, to let the confident cases just commit. The argument of this whole write-up is that the guardrails are the point, and I believe it. I also notice I want to remove them on the easy days, and I don't fully trust my read of which days are easy. That's the same discomfort I wrote about in the essay on where this all goes: the interesting job is no longer writing the logic. It's building the thing that lets other people write it, and being the one who decides what stays impossible.

Comments