brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
restateRestate is a durable execution engine that makes distributed application code resilient to crashes, with SDKs for TypeScript, Java, Go and more. Use when someone asks to "durable execution", "Restate", "resilient workflows", "distributed transactions", "saga pattern", "fault-tolerant services", or "replace Temporal with something lighter". Covers durable ha…
Independent GitHub snapshot · Automated classification · Original documentation governs
Restate is a durable execution engine — your code runs reliably even when things crash. Write normal async functions, and Restate ensures they complete: if a service crashes mid-execution, it resumes exactly where it left off. No lost state, no duplicate side effects. Like Temporal but with a simpler programming model — just annotate your functions, no state machines or DSLs.
Two processes are involved: the Restate Server (a single binary that stores the execution journal, exposes the ingress on port 8080 and the admin API and UI on port 9070) and your service (an HTTP endpoint on port 9080 by default). Callers always talk to the server, never to the service directly.
# SDK for your service
npm install @restatedev/restate-sdk
# Server and CLI (also on Homebrew: restatedev/tap/restate-server, restatedev/tap/restate)
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
restate-server # ingress :8080, admin API + UI :9070, data in ./restate-data
# Or run the server in Docker
docker run --name restate_dev --rm -p 8080:8080 -p 9070:9070 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
After starting your service, register it with the server. Nothing can be invoked until this is done, and it must be repeated whenever handlers are added or renamed:
restate deployments register http://localhost:9080 --yes # --yes skips the confirmation prompt
# Server in Docker: restate deployments register http://host.docker.internal:9080 --yes
# Development, same URL with changed handlers: add --force to replace the earlier registration
restate services list
// services/payment.ts — Durable payment service
import * as restate from "@restatedev/restate-sdk";
type Order = { orderId: string; userId: string; amountCents: number };
export const paymentService = restate.service({
name: "payments",
handlers: {
// If the process crashes between steps, the handler is replayed and
// completed steps return their recorded result instead of running again
processPayment: async (ctx: restate.Context, order: Order) => {
// Stable across retries — unlike crypto.randomUUID()
const idempotencyKey = ctx.rand.uuidv4();
const charge = await ctx.run("charge-payment", async () => {
return await stripeApi.charge(order.userId, order.amountCents, idempotencyKey);
});
await ctx.run("confirm-order", async () => {
await orderDb.confirm(order.orderId, charge.id);
});
await ctx.run("notify", async () => {
await emailApi.send(order.userId, "Order confirmed!");
});
return { orderId: order.orderId, chargeId: charge.id, status: "completed" };
},
},
});
// Starts an HTTP/2 server; port defaults to 9080. List every service, object and workflow you define
restate.serve({ services: [paymentService], port: 9080 });
restate.endpoint().bind(...).listen() from older tutorials is deprecated; use restate.serve, or restate.createEndpointHandler (imported from @restatedev/restate-sdk/lambda or /fetch) for AWS Lambda, Deno and Cloudflare Workers.
A step that throws an ordinary Error is retried with exponential backoff; after 70 attempts (the server default) the invocation is paused, not failed. Throw restate.TerminalError to stop retrying and return the failure to the caller, or cap a step with a retry policy:
await ctx.run("charge-payment", () => stripeApi.charge(order.userId, order.amountCents), {
initialRetryInterval: { milliseconds: 500 },
maxRetryAttempts: 5, // afterwards a TerminalError is thrown
});
// services/cart.ts — Stateful shopping cart (single-writer per key)
type CartItem = { sku: string; quantity: number; priceCents: number };
export const cartObject = restate.object({
name: "cart",
handlers: {
// Exclusive handler: only one runs per cart key at a time — no race conditions
addItem: async (ctx: restate.ObjectContext, item: CartItem) => {
const items = (await ctx.get<CartItem[]>("items")) ?? [];
items.push(item);
ctx.set("items", items);
return { items: items.length };
},
checkout: async (ctx: restate.ObjectContext, userId: string) => {
const items = (await ctx.get<CartItem[]>("items")) ?? [];
if (items.length === 0) {
throw new restate.TerminalError("Cart is empty", { errorCode: 400 });
}
const amountCents = items.reduce((sum, i) => sum + i.priceCents * i.quantity, 0);
// Durable call to another service; ctx.key is the cart ID from the URL
const result = await ctx
.serviceClient(paymentService)
.processPayment({ orderId: ctx.key, userId, amountCents });
ctx.clear("items");
return result;
},
// Shared handler: read-only, runs concurrently. Without the wrapper
// the handler is exclusive and queues behind writers.
getItems: restate.handlers.object.shared(
async (ctx: restate.ObjectSharedContext) => (await ctx.get<CartItem[]>("items")) ?? []
),
},
});
Call other handlers with ctx.serviceClient(svc), ctx.objectClient(obj, key) and ctx.workflowClient(wf, id). The ...SendClient variants send a one-way message; add restate.rpc.sendOpts({ delay: { hours: 5 } }) as the last argument to schedule it.
A workflow runs its run handler exactly once per workflow ID. Other handlers take a WorkflowSharedContext and can query state or resolve a promise the workflow is waiting on.
// services/signup.ts — Wait for an email click without holding a process
export const signupWorkflow = restate.workflow({
name: "signup",
handlers: {
run: async (ctx: restate.WorkflowContext, user: { email: string }) => {
const code = ctx.rand.uuidv4();
await ctx.run("send-email", () => emailApi.sendVerification(user.email, code));
ctx.set("status", "waiting-for-click");
const clicked = await ctx.promise<string>("email-clicked");
ctx.set("status", "verified");
return clicked === code;
},
click: async (ctx: restate.WorkflowSharedContext, code: string) => {
await ctx.promise<string>("email-clicked").resolve(code);
},
status: async (ctx: restate.WorkflowSharedContext) =>
(await ctx.get<string>("status")) ?? "unknown",
},
});
// services/booking.ts — Saga: undo completed steps when a later one fails for good
export const bookingService = restate.service({
name: "booking",
handlers: {
bookTrip: async (ctx: restate.Context, trip: TripRequest) => {
const compensations: (() => Promise<unknown>)[] = [];
try {
// Register the undo before the action, in case the action half-succeeds
compensations.push(() => ctx.run("cancel-flight", () => flightApi.cancel(trip.tripId)));
const flightId = await ctx.run("book-flight", () => flightApi.book(trip.flight));
compensations.push(() => ctx.run("cancel-hotel", () => hotelApi.cancel(trip.tripId)));
const hotelId = await ctx.run("book-hotel", () => hotelApi.book(trip.hotel));
const carId = await ctx.run("book-car", () => carApi.book(trip.car));
return { flightId, hotelId, carId, status: "confirmed" };
} catch (e) {
// Only terminal errors mean "give up"; anything else is retried by Restate
if (e instanceof restate.TerminalError) {
for (const undo of compensations.reverse()) await undo();
}
throw e;
}
},
},
});
# Service: /restate/call/{service}/{handler}
curl localhost:8080/restate/call/payments/processPayment \
-H 'idempotency-key: order-7731' \
--json '{"orderId":"order-7731","userId":"user-381","amountCents":12300}'
# Virtual object or workflow: the key goes between service and handler
curl localhost:8080/restate/call/cart/order-1042/getItems
# Fire-and-forget, optionally delayed
curl "localhost:8080/restate/send/payments/processPayment?delay=10s" \
--json '{"orderId":"order-7732","userId":"user-381","amountCents":4900}'
The /restate/call and /restate/send prefixes are for Restate Server 1.7 and later. On 1.6 and earlier use /{service}/{handler} and /{service}/{handler}/send.
User prompt: "Build a payment flow that never loses a charge — even if the server crashes between charging the card and updating the database."
The agent writes paymentService and cartObject as above, starts restate-server and the service, registers the deployment, then drives it through the ingress:
restate deployments register http://localhost:9080 --yes # --yes skips the confirmation prompt
curl localhost:8080/restate/call/cart/order-1042/addItem \
--json '{"sku":"KB-87","quantity":2,"priceCents":4900}'
curl localhost:8080/restate/call/cart/order-1042/checkout --json '"user-381"'
{"items":1}
{"orderId":"order-1042","chargeId":"ch_02a61802","status":"completed"}
If the service is killed after charge-payment completes, the retry skips the charge and continues with confirm-order. A second checkout on the now empty cart returns HTTP 400 {"code":400,"message":"Cart is empty","source":"invocation"} instead of retrying.
User prompt: "Implement an order workflow: reserve inventory → charge payment → ship. If any step fails, roll back everything."
The agent builds a service shaped like bookingService, one ctx.run per step, with each compensation registered before its action. When the last step throws new restate.TerminalError("No cars left", { errorCode: 409 }), the compensations run in reverse order and the caller receives the error:
curl -i localhost:8080/restate/call/booking/bookTrip \
--json '{"tripId":"LIS-2292","flight":"TP1351","hotel":"Bairro Alto","car":"compact"}'
HTTP/1.1 409 Conflict
{"code":409,"message":"No cars left","source":"invocation"}
The service log shows cancel-hotel then cancel-flight. Inspect any invocation, its journal and its retries in the UI at http://localhost:9070 or with restate invocations list.
ctx.run() for side effects — wrap every non-deterministic operation (HTTP calls, database writes, Date.now(), random IDs). Name each step; the name shows up in the UI and CLI.ctx.run — ctx.get, ctx.sleep and nested ctx.run are not allowed in the closure.ctx.rand.uuidv4()) to external APIs.TerminalError to stop retries — by default the server gives an invocation 70 attempts and then pauses it instead of failing it. Validation and business failures must be terminal or the caller never gets an answer.TerminalError only — a bare catch that compensates on every error also undoes work on transient failures that Restate was about to retry.ctx.sleep or a waiting call in an exclusive handler blocks every other call to that key; request-response calls between exclusive handlers (A → B → A) can deadlock.ctx.sleep({ seconds: 10 }) for delays — durable timers that survive crashes. For long delays prefer a delayed message so the handler can finish.identityKeys to restate.serve so only your Restate instance can call it. Do not expose port 9070 (admin API) publicly.restate-data wipes all invocations, state and registrations.