The Hidden Cost of Stateful Orchestration
SchemaBridge Team · 2026-02-02 · Distributed Systems, Immutable Infrastructure, Orchestration, Anti-Patterns, Reliability
Why your 'simple' internal tools might be your biggest reliability risk, and how applying Immutable Infrastructure patterns to application logic can save you.
In the world of microservices, we obsess over decoupling. We use queues, we adopt event-driven architectures, and we break monoliths. Yet, inside our orchestration engines, we often commit the cardinal sin of distributed systems: We write stateful loops.
At SchemaBridge, we recently overhauled our core system workflows — the ones that drive our own gradual rollouts. In doing so, we found ourselves confronting the same anti-patterns that plague many engineering organizations. This isn't just a story about our release notes; it's an exercise in applying Immutable Infrastructure principles to the application layer.
The "Loop" Anti-Pattern
Consider the classic "Poller":
while not deployment.is_ready():
sleep(60)
check_status()
This code assumes a stable universe. It assumes the polling process will live forever. In reality, this is a Distributed Monolith artifact. A process that holds its state in memory across a long synchronous wait is fragile: when its host restarts or a call hangs, it either loses that state or lingers as a "zombie" nobody is watching, and its failure modes become unpredictable.
The Solution: Recursive Chaining
We moved from an internal while loop to a Recursive Execution Model, similar to the Continuation Passing Style used in functional programming.
- Pattern: Each round spawns a monitor child workflow that waits with a durable
DELAYstep instead ofsleep(); aSPAWNERstep then starts the next round as a child workflow — the rollout workflow spawning another run of itself. - Result: The parent still waits for its child to report back, but it waits durably: the wait is a row in PostgreSQL (a
WAIT_ALLmerge expecting the child's callback), not a thread or an in-memory loop. If a worker dies mid-rollout, nothing is lost; the database state remains the source of truth.
Immutable Logic: Versioning with Canonical Hashing
Versioning is famously hard. When you update a workflow definition, what happens to the executions already running?
- The Anti-Pattern: "In-place updates." You deploy new code, and existing executions suddenly start behaving differently.
- The Industry Standard: Immutable Infrastructure. Just as we don't SSH into servers to patch them (we replace them), we shouldn't patch running workflow definitions.
Workflow versions in SchemaBridge are immutable: a deploy creates a new version, and a run is tied to the version it started on. What we added is Canonical DSL Hashing: every imported workflow definition is serialized to a canonical JSON form and hashed, and the hash is stored with the workflow (dslHash).
That hash answers a question that used to be surprisingly hard — did the logic actually change? When a configuration import or our own system-workflow bootstrap meets a definition whose hash matches what is already live, it plans a no-op instead of redeploying, and a live workflow that has drifted from its source definition is detectable by comparing hashes.
Locking Down the "God Mode"
Finally, we addressed the security of internal tools. It is common to give internal "cleanup" scripts root access. This violates the Principle of Least Privilege.
We found that our own internal "Gradual Rollout" tool located the workflows it operated on through hardcoded logical IDs, which made it easy to point at — and clobber — the wrong one. We fixed this in two ways:
- Resolve by role, not by name: The rollout now finds its target entry point through explicit tags, validated for uniqueness, instead of hardcoded logical-ID lookups.
- A least-privileged credential: The Gateway steps inside our rollout workflows authenticate with their own named API key, which deliberately lacks
config:manage— the permission to rewrite the system tenant's configuration. The automation that runs from the least-trusted place can no longer change the rules it runs under.
Conclusion
If your orchestration relies on sleep(), you are fighting the cloud.
By embracing Recursion, Immutability, and Least Privilege, we move from "Hope-based" orchestration to orchestration whose state you can inspect and whose blast radius you can bound.
Further Reading: