AWS Step Functions vs Camunda for Sagas: Two Orchestrators, Different Blast Radii
DEV Community

AWS Step Functions vs Camunda for Sagas: Two Orchestrators, Different Blast Radii

Here's a trap I've watched smart teams walk into. They pick a saga orchestrator by comparing the two engines feature-for-feature, decide they both "do sagas," flip a coin weighted by whatever's already in their cloud bill, and move on. Then a year later one team is tuning Zeebe partitions at 2am and the other is staring at a Step Functions bill that grew faster than their traffic, both wondering how the "same pattern" turned into such different jobs. They do run the same pattern. A saga is a sequence of local transactions where, if step four fails, you run compensating actions to undo steps one through three, because you don't have a distributed transaction to roll back for you. Both AWS Step Functions and Camunda 8 are orchestration-style saga coordinators: a central brain that knows the steps, drives them in order, and triggers the rollback when something breaks. That part is genuinely the same. What's not the same is what you're on the hook for. We run Camunda 8 in production for credentialing and scheduling workflows, so I've lived one of these; the other I've read the docs on and priced out for real proposals. The honest comparison isn't "which has more features." It's "which failure is yours to own." Let me lay both out. The saga both of them run Orchestration sagas need a coordinator because someone has to remember where the process is. Choreography (services reacting to each other's events with no central brain) works until you need to answer "why is order 8842 stuck," and then nobody can, because the state lives smeared across six services' logs. An orchestrator centralizes that: one place holds the process state, drives the next step, and owns the compensation logic. So the shape is identical on both. Take a classic order saga: reserve inventory, charge the card, ship. If the charge fails, release the inventory. If shipping fails, refund the charge and release the inventory. Three forward steps, a compensation for each, run in reverse. The pattern is old and boring, which is the point. The interesting part is what each engine makes you do to express it, and what each one does when the wheels come off. Step Functions: the orchestrator AWS operates for you Step Functions is a managed state machine. You describe your saga in Amazon States Language, which is JSON, and AWS runs the thing. There are no brokers to operate, no cluster to size, no exporter to babysit. For a lot of teams that single sentence is the whole pitch, and it's a good one. You get retries and error routing as declarative config. Here's the reserve-then-charge slice, with a retry on the flaky step and a Catch that routes a failed charge to the compensating action: { "StartAt": "ReserveInventory", "States": { "ReserveInventory": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "reserveInventory" }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts": 3, "IntervalSeconds": 1, "BackoffRate": 2.0 } ], "Next": "ChargePayment" }, "ChargePayment": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "chargePayment" }, "Catch": [ { "ErrorEquals": ["States.ALL"], "Next": "ReleaseInventory" } ], "Next": "Ship" }, "ReleaseInventory": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "releaseInventory" }, "Next": "SagaFailed" }, "SagaFailed": { "Type": "Fail", "Error": "SagaFailed" } } } Look at what compensation actually is here: it's you, by hand, pointing each step's Catch at the right cleanup state and remembering to undo everything that already succeeded. There's no "compensate the whole saga" primitive. If Ship fails, its Catch has to route to a state that refunds the payment AND then to a state that releases the inventory, in the right order. Miss one and you've got a half-rolled-back order and no error to tell you. The state machine is honest and explicit, which is a nice way of saying verbose and entirely your responsibility. For long-running sagas you use Standard Workflows, and this is where the surprise lives. Standard Workflows bill per state transition. Every step your machine takes is a transition: a task, a wait, a choice. The first 4,000 a month are free, then it's $0.000025 each. Cheap, until you read the next sentence: every retry attempt is its own billed state transition. So the exact moment your dependencies are flaky and your saga is retrying and compensating, which is the moment you most need it to work, is also the moment it's transitioning the most and billing the most. Do the math on a bad month. Say the order saga is six states on the happy path. Add a payment step that burns its three retries and then compensates two prior steps: call it eleven transitions for that execution instead of six. Now imagine a million sagas in a month running hot like that: 1,000,000 sagas x ~11 transitions = ~11,000,000 state transitions minus 4,000 free x $0.000025 = ~$275 / month, in Step Functions transitions alone (Lambda invocations, and everything the tasks actually do, billed separately) That number is small until it isn't. The point isn't the dollar figure, it's the shape of the curve: your orchestration cost scales with steps and with failures, not with business value. A retry storm is a billing event. For high-volume, short-lived sagas AWS pushes you to Express Workflows, which bill on requests plus duration plus memory instead, but Express caps how long a saga can run and changes the durability guarantees, so it's a different tool, not a cheaper Standard. Human steps and long waits work through the callback pattern: a task pauses with a token and resumes when something calls back with that token. It's a clean primitive. It is also, again, plumbing you assemble. There's no built-in inbox, no task list, no assignment or escalation. If your saga has a "a human approves this" step, Step Functions gives you the pause and the resume and leaves the actual human workflow to you. Camunda 8: the orchestrator you operate Camunda flips almost every one of those tradeoffs. You model the saga in BPMN, and compensation is a first-class construct: you attach a compensation boundary event to each activity, define its handler (the undo), and when something throws, one compensation throw event fires all the relevant handlers, in reverse, automatically. You don't hand-route rollbacks. You declare "charging the card is compensated by refunding it" once, on the task, and the engine does the reverse walk. Camunda's own framing is that Zeebe acts as the saga coordinator that solves the transaction without two-phase commit, and the compensation modeling is the part that makes that pleasant instead of a pile of Catch states. The workers are your code. A Zeebe worker subscribes to a task type and does the real work; throwing a BPMN error is what triggers the compensation path: import { ZBClient } from "zeebe-node"; const zbc = new ZBClient(); zbc.createWorker({ taskType: "charge-payment", taskHandler: async (job) => { try { await payments.charge(job.variables.orderId, job.variables.amount); return job.complete(); } catch (err) { // A BPMN error, not a crash: the modeled compensation boundary // events upstream fire and unwind the saga for us. return job.error("PAYMENT_FAILED", "card declined"); } }, }); Two things Step Functions makes hard, Camunda makes boring. First, human tasks are native: user tasks are part of the model, and since 8.5 you drive them through the Zeebe REST API, with a task list, assignment, and forms as real product surface rather than something you glue on. For our credentialing flows, where a human genuinely has to review and approve, that's not a nice-to-have, it's the reason the engine earns its keep. Second, and this is the one people underestimate until it bites: in-flight process versioning. When you fix a bug in a saga that has thousands of instances mid-flight, deploying version two doesn't disturb the running ones; they finish on the version they started on. When you actually need to move them, you migrate in-flight instances, through Operate or the Zeebe API, and you can canary a new version against real traffic. A saga that spans days or weeks will outlive several deploys of its own definition. Having a real answer to "we changed the process while ten thousand of them were running" is the difference between a workflow engine and a fancy job queue. The bill for all this is that you operate it. Even on Camunda's SaaS you're closer to the machine; self-managed, you're running Zeebe brokers, choosing a partition count, and feeding an exporter into Elasticsearch so Operate has something to show. Zeebe is a distributed, log-based engine, every state change is an append to a replicated log, which is exactly why it recovers cleanly and exactly why it's a real distributed system with your name on the pager. Nobody at AWS gets paged when your exporter falls behind. Where the blast radii actually differ Both engines can lose you a saga. What differs is which failure domain is yours. With Step Functions, the engine's blast radius is AWS's problem. If a broker equivalent falls over, that's their incident, their multi-AZ replication, their 3am. Your blast radius moved somewhere else: into cost, which grows with every retry, and into lock-in, because Amazon States Language and the deep .sync and callback integrations don't travel. The day you want to run the same saga off AWS, you're not porting a config, you're rewriting the orchestrator. That's a real blast radius, it's just a slow one that shows up in a migration quarter instead of a pager. With Camunda, the engine's blast radius is yours. A misconfigured partition count, an exporter that can't keep up, a botched broker upgrade, those are your incident. In exchange, you own the model. The saga is BPMN you can read, version, and migrate; the compensation is declared, not wired; the engine runs wherever you put it, on any cloud or none. You

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.