The Orchestration Prenup - A Framework Choice You Can Still Walk Away From

The Orchestration Prenup - A Framework Choice You Can Still Walk Away From

The problem

I've watched the same six weeks play out on an internal ticket-triage feature more than once. Week one, the requirement is narrow: classify an incoming ticket and route it to a queue, one call to the model, done. An engineer wires that up directly against whatever orchestration framework is sitting on the team's laptop already. Ticket parsing, routing rules, prompt construction, all of it gets written straight against that framework's primitives, scattered across the route handlers and the routing-rule module. Nobody drew a boundary between what the feature does and which engine happens to be running it, because at that point there was no reason to think it mattered.

Then week six arrives with the requirement nobody scoped for: high-severity tickets now need a branching escalation step and a human-approval gate before they route anywhere. That's multi-step state the original implementation was never built to hold. The team bolts the new state and conditional edges on top of what's there, and because there was never a seam between the business rules and the orchestration calls, nearly every file that touches routing has to be touched again to make room for the branch. What should have been a contained addition turns into a migration that runs through the whole codebase, because the framework choice was never treated as a decision that might need to be reversed. It was treated as a foundation, poured once, in concrete, on day one.

The frustrating part is that the cost of that choice is invisible right up until the moment it isn't. Nobody pays for it at commitment time. The bill comes due months later, disguised as an ordinary feature request that turns out to require touching everything.

The pattern

The name says what I mean by it. The orchestration engine you pick, whatever framework sequences your steps and hands off between them, is a bet. A bet you can walk away from is a fundamentally different animal than a foundation you've already poured. The mechanism that keeps it walkable is an interface, call it an OrchestrationPort, sitting between the business logic and whatever engine actually runs it, with a pilot in a second framework built and validated before anyone commits deeply to the first.

Concretely: the triage logic lives entirely behind one method, something like run_triage(ticket) -> TriageResult. A first adapter implements that port against the framework the team already knows. Before that ships, though, a second adapter gets built against a different framework, one capable of the branching and state-holding the first one wasn't designed for. Nobody has to use the second adapter yet. It exists as a documented, working alternative that nobody was forced to commit to.

When the branching requirement lands, the change is a new adapter picking up production traffic. The route handlers never move. The routing-rule module never moves. The port's contract stays fixed at "return a result," the same thing it committed to on day one, so there's nothing in that contract for a framework swap to disturb. I like to measure this the blunt way: count files touched, lines changed, and orchestration-specific imports touched for the exact same requirement, once against a codebase with no seam and once against a codebase with the port in place. Build the seam correctly and that number for the ported version should come in at least 70 percent lower than the coupled baseline, with the blast radius isolated almost entirely to the new adapter file and its registration. That's not a number I pulled off a production migration I lived through. It's the bar I hold my own implementations to when I check whether the seam actually did its job.

The deeper insight underneath the mechanics is one Parnas named in 1972 long before anyone was orchestrating LLM calls: isolate the decision most likely to change behind an interface that doesn't change with it. An orchestration framework is exactly the kind of decision that changes on a timescale of months while the business logic underneath it changes far more slowly. Coupling the two means every framework-level change drags the business logic along for the ride, whether it wanted to move or not.

Design considerations

The pattern only pays for itself if someone actually spends the time to build and validate that second adapter before it's needed. That's real engineering hours spent on a pilot that might never see production traffic, and it's asking for that investment from exactly the team least likely to have slack for it, the same team under enough time pressure that they wired the first version directly against the framework in the first place. The payoff is deferred. The cost is upfront. That mismatch, on its own, explains why this pattern gets skipped more often than it gets adopted.

A port also only insulates you against one specific kind of change: which engine executes the orchestration. It says nothing about a change in what the business logic itself needs to do. If the actual triage rules need to get smarter, more nuanced, aware of new fields, that work still has to be written by hand behind the port either way. The pattern narrows the blast radius of a framework swap. It does nothing for the blast radius of a logic change.

There's a second limitation that only shows up after you've lived with a port for a while, and it's the one I find more interesting because it's less obvious going in. The interface you draw around v1's needs can quietly become the wrong shape once v2 arrives. run_triage(ticket) -> TriageResult is a clean contract for a single classify-and-route call, and it stays clean even once branching and an approval gate move inside the second adapter, because the port never had to know about the branch, only about the eventual result. That won't hold for every future requirement, though. Draw the port too narrowly around what v1 needed to return, and it can end up needing its own migration once a later requirement asks for something the original result shape was never built to carry, streaming partial results mid-classification, say, instead of one final answer. The port protects you from one category of lock-in. Every other interface decision you make on day one is still yours to get wrong.

None of that argues against building the seam. It argues for being precise about what the seam buys. If your team is genuinely confident the orchestration framework you're starting with will never need to change, because the workflow is simple and stable and staying that way, the extra adapter layer is overhead without a corresponding return. But state that confidence out loud and check it against the actual requirement history of the feature. Writing the port feels like more work than shipping the demo, which is exactly why most teams skip it by omission instead of by evaluation, and the decision by omission is the one that turns an ordinary feature request into a six-week rewrite.