Optimizing Trading Engine Processes with Sagas and Restate.dev

Introduction
What happens when a trade partially succeeds?
Your order is placed, funds are reserved… but settlement fails. Do you roll back? Retry? Compensate? And how do you prove your system behaves correctly when the market, network, or exchange misbehaves?
This is where Saga workflows, Test-Driven Development (TDD), and Restate.dev intersect.
In this article, we’ll explore how to design a resilient, deterministic trading engine using Saga-based workflows, built and verified through TDD, and powered by Restate.dev. We’ll wrap up with a hands-on tutorial using the Restate SDK and a TypeScript Elysia backend.
Along the way, we’ll ask some uncomfortable (but important) questions:
Can your trading engine survive retries without duplicating trades?
Can you replay yesterday’s outage and get the same result today?
Can you test failure paths as confidently as success paths?
Prerequisites for the hands-on section:
Bun installed (install here)
Restate.dev installed locally or a cloud account
Why Trading Engines Need Deterministic Workflows
Trading systems operate in hostile environments:
Unreliable exchanges
Network partitions
Timeouts and partial failures
Strict financial correctness requirements
A single bug can mean:
Double execution
Inconsistent balances
Regulatory risk
Brain teaser:
If your trade execution retries after a timeout, how do you know it won’t execute twice?
Traditional microservices rely heavily on:
Distributed transactions (hard)
Message queues (complex)
Manual idempotency (error-prone)
This is where Saga workflows shine.
Saga Workflows in a Nutshell
A Saga breaks a long-running transaction into steps, each with:
A forward action
A compensating action
Example trading saga:
Validate order
Reserve funds
Place an order on the exchange
Settle trade
If step 3 fails, compensate step 2. If step 4 fails, compensate step 3.
Sounds simple — until retries, crashes, and restarts come into play.
Why Restate.dev?
Restate is a lightweight and durable execution engine that allows you to write distributed workflows as if they were local functions — while guaranteeing:
Deterministic execution
Automatic retries
Exactly-once semantics
Built-in state persistence
No external workflow engines. No custom retry logic. No fragile state machines.
Key Benefits for Trading Systems
Resilience by default – crashes and restarts don’t break workflows
Replayability – failed executions can be replayed deterministically
Idempotency built-in – no duplicate trades on retries
Testability – workflows behave the same in tests and production
Just saying:
How do you feel if your production trading logic behaved exactly like your unit tests?
Test-Driven Development for Saga Workflows
TDD flips development upside down:
Write the test
Watch it fail
Write minimal code
Refactor safely
For Saga workflows, TDD becomes even more powerful.
Why TDD Matters Here
You can test failure paths first
You can simulate crashes and retries
You can verify compensations explicitly
Brain teaser:
Have you ever written a test that expects a failure and validates the rollback?
Restate makes this possible because workflows are pure, deterministic functions with durable state.
Our Trading Engine Architecture Overview
High-level components:
Elysia (TypeScript) – API layer
Restate SDK – workflow and saga orchestration
Exchange Adapter – external exchange API
Ledger / State Store – balances and positions
Elysia handles HTTP. Restate handles correctness.
Hands-On Tutorial: Building a Saga-Based Trading Engine
Step 1: Elysia Project Setup (using the default settings)
bun create elysiajs saga-workflow
# Install Restate TS-SDK
bun add @restatedev/restate-sdk @restatedev/restate-sdk-clients
Initialize Restate and your Elysia backend.
Step 2: Setup Restate and Define the Trading Saga
Create a restate.ts file and paste the code below, it configures restate and
import { tradeWorkflow } from "./saga.ts";
import { serve } from "@restatedev/restate-sdk"
import * as clients from "@restatedev/restate-sdk-clients";
export const restateClient = clients.connect({ url: "http://localhost:8080" }); // the url of your restate server (either cloud or local URL)
serve({
services: [tradeWorkflow],
port: 9080
});
Create a saga.ts file and paste the code below,
import { TerminalError, workflow, WorkflowContext } from "@restatedev/restate-sdk"
const OrderManager = {
validate: (order) => {
console.log("Order validation logic", order)
},
placeOnExchange: (order) => {
console.log("Order placement logic", order)
},
reserve: (order) => {
console.log("Order reservation logic", order)
},
settle: (order) => {
console.log("Order settlement logic", order)
},
release: (order) => {
console.log("Order placement logic", order)
},
}
const tradeWorkflowName = "tradeWorkflow"
export const tradeWorkflow = workflow({
name: tradeWorkflowName,
handlers: {
run: async (ctx: WorkflowContext, order: any) => {
const compensations = [];
//Workflow:
// Validate order
// Reserve funds
// Place an order on the exchange
// Settle trade
try {
const orderId = ctx.rand.uuidv4();
compensations.push(() =>
ctx.run("release-order", () => OrderManager.release(orderId)),
);
const orderRef = await ctx.run("validate", () =>
OrderManager.validate(order),
);
compensations.push(() =>
ctx.run(`release-order-${orderId}`, () => OrderManager.release(orderId)),
);
const reservedOrder = await ctx.run("reserve-fund", () =>
OrderManager.reserve(orderRef),
);
compensations.push(() =>
ctx.run(`release-order-${orderId}`, () => OrderManager.release(orderId)),
);
const placedOrder = await ctx.run("place-order", () =>
OrderManager.placeOnExchange(reservedOrder),
);
compensations.push(() =>
ctx.run(`release-order-${orderId}`, () => OrderManager.release(orderId)),
);
const settledTrade = await ctx.run("settle-trade", () =>
OrderManager.settle(placedOrder),
);
return {
data: settledTrade,
message: "Order placed successfully"
}
} catch (error) {
if (error instanceof TerminalError) {
for (const compensation of compensations.reverse()) {
await compensation();
}
}
}
},
},
});
Notice in the saga.ts file:
Each step is durable
Retries won’t duplicate execution
Compensation is explicit and testable
Step 3: Expose the Workflow via Elysia in the server.ts file
import { Elysia } from "elysia"
import { restateClient } from "./services/restate.ts";
import { tradeWorkflow } from "./services/saga.ts";
export const app = new Elysia()
.post("/trade", async ({ body }) => {
return restateClient.workflowClient<typeof tradeWorkflow>({ name: "tradeWorkflow" }, "req-id").workflowSubmit({
order: body
})
})
Now your trading saga is an HTTP API.
Side note:
When was the last time you tested a crash instead of a happy path?
Step 4: Deterministic Replay
In your restate cloud or local instance, Restate allows replaying failed executions:
Same inputs
Same decisions
Same outcomes
This is golden for:
Post-mortems
Audits
Research simulations
Why should this matter to me
I categorized the importance of reliable workflows based on the role of users
Quant Traders
Deterministic execution logic
Safer strategy automation
Reproducible simulations
Developers
Fewer edge-case bugs
Easier reasoning about failures
Stronger confidence in production
Researchers
Replayable experiments
Deterministic systems modeling
Reliable failure analysis
Final Thoughts
What if failures were no longer scary?
By combining Saga workflows, Test-Driven Development, and Restate.dev, you can build trading engines that are:
Resilient
Deterministic
Testable by design
Instead of asking “What if this fails?”, you start asking:
“How do we prove it recovers correctly?”
That mindset shift is where robust trading systems are born. You can check out the full-code in the GitHub repo link: https://github.com/litmus-zhang/personal-playground/tree/backend/saga-workflow/backend/saga-workflow




