Bird
Raised Fist0

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

LLDhardInterview freq: medium
Choose your preparation mode4 modes available
▶
Class Trace
Step 1/5
WorkflowEngine Singleton

WorkflowEngine is a Singleton with a volatile instance field to ensure safe lazy initialization.

🎯 Tests whether candidate knows volatile is needed on Singleton instance to prevent JVM reordering and ensure fully constructed object visibility across threads.
public class WorkflowEngine {
  private static volatile WorkflowEngine instance;
  private WorkflowEngine() {}
  public static WorkflowEngine getInstance() {
    if (instance == null) {
      synchronized (WorkflowEngine.class) {
        if (instance == null) {
          instance = new WorkflowEngine();
        }
      }
    }
    return instance;
  }
  public synchronized void startWorkflow(Workflow w) {
    // start workflow logic
  }
}
Workflow composition of Steps

Workflow composes multiple Step objects, owning their lifecycle, enforcing strong composition.

🎯 Tests understanding of composition lifecycle: Steps cannot exist independently without Workflow, enforcing encapsulation and lifecycle management.
public class Workflow {
  private List<Step> steps = new ArrayList<>();
  public void addStep(Step s) {
    steps.add(s);
  }
  public List<Step> getSteps() {
    return Collections.unmodifiableList(steps);
  }
}
Step as Abstract class with shared state

Step is an abstract class holding shared state like id and name, and a synchronized execute method.

🎯 Tests why abstract class is chosen over interface: Step holds instance fields which interfaces cannot have in Java, and synchronized execute ensures thread-safe step execution.
public abstract class Step {
  protected String id;
  protected String name;
  public synchronized void execute(WorkflowInstance inst) {
    // execution logic
  }
}
Condition interface realization

Condition is an interface realized by concrete classes to evaluate workflow instance state.

🎯 Tests Strategy pattern usage: Condition interface enables open-closed principle by allowing new conditions without modifying existing code.
public interface Condition {
  boolean evaluate(WorkflowInstance inst);
}
Executor interface with synchronized executeStep

Executor interface defines executeStep method which is synchronized in implementations to ensure thread-safe step execution.

🎯 Tests concurrency control at method level using synchronized keyword to prevent race conditions during step execution.
public interface Executor {
  void executeStep(Step step, WorkflowInstance inst);
}
public class DefaultExecutor implements Executor {
  public synchronized void executeStep(Step step, WorkflowInstance inst) {
    step.execute(inst);
  }
}
📐
Class Diagram
Class Diagram
       +----------------+
       | <<Singleton>>  |
       | WorkflowEng    |
       |----------------|
       | - instance:    |
       |   volatile     |
       |   WorkflowEng  |
       |----------------|
       | + getInstance():|
       |   WorkflowEng  |
       | + startWorkflow(|
       |   Workflow w)  |
       +----------------+
              │
              │ composition
              ▼
       +----------------+
       | Workflow       |
       |----------------|
       | - steps: List< |
       |   Step>        |
       |----------------|
       | + addStep(s:   |
       |   Step)        |
       | + getSteps():  |
       |   List<Step>   |
       +----------------+
              │
              │ composition
              ▼
       +----------------+
       | <<Abstract>>   |
       | Step           |
       |----------------|
       | - id: String   |
       | - name: String |
       |----------------|
       | + execute(inst:|
       |   WorkflowInst)|
       |   [sync]       |
       +----------------+
              │
              │ association
              ▼
       +----------------+
       | Condition      |
       |----------------|
       | + evaluate(inst:|
       |   WorkflowInst)|
       |   : boolean    |
       +----------------+
              │
              │ realization
              ▼
       +----------------+
       | Executor       |
       |----------------|
       | + executeStep( |
       |   Step,        |
       |   WorkflowInst)|
       |   [sync]       |
       +----------------+

This diagram highlights key Java-specific design decisions in the Workflow Engine. WorkflowEngine is a Singleton with a volatile instance field to ensure safe lazy initialization and visibility across threads. Workflow composes Steps, enforcing strong lifecycle ownership via composition. Step is an abstract class holding shared state and a synchronized execute method, chosen over interface because interfaces cannot hold state in Java. Condition is an interface realized by concrete classes, applying the Strategy pattern to enable extensibility without modification, adhering to the Open-Closed Principle. Executor interface defines a synchronized executeStep method to ensure thread-safe execution of steps, preventing race conditions. These decisions demonstrate precise use of Java concurrency, lifecycle management, and OOP principles.

Primary Focus

✓ Abstract class vs Interface + Composition lifecycle + Strategy pattern realization + Singleton volatile

Practice

(1/5)
1. In an in-memory SQL database design, a Table class has a private constructor and a static synchronized getInstance() method returning the single instance. Which pattern is applied here and why is this the correct use case?
easy
A. Prototype pattern, because it clones an existing Table instance for reuse.
B. Factory Method pattern, because it controls object creation with a static method.
C. Builder pattern, because it incrementally constructs the Table instance.
D. Singleton pattern, because it ensures only one instance of Table exists in memory.

Solution

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

    A private constructor prevents external instantiation, and a static synchronized getInstance() method ensures controlled access to a single instance.
  2. Step 2: Match pattern intent to scenario

    The Singleton pattern guarantees a single instance per JVM, suitable for a Table class representing a unique in-memory table. Factory Method and Builder focus on object creation but don't enforce single instance. Prototype clones objects, which is unrelated here.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Private constructor + static synchronized getInstance() -> Singleton [check]
Hint: Private constructor + static getInstance() -> Singleton [check]
Common Mistakes:
2. In the Ride Booking system, the parkVehicle() method checks if a parking spot is available and then marks it occupied. Without synchronization, which exact thread interleaving causes double-booking of the same spot?
medium
A. Thread 1 checks isAvailable() -> true, context switch before marking occupied; Thread 2 checks isAvailable() -> true, both mark occupied.
B. Thread 1 reads spot as occupied, Thread 2 reads spot as free, both park simultaneously.
C. Thread 1 marks spot occupied, Thread 2 waits and then parks after spot is freed.
D. Thread 1 and Thread 2 both wait on a lock, preventing double-booking.

Solution

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

    Without synchronization, Thread 1 reads spot as free (isAvailable() -> true) but is preempted before marking it occupied.
  2. Step 2: Thread 2 also reads spot as free and proceeds to mark it occupied, causing double-booking.

    This is the classic check-then-act gap causing race conditions.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Unsynchronized check-then-act -> double-booking race [check]
Hint: Check-then-act without sync -> double-booking race [check]
Common Mistakes:
3. In the Amazon Locker Service, if the assignLocker() method is not synchronized, which exact sequence of thread interleaving causes two packages to be assigned the same locker?
medium
A. Thread 1 assigns locker and updates status; Thread 2 waits until Thread 1 finishes, then assigns a different locker.
B. Thread 1 and Thread 2 both call assignLocker() simultaneously and both fail to find any available lockers.
C. Thread 1 checks locker availability -> true, context switch; Thread 2 checks availability -> true; both assign the same locker before status update.
D. Thread 1 releases a locker; Thread 2 immediately assigns it without checking availability.

Solution

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

    Without synchronization, Thread 1 reads locker as available, then context switches before marking it occupied.
  2. Step 2: Thread 2 also reads locker as available and proceeds to assign it, causing double-booking.

    Both threads assign the same locker before either updates the status.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Check-then-act gap -> double assignment [check]
Hint: Check availability -> context switch -> both assign same locker [check]
Common Mistakes:
4. You need to add ElectricCar and ChargingSpot to the existing Abstract Factory design without modifying existing classes. What is the minimum change to support this extensibility in Java?
hard
A. Add ELECTRIC_CAR and CHARGING to existing enums, create ElectricCar and ChargingSpot classes, and update compatibility map; no existing class modified.
B. Modify Vehicle and Spot base classes to include electric-specific fields and methods.
C. Create a new factory subclass for electric vehicles and modify the client code to use it.
D. Add electric-specific logic inside existing Vehicle and Spot classes using instanceof checks.

Solution

  1. Step 1: Identify extensibility requirements

    New vehicle and spot types must be added without modifying existing classes to respect OCP.
  2. Step 2: Apply Java-specific extensibility

    Adding new enum values and creating new classes for ElectricCar and ChargingSpot allows extension without modifying existing classes. Updating compatibility map centralizes compatibility logic.
  3. Step 3: Eliminate violating options

    Options B and D modify existing classes, violating OCP. Create a new factory subclass for electric vehicles and modify the client code to use it. requires client code changes but not existing classes.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    New enums and classes added, existing classes untouched [check]
Hint: Add enums and classes, update map; no existing class changed [check]
Common Mistakes:
5. To add ElectricCar needing ChargingSpot support in the current parking lot design, what is the minimum change that respects extensibility and avoids modifying existing classes?
hard
A. Add ELECTRIC_CAR to VehicleType enum, CHARGING to SpotType, create ElectricCar subclass, and update SpotCompatibility.COMPATIBILITY_MAP accordingly.
B. Modify Vehicle class to include a boolean isElectric and add charging logic in parkVehicle().
C. Create a new ElectricVehicleManager class that handles electric cars separately from existing parking logic.
D. Add charging spot logic inside existing ParkingLot class with conditional checks for electric cars.

Solution

  1. Step 1: Identify extensibility requirements

    Adding ElectricCar and ChargingSpot should not modify existing classes to respect Open-Closed Principle and avoid class explosion.
  2. Step 2: Evaluate minimal change approach

    Extending enums and compatibility map, plus subclassing ElectricCar, allows new types without changing existing classes, preserving extensibility.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Extending enums + subclass + compatibility map update -> minimal change [check]
Hint: Extend enums + subclass + compatibility map -> minimal invasive change [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