Skip to content
Engineering@Stord

Service Boundaries Should Follow the Business, Not the Integration

by Nick Traylor ยท August 26, 2026

The Boundary Was Wrong, Not the Architecture

There's a common instinct when a service needs to talk to an external system: give the integration its own service. It reads as good separation of concerns on a diagram. Whether it actually is depends on something the diagram doesn't show, whether the thing being integrated with is a business entity with its own lifecycle, or just a system you happen to be talking to.

We had a case of the second kind. A core part of our platform is the lifecycle of an order moving through fulfillment: created, confirmed, updated, canceled, shipped. Handling the call out to the warehouse system that actually executes each of those steps had been pulled into its own service, sitting between the service that owns the order lifecycle and the warehouse system itself. Clean on paper. In practice, it meant a single entity's lifecycle was now split across a process boundary, with a message-queue hop in the middle, and the service that's supposed to own that entity had no direct line into its own confirmation step.

That stopped being an abstract concern the day we needed to fix a race condition: a downstream status change needed a stable, in-process place to land a confirmation. What it had instead was a moving target on the other side of a queue hop, owned by code whose whole reason for existing was "the integration layer," not "this order's story." Fixing the actual bug meant fixing where the code lived first.

The tell that the original boundary was wrong. An earlier draft of the plan to untangle this made the exact same category of mistake again, at a smaller scale. It grouped migration work by which message-queue topic a piece of traffic happened to arrive on, rather than by which business entity that traffic actually belonged to. Two unrelated entities' events traveled over the same topic, for reasons that had nothing to do with the entities themselves and everything to do with how the messaging was wired up years earlier. "They arrive on the same topic" quietly stood in for "they're the same thing" long enough that it shaped a plan before anyone caught it. They weren't the same thing. The fix was to regroup by entity, and only then let topic-level plumbing be an implementation detail again instead of the organizing principle.

What the fix actually looked like. Once the entity was the boundary, the answer was to collapse the standalone integration service's logic into the service that owns the entity, rather than continuing to maintain two codebases with a network hop between them for no reason the business cares about. This wasn't a rewrite. Rewrites are how "fix a boundary mistake" turns into "six-month project with no working system in the middle." Instead: a phased migration, one slice of the entity's lifecycle at a time, each phase shipped behind its own flag and verified against real production traffic, starting with the lowest-risk slice of it, because no sandbox exists for the external system on the other end.

The one piece of migration mechanics worth keeping, even outside this specific case. For part of the transition, two independent services were each capable of acting on the same incoming message, and exactly one of them was allowed to, ever, for any given message. A plain boolean feature flag isn't safe for that: the two services each hold their own cache of the flag's value, and those caches don't update at the same instant. There's a real window where both sides, or neither, think they're the one responsible.

The fix was to make the flag's value a timestamp instead of a boolean. Each side compares the timestamp on the message itself (not their own local clock) against that cutover point, using the identical rule: before the cutover, service A handles it; at or after, service B does. Both sides are evaluating the same fact about the same message, so clock skew between the two services drops out of the problem entirely. What's left is a much narrower question, how long it takes a flag update to actually propagate, which is a known, boundable number instead of an open one.

The actual lesson. None of this is an argument against microservices, or for them. It's an argument for drawing service boundaries around what a business entity actually is and how it actually moves through your system, not around which external system a piece of code happens to call, or which message-queue topic a byte happens to travel over. Those are implementation details. When they get promoted to architecture, you end up owning a boundary nobody actually wanted, and paying for it every time you need to touch the entity it accidentally split in half.