Idempotency
Idempotency ensures that the same logical operation can be submitted more than once without creating duplicated side effects.
In workflow terms, it protects you from running the same flow twice when the second execution is really just a duplicate of the first.
Why idempotency matters
Idempotency is especially important for workflows that:
- charge money
- create orders
- process webhooks
- provision resources
- handle retrying clients or event deliveries
Without idempotency, a single duplicated request can lead to double work, double billing, or inconsistent state.
Using idempotency in Orchestrix
Orchestrix supports idempotency through an IdempotencyStore.
1. Set up a store
The simplest option is the built-in in-memory store:
import { create, createIdempotencyStore } from "@eddiecbrl/orchestrix";
const store = createIdempotencyStore();
const flow = create("my-flow", { idempotency: store });For distributed systems, use Redis or DynamoDB adapters.
2. Run with a key
To enable idempotency for a specific execution, pass a key in the run() options:
const result = await flow.run(input, {
key: "unique-request-id-123",
ttlMs: 3600000
});The key should identify the logical business operation, not just the transport request.
Examples:
payment:order_123webhook:evt_456signup:user_789
How Orchestrix behaves
When run() is called with a key:
- Orchestrix checks the configured store
- if the key is free, it marks the execution as running and starts the flow
- if a completed result is already stored, it returns that cached result
- if a failed result is stored and caching is enabled, it returns the cached failure
- if another execution is already running, it returns a
runningresult or throws if configured to do so
Running duplicate executions
If a flow with the same key is already running:
- by default, Orchestrix returns a flow result with
status: "running" - if
throwIfRunningistrue, it throwsFlowAlreadyRunningError
await flow.run(input, {
key: "payment:123",
throwIfRunning: true
});This is one of the behaviors that should be explicit in your application layer, because it affects client expectations.
Caching results
By default, successful results are cached.
That means a repeated execution with the same key can immediately return the previously completed FlowResult without rerunning the steps.
You can disable that behavior:
await flow.run(input, {
key: "payment:123",
cacheResult: false
});With cacheResult: false, Orchestrix still uses the key to avoid duplicate in-flight execution, but it does not keep the completed result for replay later.
TTL
ttlMs controls how long the idempotency record remains valid.
This helps prevent idempotency storage from growing forever while still protecting the window in which duplicates are realistically expected.
Distributed idempotency
For production environments, use a distributed store.
Redis
import { createClient } from "redis";
import { redisIdempotencyStore } from "@eddiecbrl/orchestrix";
const redisClient = createClient();
await redisClient.connect();
const store = redisIdempotencyStore(redisClient);
const flow = create("payment-flow", { idempotency: store });DynamoDB
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { dynamoIdempotencyStore } from "@eddiecbrl/orchestrix";
const client = new DynamoDBClient({});
const store = dynamoIdempotencyStore(client, {
tableName: "orchestrix-idempotency"
});Choosing good idempotency keys
Good keys are:
- stable
- tied to the business operation
- unique at the right scope
Bad keys are often:
- random values generated for each retry
- timestamps
- values that change between attempts of the same logical action
Best Practices
- Use idempotency for side-effect-heavy flows.
- Choose keys based on business identity, not network identity.
- Set a TTL that matches realistic duplicate windows.
- Decide explicitly whether duplicate in-flight calls should return
runningor throw.