Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,050visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsMachine Coding Exercises
✓ FreeAdvanced· 8 min read

Design a Workflow Engine

Workflow, Step, WorkflowExecutor, and WorkflowContext as the core classes, sequential execution with output-based branching, and the direct conceptual link to the saga pattern orchestrator.

Published September 23, 2026


Design a Workflow Engine

Core classes

interface Step { StepResult execute(WorkflowContext context); }
class StepResult { boolean success; String nextStepId; /* null means "use default next step" */ }
class WorkflowContext { Map<String, Object> data = new HashMap<>(); } // shared state passed between steps

class Workflow {
    String id;
    Map<String, Step> steps; // stepId -> Step
    String startStepId;
}

class WorkflowExecutor {
    void run(Workflow workflow, WorkflowContext context) {
        String currentStepId = workflow.startStepId;
        while (currentStepId != null) {
            Step step = workflow.steps.get(currentStepId);
            StepResult result = step.execute(context);
            if (!result.success) { handleFailure(workflow, currentStepId, context); break; }
            currentStepId = result.nextStepId;
        }
    }
}

Step as an interface is what lets a workflow be composed of independently-testable, independently-reusable units — each step only knows how to do ITS job and decide the next step ID; it has no awareness of the workflow's overall shape.

Advertisement

Sequential execution with branching

class CheckInventoryStep implements Step {
    public StepResult execute(WorkflowContext ctx) {
        boolean inStock = inventoryService.checkStock((String) ctx.data.get("productId"));
        ctx.data.put("inStock", inStock);
        return new StepResult(true, inStock ? "reserveInventory" : "notifyOutOfStock"); // branches based on OUTPUT
    }
}

A step's nextStepId being determined by ITS OWN execution result (not a fixed, hardcoded sequence baked into the WorkflowExecutor) is what enables genuine BRANCHING — the same workflow definition can take different paths depending on what happens at each step, without the executor itself needing any workflow-specific branching logic. The executor stays generic; all the domain-specific branching logic lives inside individual steps.

The direct conceptual link to the saga pattern orchestrator

This workflow engine's shape — a sequence of steps, each with a defined outcome, executed by a central coordinator tracking progress — is STRUCTURALLY the same shape as the saga pattern's orchestrator (Data Ownership Model's cross-service-write coordination) — a saga IS a workflow, specifically one where each step is a call to a DIFFERENT SERVICE'S local transaction, and "failure handling" means running COMPENSATING actions for already-completed steps rather than simply stopping. Building this generic workflow engine here is what makes a saga orchestrator's design legible as "a workflow engine, specialized for cross-service transactions with compensation" rather than an entirely separate concept to learn from scratch.

Retries, compensation and persisted progress

The executor above runs steps in memory, and stops at the first failure. Three additions turn it into something you could run real business processes on: retries for transient failures, compensation to undo completed steps when the workflow can't finish, and persisted state, so a crash doesn't lose track of where each workflow was.

interface Step {
    String id();
    StepResult execute(WorkflowContext ctx) throws Exception;
    default void compensate(WorkflowContext ctx) {}                  // undo, for steps with side effects
    default RetryPolicy retryPolicy() { return RetryPolicy.none(); }
}

record RetryPolicy(int maxAttempts, Duration initialBackoff, Set<Class<? extends Exception>> retryOn) {
    static RetryPolicy none() { return new RetryPolicy(1, Duration.ZERO, Set.of()); }
    boolean shouldRetry(Exception e, int attempt) {
        return attempt < maxAttempts && retryOn.stream().anyMatch(t -> t.isInstance(e));
    }
}

enum RunStatus { RUNNING, COMPLETED, COMPENSATING, COMPENSATED, FAILED }

class WorkflowRun {                          // the persisted record of one execution
    String runId; String workflowId; RunStatus status;
    String currentStepId; Deque<String> completedSteps = new ArrayDeque<>();
    Map<String, Object> data = new HashMap<>();
}

class WorkflowExecutor {
    private final WorkflowRunRepository runs;   // durable storage (a DB table), saved after every step

    void execute(Workflow wf, WorkflowRun run) {
        String stepId = run.currentStepId;
        while (stepId != null) {
            Step step = wf.step(stepId);
            StepResult result;
            try {
                result = runWithRetry(step, new WorkflowContext(run.data));
            } catch (Exception permanentFailure) {
                compensate(wf, run);
                return;
            }
            run.completedSteps.push(stepId);
            stepId = result.nextStepId() != null ? result.nextStepId() : wf.defaultNext(stepId);
            run.currentStepId = stepId;
            runs.save(run);                     // checkpoint: a restart resumes from here
        }
        run.status = RunStatus.COMPLETED;
        runs.save(run);
    }

    private StepResult runWithRetry(Step step, WorkflowContext ctx) throws Exception {
        Duration backoff = step.retryPolicy().initialBackoff();
        for (int attempt = 1; ; attempt++) {
            try { return step.execute(ctx); }
            catch (Exception e) {
                if (!step.retryPolicy().shouldRetry(e, attempt)) throw e;
                Thread.sleep(backoff.toMillis());
                backoff = backoff.multipliedBy(2);   // exponential backoff (add jitter in production)
            }
        }
    }

    private void compensate(Workflow wf, WorkflowRun run) {
        run.status = RunStatus.COMPENSATING;
        runs.save(run);
        while (!run.completedSteps.isEmpty()) {
            String done = run.completedSteps.pop();            // reverse order of completion
            try { wf.step(done).compensate(new WorkflowContext(run.data)); }
            catch (Exception e) { run.status = RunStatus.FAILED; runs.save(run); alertOperator(run, done, e); return; }
            runs.save(run);
        }
        run.status = RunStatus.COMPENSATED;
        runs.save(run);
    }
}

Why each piece matters:

  • Retry only transient errors (timeouts, 503s), with backoff. A validation error will fail identically every time, so retrying it only delays the inevitable.
  • Compensation runs in reverse order: release inventory after refunding the payment, mirroring the order in which things were done. It's the saga pattern, with this engine as the orchestrator.
  • A failed compensation is escalated to a human. You can't always undo automatically (the refund API itself may be down), and a silent half-rolled-back state is the worst outcome.
  • Checkpoint after every step. After a crash, the engine reloads RUNNING workflows and resumes each from currentStepId.

The idempotency requirement

Checkpointing creates an unavoidable gap: a step can succeed, and the process can crash before the checkpoint is saved. On restart, that step runs again. So every step with side effects must be idempotent, meaning safe to run twice. The usual technique is to pass a key derived from the run and step (runId + ":" + stepId) to downstream services, which remember keys they've already processed:

class ChargePaymentStep implements Step {
    public StepResult execute(WorkflowContext ctx) {
        String idempotencyKey = ctx.runId() + ":charge";
        PaymentReceipt r = payments.charge(ctx.get("orderId"), ctx.get("amount"), idempotencyKey);
        ctx.put("paymentId", r.id());
        return StepResult.next();
    }
    public void compensate(WorkflowContext ctx) {
        payments.refund(ctx.get("paymentId"), ctx.runId() + ":refund");
    }
}

This is the same reasoning behind production engines such as Temporal, AWS Step Functions and Camunda. They add durable timers (for "wait three days for approval"), versioning of workflow definitions, and horizontal scaling of workers. The core model (steps, checkpoints, retries, compensation, idempotency) is what's shown here.

Follow-up questions this topic invites — and their answers

Q: How would you add compensating-action support to turn this into a genuine saga orchestrator? A: Each Step would additionally expose a compensate(WorkflowContext) method; WorkflowExecutor would need to track which steps have ALREADY SUCCEEDED, and on a later step's failure, walk backward through the completed steps calling their compensate() methods in reverse order — directly implementing the Payment System / E-Commerce Checkout saga examples discussed earlier in this course.

Q: Does this design support PARALLEL steps (two independent steps running at once), or only sequential? A: As written, purely sequential — supporting parallelism would require StepResult to be able to specify MULTIPLE next steps, and WorkflowExecutor to track multiple concurrently-in-progress branches (and typically a JOIN point where they must all complete before proceeding) — a meaningfully more complex executor, though the same Step/WorkflowContext building blocks still apply.

Q: How would a long-running workflow (spanning hours or days, e.g. waiting for a human approval step) be handled, given this in-memory implementation? A: The WorkflowContext and current-step-position would need to be PERSISTED between steps (not held purely in memory across the while loop), letting execution PAUSE and RESUME later — a durable workflow engine (like Temporal or AWS Step Functions in practice) is built around exactly this persistence requirement, which this simplified in-memory version deliberately sets aside to focus on the core step/branching logic.

Q: Should WorkflowContext's data map be typed, rather than Map<String, Object>? A: For a real production system, yes — a loosely-typed context risks a step reading a key that was never set, or casting to the wrong type, both only discovered at runtime; a more robust design would use a typed context object per workflow (or at minimum, well-documented, validated keys), trading some genericity for real compile-time safety.

Q: Why must workflow steps be idempotent even with checkpointing? A: A step can complete its side effect, and the process can crash before the checkpoint is written. On recovery, the engine re-runs that step. Idempotency keys let downstream systems recognize the repeat and return the original result, instead of acting twice.

Q: Why are compensations run in reverse order? A: Later steps often depend on earlier ones. You ship only after charging, and you charge only after reserving stock. Undoing in reverse keeps each compensation's assumptions valid, just as a stack unwinds.

Q: How would the engine handle a step that waits days for human approval? A: Don't block a thread. Persist the run in a WAITING state with the event or timer it's waiting for. When the approval event arrives, or the timer fires, the engine loads the run and continues from the next step.

Advertisement

Previous

Design a Plugin/Extension System

Next

Multi-Pattern Capstone: Design a Food Delivery App

AI Tutor

Lesson: Design a Workflow Engine

Quick actions

AI responses can be inaccurate. Verify critical information.