One flow, many services§
queuesdefaultQueuepolyglot workerspull-based
The problem§
The order flow touches four teams: validation lives with the orders service, charging with payments, receipt rendering with a GPU pool, email with notifications. One process, four codebases, four deploy cadences — and nobody wants a broker between them or a central service that imports everyone's code.
The shape§
Each step names a queue; each service runs a worker subscribed only to its queues:
Blueprint orders = Workflow.define("orders")
.step("validate", "orders") // queue: orders service
.step("charge", "payments") // queue: payments service
.step("render-receipt", "gpu") // queue: the GPU pool
.effect("email", "notify") // queue: notifications
.build();
Four separate processes — deployed, scaled, and owned independently:
// payments-service (Java)
new Worker(client, "payments-1").register(orders)
.handlers(new PaymentHandlers()) // only charge() matches a step it serves
.start();
// notifications-service (Go) — same instance, different language
w := wiggle.NewWorker(client, "notify-1",
wiggle.Register(orders), wiggle.Handlers(NotifyHandlers{}))
The server dispatches each step to its queue; whichever worker serves that queue pulls it. The instance's durable state stays in one place — each service sees the full context on its steps and returns the next context.
Why this shape§
- No broker, no choreography drift. The graph is the source of truth for the end-to-end process — not an emergent property of who publishes which event. When the flow changes, one definition changes, and the console shows the real, current shape.
- Workers pull; nothing dials into your services. Long-polling gRPC means no inbound connectivity, no service mesh requirement, and backpressure by construction — a slow GPU pool queues its own steps without stalling the payments team.
- Unserved steps just wait. Deploying the flow before every team is ready is fine: steps for a queue nobody serves sit leased-less until a worker shows up. Rolling deploys of one service never lose work — leases expire and steps redeliver.
- Polyglot by name-matching. Binding is activity-name over the graph, so the language of each worker is invisible to the others — a Java flow can grow a Python step tomorrow.
Variations§
defaultQueue("cpu")sets the workflow-wide default; per-step queue args override it — handy insideforEachbodies where one step needs the GPU pool.- Queue-lag monitoring warns when a queue isn't draining (
WIGGLE_QUEUE_LAG_WARN_MILLIS) — usually a service that forgot to deploy its worker. - The queues doc walks the full end-to-end with diagrams.