long-running operation
rule
an LRO is the asynchronous request-reply contract for work that cannot finish within one HTTP request: the command durably creates or reuses an operation resource, returns its location, and the client polls it until the owning state machine reports a terminal state.
the pair is indivisible:
- an LRO without a state machine has no authoritative status semantics.
- a state machine alone does not define acceptance, polling, retry guidance, or terminal delivery.
consequence of skipping this pattern:
- returning a provider-owned or executor-specific identifier (e.g., a workflow instance ID) as the status breaks all clients when the executor is replaced.
- clients lose a durable operation identity and cannot safely retry after network loss or timeout.
- status stored independently from facts: an interruption between effect and status update leaves the state machine and visible state permanently out of sync; observers read stale or inconsistent status across retries.
- each executor becomes an implicit partial copy of the state machine logic; disagreement compounds across clients and replicas.
why
asynchronous work outlives the request that started it, so the client needs a durable handle. a provider-owned identifier as the product resource couples every client to a replaceable executor; status stored apart from facts lets an interruption split what happened from what is visible. a durable operation resource with state derived from the owning state machine gives:
- a safe retry identity;
- a stable polling contract;
- a terminal answer that survives network loss.
acceptance contract
before acknowledging work, the command boundary:
- validates input, identity, and authorization;
- adopts a caller idempotency key or derives one from a stable command identity;
- creates or reuses one durable operation ID;
- commits operation intent and a dispatch claim atomically;
- dispatches the replaceable executor or leaves a claim that reconciliation can dispatch safely.
accepted work returns 202 Accepted: Location names the canonical polling
resource, Retry-After gives the first polling delay, and the body repeats the
durable identity and current semantic state.
HTTP/1.1 202 AcceptedLocation: /operations/op_01Retry-After: 3Content-Type: application/json{"operationId": "op_01","state": "accepted","statusUrl": "/operations/op_01"}
acceptance means the command is durable, not that execution has started or will
succeed. if the service cannot create recoverable intent, it returns a typed
failure instead of 202.
polling resource
GET statusUrl returns 200 OK while the operation exists. state is always
derived by the owning state-machine fold over canonical facts, never copied
from an executor SDK.
the domain owns the exact closed state set; every state is terminal or nonterminal:
- nonterminal representations may include updated retry guidance;
- terminal success includes a typed result or a stable link to it;
- terminal failure includes a safe typed problem;
- absence, expiry, and lost authorization use ordinary typed HTTP failures and never fabricate progress.
type OperationRepresentation<Result, Problem> = {operationId: string;state: OperationState;statusUrl: string;result?: Result;problem?: Problem;};
result is legal only for terminal success, problem only for terminal
failure. prefer a discriminated union when the contract language can express
it.
idempotency, dispatch, and reconciliation
- repeating one command with the same idempotency key returns the same operation ID and status URL — never starts a second logical operation merely because the first HTTP response was lost.
- execution claims are durable and bounded. a reconciler finds accepted work whose dispatch is absent or expired, claims it idempotently, and dispatches again.
- executors commit canonical outcome facts before freshness events or notifications; the state machine then derives the terminal result.
client contract
the polling client:
- follows
LocationorstatusUrlrather than constructing provider URLs; - honors
Retry-After, then uses bounded exponential backoff with jitter; - stops on every terminal state and on lost authorization or expiry;
- tolerates repeated representations and transient transport failures;
- does not infer success from elapsed time or HTTP acceptance.
cancellation, when the domain supports it, is a separate authorized command: it
commits cancellation intent; the state machine decides whether that intent
produces cancelling, cancelled, or a conflict with an already terminal
outcome.
identifiers and provider boundary
operationIdidentifies the client-visible durable operation resource.correlationIdjoins logs and traces across one execution path.- neither name substitutes for the other.
- provider workflow IDs, queue message IDs, job handles, and provider status are internal execution facts: recordable for diagnosis and reconciliation, but never polled by API consumers or treated as semantic operation state.
- replacing the executor must not change the operation URL or state contract.
verification
- lose the acceptance response, retry the command, and receive the same operation ID.
- interrupt after intent commit but before dispatch, then prove reconciliation starts the work once.
- interrupt after outcome commit but before notification, then prove polling returns the terminal state.
- return every nonterminal and terminal representation through the generated client contract.
- replace or fake the executor and prove no provider identifier enters the public response.
- poll at the advised cadence and prove the client stops at every terminal state.
good and bad
bad
{"workflowInstanceId": "provider-7842","status": "running"}
a replaceable executor becomes the product resource; status semantics sit outside the domain contract. clients must construct provider URLs to poll, lose any safe retry identity after a lost response, and break in concert the moment the executor is replaced or upgraded.
good
{"operationId": "op_01","state": "accepted","statusUrl": "/operations/op_01"}
one durable product resource; the owning state machine derives every state from durable facts. a lost acceptance response retries into the same operation identity, polling survives network loss, and swapping the underlying executor changes nothing the client can see.