Bird
Raised Fist0

Design a Workflow Engine - Step, Transition, Condition, Executor, State Store

LLDhardInterview freq: medium
Choose your preparation mode4 modes available
๐ŸŽฏ
Design a Workflow Engine - Step, Transition, Condition, Executor, State Store
40 min ยท medium
medium

Design a workflow engine that models business processes as a directed graph of Steps connected by Transitions. Each Transition has a Condition that determines if it can be taken. Executors run the logic of Steps. The engine must maintain workflow state persistently and support concurrent execution of multiple workflows.

  • Steps represent discrete units of work with unique IDs and can have multiple outgoing Transitions.
  • Transitions connect Steps and have Conditions that evaluate runtime data to decide if the Transition is valid.
  • Executors encapsulate the logic to execute a Step and produce output for Conditions.
  • State Store persists the current Step of each workflow instance and must be thread-safe for concurrent access.
  • Conditions can be composite (AND, OR) or atomic, requiring an extensible design.
  • The engine must allow adding new Step types and Condition types without modifying existing code.
๐Ÿ“‹
Guided Steps
1/5
Step 1
Design Step and Transition classes with state
Define the Step and Transition classes capturing their state and relationships. How do you represent the connections and ensure Steps can have multiple outgoing Transitions?
๐Ÿ’ก Hint
Use composition for Step containing a list of Transitions. Use enum for Transition status if needed. Remember Java classes can hold instance state; interfaces cannot.
abstract class Step with List<Transition> outgoingTransitions - abstract because Step holds instance state (id, transitions) which interfaces cannot hold in Java
Mid: Uses composition to hold Transitions inside Step. Senior: Explains why abstract class is needed for shared state and how List<Transition> enables multiple outgoing edges.
โš ๏ธCommon Wrong Answer
Using interface for Step with default methods but no fields - interfaces cannot hold instance state in Java, so transitions cannot be stored, breaking the model.
Step 2
Model Condition hierarchy for extensible evaluation
Design the Condition abstraction to support atomic and composite conditions. How do you ensure extensibility and type safety?
๐Ÿ’ก Hint
Use an abstract class or interface for Condition with an evaluate() method. Use enum or sealed classes for condition types. Composite conditions hold child Conditions.
interface Condition with boolean evaluate(Context ctx) - interface used here because Condition has behavior but no shared instance state; composite conditions hold List<Condition> as aggregation
Mid: Defines Condition interface with evaluate method. Senior: Explains why interface is chosen (no shared state), and how composite pattern is implemented with aggregation of Conditions.
โš ๏ธCommon Wrong Answer
Using abstract class Condition with mutable shared state leads to thread-safety issues when reused across workflows.
Step 3
Implement Executor abstraction for Step execution
How do you design the Executor abstraction to run Step logic and produce outputs for Conditions? How do you decouple Executor from Step?
๐Ÿ’ก Hint
Use interface Executor with execute(StepContext) returning ExecutionResult. Executor implementations are stateless and can be shared.
interface Executor with ExecutionResult execute(StepContext ctx) - interface because Executor defines behavior without instance state; decouples execution logic from Step state
Mid: Defines Executor interface with execute method. Senior: Explains statelessness of Executor implementations and how interface enables multiple implementations without inheritance constraints.
โš ๏ธCommon Wrong Answer
Embedding execution logic inside Step subclasses violates Single Responsibility Principle and makes extension harder.
Step 4
Design thread-safe State Store for workflow progress
Design the State Store to persist and retrieve the current Step of each workflow instance. How do you ensure thread safety for concurrent workflow executions?
๐Ÿ’ก Hint
Use ConcurrentHashMap or synchronized methods. Use volatile for shared mutable state references. Consider atomic operations for state transitions.
class StateStore with private final ConcurrentHashMap<String, String> workflowStepMap - uses ConcurrentHashMap for thread-safe atomic get/put operations without explicit synchronization
Mid: Uses thread-safe collection for state storage. Senior: Explains how ConcurrentHashMap prevents race conditions during concurrent reads/writes without blocking all threads.
โš ๏ธCommon Wrong Answer
Using HashMap without synchronization leads to race conditions and corrupted state under concurrent access.
Step 5
Implement Transition evaluation and workflow progression
How do you implement the logic to evaluate Transitions' Conditions and move the workflow to the next Step atomically?
๐Ÿ’ก Hint
Use synchronized method to atomically check current Step, evaluate Conditions, and update State Store. Use volatile for current Step reference if cached.
public synchronized boolean advanceWorkflow(String workflowId, Context ctx) - synchronized to atomically read current Step, evaluate Transitions, and update State Store to prevent lost updates
Mid: Uses synchronized method to ensure atomicity. Senior: Traces thread interleaving where without synchronization two threads read same Step and both advance, causing inconsistent state.
โš ๏ธCommon Wrong Answer
Not synchronizing advanceWorkflow leads to race conditions where multiple threads advance the same workflow simultaneously, causing invalid state transitions.
๐Ÿ”Œ
API Design Challenge
Design the API for the workflow engine to allow clients to start workflows, advance them based on conditions, and query current state. How do you ensure thread safety and extensibility in the API?
  • Use synchronized or atomic operations in methods that modify workflow state to prevent race conditions.
  • Design interfaces for Steps, Conditions, and Executors to allow adding new implementations without changing existing code.
  • Provide clear method signatures for starting workflows, advancing steps, and querying state with appropriate parameters and return types.
๐Ÿ“Š
Scoring Rubric
โ–ผ Show
Must Have - 60%
  • abstract class Step with instance state and composition of Transitions
  • interface Condition with evaluate method and composite pattern
  • interface Executor for stateless execution logic
  • StateStore with ConcurrentHashMap for thread-safe state persistence
  • synchronized advanceWorkflow method to prevent race conditions
Good to Have - 30%
  • Use enum for ConditionType and TransitionStatus
  • Explain SOLID principles applied
  • Discuss lifecycle relationships between components
Bonus - 10%
  • Explain why volatile is not needed for ConcurrentHashMap
  • Provide example of composite Condition implementation
  • Discuss thread safety in detail
โœ…
Model Answer
โ–ผ Reveal

The workflow engine models business processes as a directed graph of Steps connected by Transitions guarded by Conditions. Executors run Step logic producing outputs for Conditions. The State Store persists workflow progress using thread-safe collections. The design uses Java-specific OOP and concurrency features to ensure extensibility, thread safety, and maintainability.

  • Use abstract class for Step to hold instance state (id, transitions) since interfaces cannot hold state in Java
  • Use interface for Condition to define behavior without shared mutable state, enabling composite pattern with aggregation
  • Use interface for Executor to separate execution logic from Step state and allow stateless shared implementations
  • Use ConcurrentHashMap in StateStore to provide thread-safe atomic get/put operations without explicit synchronization
  • Synchronize advanceWorkflow method to prevent race conditions during concurrent workflow progression
  • Use enums for ConditionType and TransitionStatus to ensure type safety and exhaustive handling
  • Avoid volatile for StateStore map references because ConcurrentHashMap provides necessary visibility guarantees
  • abstract class Step holds unique id and List<Transition> - abstract because it holds instance state which interfaces cannot hold in Java
  • interface Condition with boolean evaluate(Context) - interface chosen for behavior without shared state; composite pattern implemented via aggregation of Conditions
  • interface Executor with ExecutionResult execute(StepContext) - stateless behavior abstraction enabling multiple implementations
  • StateStore uses ConcurrentHashMap<String, String> for thread-safe workflow state persistence without explicit synchronization
  • advanceWorkflow method synchronized to atomically check current Step, evaluate Conditions, and update StateStore to prevent race conditions
  • Use enum for ConditionType and TransitionStatus to ensure type safety and exhaustive switch statements
  • Composition relationship: Step owns Transitions; aggregation: Condition composites hold child Conditions; association: Executor used by Step execution
  • Volatile not needed for StateStore map because ConcurrentHashMap provides visibility guarantees
  • SOLID principles: Single Responsibility (Executor separate from Step), Open/Closed (Condition extensible via interface), Liskov Substitution (Step subclasses interchangeable)
  • Lifecycle: Step owns Transitions (composition), Conditions can exist independently (aggregation), Executors stateless and shared (association)

Practice

(1/5)
1. In a Wallet System design, the Transaction class has a private constructor and a static synchronized getInstance() method that returns the single instance. Which design pattern is applied here and why is this the correct use case?
easy
A. Factory Method pattern, because it controls object creation with a static method.
B. Singleton pattern, ensuring only one Transaction instance exists to maintain consistency.
C. Builder pattern, to construct complex Transaction objects step-by-step.
D. Prototype pattern, to clone existing Transaction instances efficiently.

Solution

  1. Step 1: Identify the pattern from constructor and method signatures

    The private constructor prevents external instantiation, and the static synchronized getInstance() method ensures a single instance is created and accessed thread-safely.
  2. Step 2: Why Singleton fits this Wallet Transaction scenario

    Singleton is appropriate when exactly one instance is needed to coordinate actions or maintain global state, such as a ledger manager or transaction coordinator. Factory Method and Builder patterns do not enforce single instance, and Prototype is for cloning, not restricting instances.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Private constructor + synchronized static getInstance() -> Singleton pattern [check]
Hint: Private constructor + static synchronized method -> Singleton [check]
Common Mistakes:
2. In designing the ESPNcricinfo system, should the Ball entity be modeled as an abstract class or an interface? Consider that Ball has state like ballNumber and behavior like calculateRuns(). Which is the best choice and why?
easy
A. Abstract class, because Ball has shared state and behavior that subclasses inherit.
B. Interface, because Ball only defines behavior and no state should be stored.
C. Interface, because Java interfaces can now have default methods with state variables.
D. Abstract class, because it allows multiple inheritance of Ball features.

Solution

  1. Step 1: Analyze Ball's state and behavior

    Ball has state (ballNumber) and behavior (calculateRuns), so it needs to store data and provide common logic.
  2. Step 2: Abstract class vs interface for state

    Interfaces cannot hold instance state; abstract classes can hold state and provide shared implementation.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Stateful entity with behavior -> abstract class [check]
Hint: Stateful with behavior -> abstract class, stateless behavior -> interface [check]
Common Mistakes:
3. In a parking system, should Vehicle be an abstract class or an interface if it needs to hold state like licensePlate and provide default behavior for getSize()? Why?
easy
A. Interface, because Java 8+ interfaces can have default methods and static fields
B. Interface, because it allows multiple inheritance and enforces method contracts without state
C. Abstract class, because it can hold state and provide default method implementations
D. Abstract class, because it forces subclasses to implement all methods without defaults

Solution

  1. Step 1: Analyze state and behavior requirements

    Since Vehicle needs to hold state like licensePlate and provide default behavior, it requires fields and method implementations.
  2. Step 2: Abstract class vs interface capabilities

    Abstract classes can have instance fields and concrete methods, while interfaces cannot hold instance state (only constants) and default methods cannot manage instance variables.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    State + default behavior -> abstract class, not interface [check]
Hint: State needs abstract class; interfaces lack instance fields [check]
Common Mistakes:
4. In the Food Ordering System, the KitchenQueue class has a parkOrder() method that checks if a slot is available before assigning an order. Without synchronization on parkOrder(), which exact thread interleaving causes double-booking of the same slot?
medium
A. Thread 1 and Thread 2 both call parkOrder() simultaneously and the JVM throws an exception.
B. Thread 1 checks slot availability -> true, context switch before assignment; Thread 2 checks availability -> true, both assign same slot.
C. Thread 1 assigns slot, then Thread 2 waits for Thread 1 to finish before assigning next slot.
D. Thread 1 and Thread 2 both wait on a lock, causing deadlock and no assignment.

Solution

  1. Step 1: Identify the check-then-act race condition

    Without synchronization, Thread 1 reads slot as available but is preempted before assignment; Thread 2 also reads available and proceeds.
  2. Step 2: Both threads assign the same slot leading to double-booking

    Because the availability check and assignment are not atomic, both threads assign the same slot concurrently.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Check-then-act gap without sync -> double assignment [check]
Hint: Check availability -> context switch -> both assign same slot [check]
Common Mistakes:
5. In the Stack Overflow voting system, if the castVote() method is not synchronized, which exact sequence of thread interleaving causes a double vote count for the same user on a question?
medium
A. Thread 1 reads vote count, Thread 2 reads vote count, both increment and write back simultaneously
B. Thread 1 checks if user has voted -> false, context switch; Thread 2 checks if user has voted -> false; both proceed to cast vote
C. Thread 1 updates vote count, Thread 2 waits; Thread 1 finishes, Thread 2 updates vote count again
D. Thread 1 and Thread 2 both block on the same lock, causing deadlock

Solution

  1. Step 1: Identify the check-then-act race condition

    The critical issue is the check if the user has already voted. Without synchronization, both threads see no vote recorded and proceed.
  2. Step 2: Trace exact interleaving causing double vote

    Thread 1 checks voted -> false, context switch; Thread 2 checks voted -> false; both cast votes, causing double count.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Check-then-act gap without sync -> double vote [check]
Hint: Check-then-act gap -> double vote without sync [check]
Common Mistakes:
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard