Learning pathsPricingChecking sign-in…
← LLD problem library

LLD-020 · LOW LEVEL DESIGN

Workflow / State-Machine Engine

Pin workflow definitions, fence worker attempts and repair known effects without treating timeout as proof of failure.

Local Java 21 reference verified: 36 checks. Learner drafts are not executed. Author self-review; independent review pending.

Existing introductory lesson · Version 1

Understand the problem

Design a local linear workflow protocol with versioned definitions, bounded retries, worker claims, cancellation and reverse compensation. Workers execute side effects outside the engine and report outcomes. Define stale completion, unknown effects and failed compensation before adding any orchestration features.

Before you start

  • State transitions and immutable definition versions
  • Task claims, leases and attempt identity
  • Idempotent side effects and unknown outcomes
  • Saga compensation and audit evidence

Scope and explicit decisions

  • One in-memory engine for linear step lists. It returns claims but runs no worker code, provider call or timer thread. No persistence, multi-process coordination, DAG/branching, parallel joins, dynamic steps, expression interpreter, signals or arbitrary user code is implemented.
  • A Definition has a unique immutable version and nonempty uniquely named steps. A step configures 1..5 attempts, positive fixed retry delay, whether external retries are safe, and whether accepted success requires compensation on failure/cancellation. Same version with changed contents rejects.
  • start binds a retry key to definition version and input String. Matching retries return the current instance view. Each instance pins that exact immutable definition; registering a newer version cannot silently migrate in-flight instances. Claim carries this input; no output propagation or per-step dataflow is implemented.
  • Engine operations are synchronized. claim selects one eligible due step/compensation and assigns a monotonically increasing per-instance token with checked lease deadline. RUNNING allows one active attempt. Workers act outside the engine monitor. Trusted caller time is nondecreasing; view/start do not perform automatic timeout scanning.
  • Stable external operation key is instance/step/work or instance/step/undo. New attempts get fresh fencing tokens but reuse that logical side-effect key. Declaring idempotent=true is a contract on the adapter/provider, not an implementation of remote deduplication.
  • SUCCESS is definite accepted effect, RETRYABLE and FATAL are trusted definite unsuccessful outcomes, UNKNOWN means outcome unresolved. UNKNOWN keeps the active attempt RUNNING so later definite evidence may arrive before lease expiry. A transport timeout cannot be reported as definite failure without external evidence.
  • At now>=leaseUntil a completion cannot become accepted success under the old claim. Timeout retires its token. A safe idempotent step with budget can retry after delay; non-idempotent, exhausted or cancelled unresolved forward work enters MANUAL_REVIEW. No automated manual-review resolution API is implemented.
  • Definite forward failure that cannot retry starts compensation of known completed steps configured compensate=true, in reverse success order. compensate=false must mean no repair is required under the workflow contract; it does not make irreversible harmful effects disappear.
  • Running cancellation records intent and waits for definite completion. A later success is recorded then compensated; a cancelled unknown timeout needs manual review because an unobserved effect may exist. Cancellation cannot retract an external operation already committed.
  • Compensation has its own stable external key and attempt budget. Definite exhausted/fatal undo failure becomes COMPENSATION_FAILED. retryCompensation is an explicit manual action resetting that undo budget; it does not bypass unresolved MANUAL_REVIEW. Terminal FAILED/CANCELLED follows completed required compensations. Histories are retained and not durable.

Worked examples

Input: reserve succeeds; idempotent charge returns RETRYABLE; retry after delay succeeds; ship returns FATAL.

Outcome: Workflow enters compensation, undoing known charge then reserve effects in reverse order. A failed undo remains visible.

Forward retry budget and reverse repair are different state machines with distinct logical external keys.

Input: Cancel while a forward worker is running. The worker later reports SUCCESS before lease expiry.

Outcome: Success becomes known history, then required compensation starts. If instead the unresolved lease expires, MANUAL_REVIEW is required.

Cancellation intent cannot prove an already issued remote action did not commit.

Try before revealing

Hint 1

Which identity belongs to an effect, and which belongs to an attempt?

Hint 2

Does a lease expiry prove the remote operation failed?

Hint 3

Which accepted effects can you actually compensate, and what if that repair fails?

Requirements and invariants

R1 — Pin each instance to an immutable definition and typed start retry identity. start protects I1.

R2 — At most one active worker claim owns the current step, and stale completion cannot advance it. claim protects I2.

R3 — Retry only under the configured safe identity/budget and keep unknown effects explicit. timeout protects I3.

R4 — Compensate known required effects in reverse order and expose failed repair. complete protects I4.

I1: One registered version has immutable contents; an instance retains its start definition and matching start keys cannot create another instance.

I2: Only RUNNING has an activeToken; only that unexpired token may record a new definite completion. Matching recorded duplicate outcomes cannot advance twice.

I3: Logical external work identity is stable across attempts, attempt tokens change, and unresolved unsafe/exhausted work remains explicit MANUAL_REVIEW.

I4: Compensation selects only known accepted forward successes marked for repair, in reverse completion order; failed undo is never silently reported as clean rollback.

Derive the design

A loop cannot represent unresolved side effects

A for-loop calling reserve, charge and ship hides the critical case: charge commits remotely, the response is lost and the caller cannot tell whether to retry or refund. Begin with explicit instance states and immutable definition identity. Separate a logical operation from an attempt token, and let the engine accept correlated evidence instead of running network calls under its lock.

Alternative: A production durable workflow platform may provide event history, replay, timers, signals and worker routing. Its determinism/versioning rules still matter. This local reference explains the protocol boundary and deliberately does not impersonate a durable engine with a list and a boolean.

Write states before classes

List due, running, unknown, successful, failed and repairing states. A pending cancellation is intent, not proof of termination. Use MANUAL_REVIEW and COMPENSATION_FAILED as visible outcomes.

Choose identities at two levels

An instance/step key identifies the external effect. A token identifies one ownership attempt. Stable keys prevent duplicate logical work only when the destination honors them; tokens reject stale local evidence.

Pin interpretation and evidence

An instance keeps its original definition and input. Record definite outcomes for duplicate detection, but do not record UNKNOWN as a final result that would prohibit later truth.

Compensate only what is known

Build reverse repair from accepted successes. Unknown timed-out work may need reconciliation before this list is complete. Never infer a safe refund or release from a missing response.

Responsibilities and ownership

Definition / Step

Owns: Immutable version, ordered names and retry/repair policy

Methods: Validated records; register

Configuration declares external safety; it does not implement the provider contract.

Engine

Owns: Instance states, authoritative time, claims, retries and transitions

Methods: start, claim, complete, timeout, cancel, retryCompensation, view

No user code or external I/O under its monitor.

Instance

Owns: Pinned definition/input, completed history, reverse undo queue, attempts and accepted outcome evidence

Methods: Private transition state

completed count is historical forward successes, not current uncompensated count.

Claim / View

Owns: Execution capability metadata versus current observation

Methods: Immutable record accessors

Token rejects local stale completion; external destination must enforce idempotency/fencing independently.

Attempt ownership

A lease fences local reporting, not an external side effect by itself.

  1. Due pinned workflow step
  2. ↓ One local active owner

    Claim: token + stable effect key
  3. ↓ Destination honors retry identity

    External execution outside engine
  4. ↓ Old token cannot advance state

    Accept definite evidence or review expiry
Known failure and repair

Unknown effects cannot be wished into the known-success compensation list.

Definite forward failure / safe cancel

Accepted undo reaches FAILED/CANCELLED

Reverse required known successes

Expose COMPENSATION_FAILED / MANUAL_REVIEW

Repair failure or unresolved effect

Data model, schema and relationships

Definition versions own immutable ordered steps; instances pin one definition/input. Attempts own temporary reporting authority, while compensation is separately tracked repair of known accepted effects.

Implementation boundary: The reference is a local linear protocol. Tables propose durable state/claim/outcome storage; persistence, worker execution, timers, replay and review resolution are still extensions.

These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.

workflow_definition: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

workflow_definition relationshipsworkflow_definition 1 to workflow_step 0..*: orders steps. workflow_step.definition_id → workflow_definition.id. workflow_definition 1 to workflow_instance 0..*: pins runs. workflow_instance.definition_id → workflow_definition.id.orders steps1 parent · 0..* childrenworkflow_stepid [PK]definition_id [FK]positionpins runs1 parent · 0..* childrenworkflow_instanceid [PK]definition_id [FK]inputworkflow_definitionid [PK]name
workflow_step: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

workflow_step relationshipsworkflow_step 1 to workflow_attempt 0..*: interprets attempt. workflow_attempt.step_id → workflow_step.id. workflow_step 1 to compensation 0..*: has undo meaning. compensation.step_id → workflow_step.id.interprets attempt1 parent · 0..* childrenworkflow_attemptid [PK]instance_id [FK]step_id [FK]has undo meaning1 parent · 0..* childrencompensationid [PK]instance_id [FK]step_id [FK]workflow_stepid [PK]definition_id [FK]position
workflow_instance: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

workflow_instance relationshipsworkflow_instance 1 to workflow_attempt 0..*: claims attempts. workflow_attempt.instance_id → workflow_instance.id. workflow_instance 1 to compensation 0..*: tracks repair. compensation.instance_id → workflow_instance.id.claims attempts1 parent · 0..* childrenworkflow_attemptid [PK]instance_id [FK]step_id [FK]tracks repair1 parent · 0..* childrencompensationid [PK]instance_id [FK]step_id [FK]workflow_instanceid [PK]definition_id [FK]input

Entity definitions and field contracts

workflow_definition

Immutable versioned execution meaning.

FieldType / keyMeaning and reference
idtext · PKDefinition version
nametext · VALUEWorkflow family

Code mapping: Definition.

  • Same version cannot change contents.

workflow_step

Ordered retry/repair policy.

FieldType / keyMeaning and reference
iduuid · PKDefinition-step identity
definition_idtext · FKPinned definition · workflow_definition.id
positioninteger · VALUELinear order
nametext · VALUEStable step name
max_attemptsinteger · VALUEBounded automatic budget
retry_delaybigint · VALUEPositive fixed delay between safe bounded attempts
idempotentboolean · VALUETrusted external retry safety contract, not remote deduplication implementation
compensateboolean · VALUEWhether known accepted success requires undo on later failure/cancellation

Code mapping: Step.

  • Unique(definition_id,position) and (definition_id,name); retain idempotency/compensation flags and retry delay.

workflow_instance

One pinned execution.

FieldType / keyMeaning and reference
iduuid · PKInstance identity
definition_idtext · FKPinned version · workflow_definition.id
inputtext · VALUEImmutable context
stateenum · VALUEExplicit orchestration state
versionbigint · VALUEConditional transition version
duetimestamp · VALUEEarliest eligible claim instant persisted for recovery

Code mapping: Instance.

  • Start key binds definition+input; RUNNING permits one active token.

workflow_attempt

Temporary worker/reporting authority.

FieldType / keyMeaning and reference
iduuid · PKAttempt identity
instance_iduuid · FKExecution owner · workflow_instance.id
step_iduuid · FKStep interpretation · workflow_step.id
tokenbigint · VALUEFresh instance fencing epoch
external_keytext · VALUEStable logical effect identity
lease_untiltimestamp · VALUEReporting deadline
compensationboolean · VALUEDistinguishes forward work from undo using separate stable external keys

Code mapping: Claim and accepted outcome evidence.

  • Unique(instance_id,token); step must belong to instance pinned definition. Work/undo keys differ and retry tokens change.

compensation

Required repair for known success.

FieldType / keyMeaning and reference
iduuid · PKRepair identity
instance_iduuid · FKWorkflow owner · workflow_instance.id
step_iduuid · FKKnown successful effect · workflow_step.id
statusenum · VALUEPending complete failed review

Code mapping: Undo queue; durable row extension.

  • Unique(instance_id,step_id) for linear nonrepeated steps; repair order reverses accepted successes.

Relationship cardinalities and their reasons

Parent → childMultiplicityFK / why
workflow_definition → workflow_step

orders steps

1 parent per child; 0..* children per parentworkflow_step.definition_id → workflow_definition.id

Definition owns immutable transition interpretation.

workflow_definition → workflow_instance

pins runs

1 parent per child; 0..* children per parentworkflow_instance.definition_id → workflow_definition.id

New versions cannot silently change existing instances.

workflow_instance → workflow_attempt

claims attempts

1 parent per child; 0..* children per parentworkflow_attempt.instance_id → workflow_instance.id

Retained attempts explain lease/ownership history.

workflow_step → workflow_attempt

interprets attempt

1 parent per child; 0..* children per parentworkflow_attempt.step_id → workflow_step.id

Verify same definition as instance; FK alone does not prove compatibility.

workflow_instance → compensation

tracks repair

1 parent per child; 0..* children per parentcompensation.instance_id → workflow_instance.id

Only known successful required effects enter compensation.

workflow_step → compensation

has undo meaning

1 parent per child; 0..* children per parentcompensation.step_id → workflow_step.id

Undo is separate logical work and may fail independently.

Transaction boundaries

  1. Claim: condition instance version/due/state/no active token, assign new token/lease and persist dispatch outbox in one transaction.
  2. Complete: deduplicate definite evidence, check current unexpired ownership, append outcome and advance state/repair obligations atomically. External execution stays outside transaction.
  3. Timeout: retire ownership; safely retry with same effect key only under idempotency/budget, otherwise retain MANUAL_REVIEW.

Indexes and uniqueness

  1. Unique start key plus definition/input fingerprint; unique instance/token.
  2. Index claimable instance(state,due), active attempt(lease_until), compensation(instance_id,status,reverse_order).

Schema traps and failure cases

  1. A database fencing token cannot stop a stale worker committing at an external provider unless that provider honors key/fence or is reconciled.
  2. A failed refund cannot be hidden as clean rollback; repair failure and unknown outcome remain explicit.

Trace the records through one operation

  1. Instance W pins definition v1, input and stable reserve/pay keys.
  2. Pay attempt expires; safe retry has fresh token and same external key.
  3. A later definite forward failure compensates known successes in reverse; failed undo remains visible.

Method contracts

register(definition); start(version,input,key) → View

Re-register equal version contents safely; reject changed contents. Start key binds version+input and returns one instance current view.

claim(instanceId,now) → Claim

READY/WAITING/COMPENSATING must be due with no active token. Claim increments attempt/token, supplies pinned version/input, logical external key and lease deadline.

complete(instanceId,token,outcome,now) → View

Matching accepted definite duplicate returns current view; contradiction rejects. A new outcome requires current unexpired active token. UNKNOWN retains ownership. Success advances or undoes one step; definite failure chooses safe retry or repair.

timeout(instanceId,token,now) → View

Only the expired active RUNNING attempt can timeout. Retire token and choose safe retry or MANUAL_REVIEW; timeout does not establish that the remote effect failed.

cancel(instanceId,now); retryCompensation(instanceId,now); view(instanceId)

Cancellation is intent while running and repair of known successes when safe. Failed compensation can be explicitly retried with reset budget. MANUAL_REVIEW has no automatic resolution in this reference.

What, when, why and how

Each concept is attached to the requirement it protects, its implementation, an alternative and a test.

Version-pinned state machine

what
Keep transition meaning stable for every in-flight instance.
when
Workflow definitions evolve while older instances are still running.
why
Loading the latest step list on every claim can reorder work or compensate a step under rules it never used.
how
register rejects changed contents under one version. start stores the immutable Definition and input with typed retry fingerprint. claim reads that pinned object.

Code: Definition, register, start, Instance · Invariants: I1

Alternatives, cost and failure example

Alternative: An explicit migration protocol can change running instances only with state compatibility checks and auditable mapping.

Cost: Definitions and instances remain retained; no schema migration or persistent event replay is implemented.

Counterexample: Rename charge to debit in v1 while an old instance is waiting and lose its stable external key.

Test: Register changed v1 contents rejects; register v2 then prove old instance claims v1.

Logical operation identity and fenced attempts

what
Separate what effect should happen from which worker currently owns reporting it.
when
A worker times out and another retries while the old worker may still finish.
why
A new random provider key on each retry duplicates effects; accepting an old token advances stale work.
how
Claim uses stable instance/step/work or undo key, fresh token and lease. complete accepts only current unexpired active ownership for new outcomes. Recorded definite duplicates are safe and contradictions reject.

Code: Claim, claim, complete · Invariants: I2, I3

Alternatives, cost and failure example

Alternative: A durable conditional claim row with fencing epoch implements the same local ownership idea across processes.

Cost: Local fencing cannot stop an old worker at the external destination. That destination must honor dedupe/fencing or require reconciliation.

Counterexample: Timed-out charge attempt and replacement both send independent charge IDs.

Test: Retry preserves externalKey and changes token; stale first completion rejects without advancing the replacement.

Bounded retry with honest uncertainty

what
Retry policy is constrained by evidence and side-effect safety.
when
A callback says definite retryable failure or a lease expires without a result.
why
Treating every exception as failure can compensate a successful charge or repeat a non-idempotent action.
how
RETRYABLE uses safe idempotency/budget and fixed due delay. UNKNOWN remains RUNNING. timeout retires token; unsafe, exhausted or cancelled unknown forward work becomes MANUAL_REVIEW.

Code: failed, expire, timeout, complete · Invariants: I3

Alternatives, cost and failure example

Alternative: A reconciliation worker can query external state and submit verified resolution through a carefully designed review API; none is silently invented here.

Cost: Fixed delay has no jitter/backoff classification; declared idempotency is trusted. Manual review can remain unresolved indefinitely.

Counterexample: Lease expires on non-idempotent shipment and engine assumes nothing shipped.

Test: Assert non-idempotent timeout and cancelled unknown timeout stop in MANUAL_REVIEW instead of reporting clean cancellation.

Saga compensation of known effects

what
Repair accepted prior successes with explicit reverse operations.
when
Later forward work definitely fails or cancellation arrives.
why
Rollback of memory cannot undo external charges or shipments, and repair itself may fail.
how
beginUndo builds a reverse list from completed successes with compensate=true. Compensation claims use separate stable undo keys/budgets. Definite repair failure exposes COMPENSATION_FAILED; explicit retry resets that undo budget.

Code: beginUndo, failed, retryCompensation, complete · Invariants: I4

Alternatives, cost and failure example

Alternative: A single ACID transaction is preferable when every mutation belongs to one database and no remote effects escape its transaction.

Cost: Compensation is business repair, not time travel. Refund fees, irreversible delivery and human review need explicit domain policies.

Counterexample: Mark FAILED immediately after charge then ignore a failed refund.

Test: Force forward fatal failure, then fail compensation, retry it explicitly and reach FAILED only after accepted undo.

Read and run the Java reference

Reveal complete Solution.java

Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.

Solution.java Java 21
import java.util.*;
/** Local linear-workflow protocol. Claims are executed by an external trusted worker, not this engine. */
public final class Solution {
    private Solution() {}
    public enum State { READY, RUNNING, WAITING, COMPENSATING, SUCCEEDED, FAILED, CANCELLED, MANUAL_REVIEW, COMPENSATION_FAILED }
    public enum Outcome { SUCCESS, RETRYABLE, FATAL, UNKNOWN }
    public record Step(String name,int maxAttempts,long retryDelay,boolean idempotent,boolean compensate){public Step{if(name==null||name.isBlank()||maxAttempts<1||maxAttempts>5||retryDelay<1)throw new IllegalArgumentException("step");}}
    public record Definition(String version,List<Step> steps){public Definition{if(version==null||version.isBlank()||steps.isEmpty())throw new IllegalArgumentException("definition");steps=List.copyOf(steps);if(steps.stream().map(Step::name).distinct().count()!=steps.size())throw new IllegalArgumentException("duplicate step");}}
    public record Claim(String instanceId,String definitionVersion,String input,String step,long token,boolean compensation,String externalKey,long leaseUntil){}
    public record View(String id,String definitionVersion,State state,int completed,long due,long activeToken,boolean cancelRequested){}
    private record Start(String version,String input,String instanceId){}
    private static final class Instance {
        final String id,input;final Definition definition;final List<Integer> completed=new ArrayList<>();final ArrayDeque<Integer> undo=new ArrayDeque<>();final Map<Long,Outcome> outcomes=new HashMap<>();
        State state=State.READY;int index,attempts,undoAttempts;long nextToken,activeToken,leaseUntil,due;boolean cancel,compensating;
        Instance(String id,String input,Definition definition){this.id=id;this.input=input;this.definition=definition;}
    }
    public static final class Engine {
        private final long leaseDuration;
        private final Map<String,Definition> definitions=new HashMap<>();
        private final Map<String,Instance> instances=new HashMap<>();
        private final Map<String,Start> starts=new HashMap<>();
        private long sequence,time;
        public Engine(long leaseDuration){if(leaseDuration<1)throw new IllegalArgumentException("lease");this.leaseDuration=leaseDuration;}
        private void at(long now){if(now<time)throw new IllegalArgumentException("time");time=now;}
        public synchronized void register(Definition definition){Objects.requireNonNull(definition);var old=definitions.get(definition.version());if(old!=null&&!old.equals(definition))throw new IllegalArgumentException("version is immutable");definitions.put(definition.version(),definition);}
        private Instance instance(String id){var i=instances.get(id);if(i==null)throw new IllegalArgumentException("instance");return i;}
        public synchronized View start(String version,String input,String key){if(input==null||key==null||key.isBlank())throw new IllegalArgumentException("start");var old=starts.get(key);if(old!=null){if(!old.version().equals(version)||!old.input().equals(input))throw new IllegalArgumentException("key reused");return view(old.instanceId());}var definition=definitions.get(version);if(definition==null)throw new IllegalArgumentException("definition");var i=new Instance("W"+(++sequence),input,definition);instances.put(i.id,i);starts.put(key,new Start(version,input,i.id));return view(i.id);}
        private Step step(Instance i){return i.definition.steps().get(i.compensating?i.undo.peekFirst():i.index);}
        public synchronized Claim claim(String id,long now){
            at(now);var i=instance(id);if(!Set.of(State.READY,State.WAITING,State.COMPENSATING).contains(i.state)||now<i.due||i.activeToken!=0)throw new IllegalStateException("not claimable");
            long until=Math.addExact(now,leaseDuration),token=Math.incrementExact(i.nextToken);i.nextToken=token;i.activeToken=token;i.leaseUntil=until;i.state=State.RUNNING;if(i.compensating)i.undoAttempts++;else i.attempts++;var step=step(i);return new Claim(i.id,i.definition.version(),i.input,step.name(),token,i.compensating,i.id+"/"+step.name()+"/"+(i.compensating?"undo":"work"),until);
        }
        private void beginUndo(Instance i){
            i.activeToken=0;i.compensating=true;i.undo.clear();for(int n=i.completed.size()-1;n>=0;n--){int index=i.completed.get(n);if(i.definition.steps().get(index).compensate())i.undo.addLast(index);}i.undoAttempts=0;i.due=time;i.state=i.undo.isEmpty()?(i.cancel?State.CANCELLED:State.FAILED):State.COMPENSATING;
        }
        private void failed(Instance i,boolean retryable){
            var step=step(i);int used=i.compensating?i.undoAttempts:i.attempts;
            if(retryable&&step.idempotent()&&used<step.maxAttempts()&&!(!i.compensating&&i.cancel)){i.due=Math.addExact(time,step.retryDelay());i.state=i.compensating?State.COMPENSATING:State.WAITING;}
            else if(i.compensating)i.state=State.COMPENSATION_FAILED;else beginUndo(i);
        }
        public synchronized View complete(String id,long token,Outcome outcome,long now){
            Objects.requireNonNull(outcome);var i=instance(id);var old=i.outcomes.get(token);if(old!=null){if(old!=outcome)throw new IllegalStateException("contradictory completion");return view(id);}at(now);
            if(i.state!=State.RUNNING||i.activeToken!=token)throw new IllegalStateException("stale completion");
            if(now>=i.leaseUntil){Math.addExact(now,step(i).retryDelay());expire(i);throw new IllegalStateException("lease expired; reconcile external outcome");}
            if(outcome==Outcome.UNKNOWN)return view(id);
            // Prepare retry arithmetic before accepting an outcome that may need it.
            if(outcome==Outcome.RETRYABLE)Math.addExact(now,step(i).retryDelay());
            i.outcomes.put(token,outcome);i.activeToken=0;
            if(outcome==Outcome.SUCCESS){
                if(i.compensating){i.undo.removeFirst();i.undoAttempts=0;i.due=time;i.state=i.undo.isEmpty()?(i.cancel?State.CANCELLED:State.FAILED):State.COMPENSATING;}
                else{i.completed.add(i.index);i.index++;i.attempts=0;i.due=time;if(i.cancel)beginUndo(i);else i.state=i.index==i.definition.steps().size()?State.SUCCEEDED:State.READY;}
            }else failed(i,outcome==Outcome.RETRYABLE);return view(id);
        }
        private void expire(Instance i){var step=step(i);i.activeToken=0;int used=i.compensating?i.undoAttempts:i.attempts;if(!step.idempotent()||used>=step.maxAttempts()||(i.cancel&&!i.compensating)){i.state=State.MANUAL_REVIEW;return;}failed(i,true);}
        public synchronized View timeout(String id,long token,long now){at(now);var i=instance(id);if(i.state!=State.RUNNING||i.activeToken!=token||now<i.leaseUntil)throw new IllegalStateException("not expired active attempt");Math.addExact(now,step(i).retryDelay());expire(i);return view(id);}
        public synchronized View cancel(String id,long now){at(now);var i=instance(id);if(Set.of(State.SUCCEEDED,State.FAILED,State.CANCELLED,State.MANUAL_REVIEW,State.COMPENSATION_FAILED).contains(i.state))throw new IllegalStateException("terminal or review");i.cancel=true;if(i.state!=State.RUNNING&&!i.compensating)beginUndo(i);return view(id);}
        public synchronized View retryCompensation(String id,long now){at(now);var i=instance(id);if(i.state!=State.COMPENSATION_FAILED)throw new IllegalStateException("no failed compensation");i.undoAttempts=0;i.due=now;i.state=State.COMPENSATING;return view(id);}
        public synchronized View view(String id){var i=instance(id);return new View(i.id,i.definition.version(),i.state,i.completed.size(),i.due,i.activeToken,i.cancel);}
    }
}

claim grants one current attempt

Eligibility and active-token assignment occur together. The returned Claim carries version/input and external key; execution happens elsewhere.

complete resolves duplicates before new transitions

Previously accepted definite evidence returns its current view if identical; contradiction rejects. Fresh outcomes must own a current unexpired claim.

UNKNOWN keeps the claim unresolved

A later definite response may still resolve the active attempt before lease expiry. After expiry the old worker cannot regain ownership by reporting late success.

beginUndo and retryCompensation expose repair

Undo order is reverse known success, with fresh tokens but stable repair keys. A manual retry is explicit evidence of operator intent, not automatic cleanup of every uncertain case.

Complexity and limits

  • With S steps and H retained accepted outcomes, definition validation costs O(S); start stores a definition reference rather than copying all step state. Map lookups are expected O(1).
  • claim/view and ordinary completion are expected O(1), excluding initial record creation. beginUndo scans completed history O(S); the undo deque consumes O(S) and executes one claim at a time.
  • Space grows with registered definitions, instances, completed/undo lists, start keys and outcome history. Attempts are bounded per automatic retry cycle, but manual compensation resets and total instance count are not memory quotas.
  • One monitor serializes transitions, not external worker execution. There is no background timeout scan, durable scheduler, recovery log or distributed ownership throughput guarantee.

Walk through the actual reference

These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.

Retry token versus external identity

Step 1 of 3: Reserve succeeds; pay retryable

WAITING until t=4

Invariant: Known completed steps retained

Read the complete trace table
ActionState after actionInvariant
Reserve succeeds; pay retryableWAITING until t=4Known completed steps retained
Claim retryNew fencing token, same pay external keyRetry identity and ownership epoch differ
Pay fatal; reserve compensation fails then retriesCOMPENSATION_FAILED then FAILED after confirmed repairCompensation is tracked business work

Lease expiry cannot prove external failure

Step 1 of 2: Unknown idempotent attempt expires

Retry allowed within budget with fresh token

Invariant: Old completion cannot commit

Read the complete trace table
ActionState after actionInvariant
Unknown idempotent attempt expiresRetry allowed within budget with fresh tokenOld completion cannot commit
Non-idempotent or cancelled unknown attempt expiresMANUAL_REVIEWDo not invent a safe retry or completed compensation

Mistakes and counterexamples

Generate a new provider key for every attempt

Failing case: A late first charge and retry both commit.

Repair: Stable logical external key; fresh local attempt token.

Treat timeout as definite failure

Failing case: Charge committed but its response was lost.

Repair: Keep unknown evidence and reconcile or use proven idempotency.

Run callbacks while holding engine ownership

Failing case: A slow provider blocks cancellation and every other workflow.

Repair: Return claims and accept correlated completion later.

Hide compensation failure as rolled back

Failing case: Refund failed but workflow reports clean cancellation.

Repair: Expose repair state and explicit operator retry/reconciliation.

Change a definition in place

Failing case: A waiting old instance resumes under new step meaning.

Repair: Immutable versions and explicit migration policy.

Engineer → Senior → Staff

Choose a deep dive

Implement a versioned linear state machine.

Executed: immutable definitions, start retry keys, successful progression and duplicate outcome guards.

Change exercise

Add a step output consumed by the next step.

Worked answer guidance

Define a typed immutable result schema pinned to step/definition version, store it atomically with accepted SUCCESS and supply only accepted prior outputs in the next claim. Do not let arbitrary workers mutate shared instance input.

Interview follow-ups

Why is Claim input immutable String?

Workers need start context without receiving mutable engine state. Real payload schemas need size, privacy and version contracts.

Does completed decrease after compensation?

No. It counts historical accepted forward successes. Separate undo state represents repair; history remains evidence.

Your design attempt

Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.

Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.

Solution.java
Run program · Java 21

No unsaved changes · 208/20000

Self-review

Checks apply to this visit. Self-review is not a passed submission or verified mastery.

Open focused practice →

Provenance and scope of evidence

Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.

Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.