A workflow orchestration engine manages complex business processes by coordinating multiple tasks and services while maintaining reliability, scalability, and observability across distributed systems.
1. Problem Statement & Requirements
Clarifying questions
-
What scale and throughput expectations are we targeting?
- How many workflows can be running concurrently?
- What's the expected peak rate of workflow creation and execution?
-
What consistency requirements exist for workflows?
- Are there strict ordering or atomicity guarantees across steps?
- How critical is it to maintain exactly-once execution of workflow steps?
-
What are the key entities and their relationships in this system?
- Are workflows composed of steps, tasks, or activities?
- How do these entities relate to each other (e.g., one-to-many, many-to-many)?
-
What's the read/write ratio for workflows and their metadata?
- Is the system primarily used for creating and managing workflows, or for querying their status?
-
What are the latency sensitivity requirements?
- What's the acceptable end-to-end latency for workflow execution?
- Are there SLA constraints for individual steps?
-
What's the authentication and authorization model?
- Will users be authenticated via OAuth2, JWT tokens, or API keys?
- Is there a need for role-based access control (RBAC) to restrict workflow creation or execution?
-
Are there any external integrations or dependencies?
- Do workflows need to integrate with third-party services (e.g., payment gateways, email providers)?
- Is there a need to support asynchronous callbacks or webhooks?
Functional requirements
- Workflow Definition Management: Users should be able to define, store, and version workflows using a DSL or JSON/YAML format.
- Workflow Execution: The system must support starting, pausing, resuming, and cancelling workflows.
- Step-level Execution: Individual steps within a workflow must be executed in order or in parallel, with support for conditional logic.
- State Management: Each workflow and its steps must maintain a clear state (e.g., pending, running, completed, failed).
- Error Handling: The system must handle failures gracefully, including retries, fallbacks, and error notifications.
- Monitoring & Observability: Users should be able to query workflow status, step execution logs, and performance metrics.
- Persistence: Workflow definitions and state must be persisted reliably across system restarts.
Non-functional requirements
- Latency Targets:
- p50: < 100ms for workflow creation and state updates
- p95: < 500ms for workflow creation and state updates
- p99: < 2s for workflow creation and state updates
- Throughput:
- 10,000 concurrent workflows with 500 steps each
- 5000 workflow starts per second during peak load
- Availability:
- 99.9% uptime (5 minutes of downtime per month)
- Support for graceful degradation during partial outages
Constraints and assumptions
-
Assumptions:
- Workflows are defined in a structured format (e.g., JSON or YAML) with support for step dependencies and conditional logic.
- The system will support workflows up to 100 steps in length.
- Workflows are stateless and idempotent where possible.
- The system will not support long-running workflows (>24 hours) without explicit configuration.
-
Constraints:
- The system must not block on external service calls (e.g., database or third-party APIs) for more than 5 seconds.
- Workflow steps must not exceed 10 minutes of execution time without timeout handling.
Out of scope
- External system integrations: Integration with third-party services (e.g., payment systems, CRM) is out of scope.
- UI/Console: A web UI or dashboard for managing workflows is not part of this design.
- Advanced scheduling: Cron-based or time-triggered workflow scheduling is not included.
- Cross-cluster orchestration: Multi-cluster or multi-region workflow execution is out of scope.
- Workflow versioning and rollback: Version control for workflows and rollback to previous versions is not included.
2. API Design
API Style Selection
gRPC with Protocol Buffers is selected for the workflow orchestration engine due to:
- Strong typing and automatic code generation
- Efficient binary serialization for high-throughput workflows
- Native support for streaming and bidirectional communication
- First-class support for versioning through proto package management
- Better performance characteristics over REST for complex workflow operations
Endpoint Definitions
| Method | Path | Description | Auth |
|---|---|---|---|
| CreateWorkflow | /workflow.v1.WorkflowService/CreateWorkflow | Creates a new workflow definition | JWT |
| GetWorkflow | /workflow.v1.WorkflowService/GetWorkflow | Retrieves workflow by ID | JWT |
| UpdateWorkflow | /workflow.v1.WorkflowService/UpdateWorkflow | Updates existing workflow definition | JWT |
| DeleteWorkflow | /workflow.v1.WorkflowService/DeleteWorkflow | Deletes workflow definition | JWT |
| StartWorkflow | /workflow.v1.WorkflowService/StartWorkflow | Triggers execution of a workflow | JWT |
| GetWorkflowInstance | /workflow.v1.WorkflowService/GetWorkflowInstance | Gets instance status and state | JWT |
| CancelWorkflow | /workflow.v1.WorkflowService/CancelWorkflow | Cancels running workflow instance | JWT |
| ListWorkflows | /workflow.v1.WorkflowService/ListWorkflows | Lists all workflows with pagination | JWT |
| ListWorkflowInstances | /workflow.v1.WorkflowService/ListWorkflowInstances | Lists workflow instances with filters | JWT |
Request and Response Schemas
CreateWorkflowRequest
{
"name": "string",
"description": "string",
"version": "string",
"definition": {
"tasks": [
{
"id": "string",
"name": "string",
"type": "TASK_TYPE",
"parameters": {
"key": "value"
},
"dependencies": ["string"]
}
],
"start_task_id": "string"
},
"timeout_seconds": 3600,
"retry_policy": {
"max_attempts": 3,
"backoff_multiplier": 2.0
}
}
CreateWorkflowResponse
{
"workflow_id": "uuid",
"version": "string"
}
StartWorkflowRequest
{
"workflow_id": "uuid",
"version": "string",
"input_parameters": {
"key": "value"
},
"correlation_id": "string"
}
StartWorkflowResponse
{
"instance_id": "uuid",
"workflow_id": "uuid"
}
HTTP Status Codes and Error Response Format
Standard HTTP status codes:
- 200 OK for successful operations
- 201 Created for creation operations
- 400 Bad Request for validation errors
- 401 Unauthorized for authentication failures
- 403 Forbidden for authorization failures
- 404 Not Found for missing resources
- 429 Too Many Requests for rate limiting
- 500 Internal Server Error for server errors
Standardized error response format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"details": [
{
"field": "name",
"message": "Name is required"
}
]
}
}
API Versioning Strategy
Versioned using gRPC package names: workflow.v1, workflow.v2
Each version maintains backward compatibility for existing clients while allowing new features in newer versions.
Pagination Design
Cursor-based pagination is used because:
- More efficient for large datasets than offset-based pagination
- Consistent results even when data changes during pagination
- Better performance for deep pagination scenarios
Authentication and Authorization
All endpoints require JWT tokens with scopes:
workflow:read- Read access to workflows and instancesworkflow:write- Create/update/delete workflows and start instancesworkflow:admin- Full administrative access
Rate Limiting
Rate limiting per endpoint:
- Create/Update/Delete: 100 requests/minute per user
- Start Workflow: 500 requests/minute per user
- Read operations: 1000 requests/minute per user
Enforced using Redis-based token bucket algorithm with distributed locks.
Idempotency Design
For mutation endpoints, idempotency key header X-Idempotency-Key is supported:
// Example usage in gRPC service
public StartWorkflowResponse startWorkflow(StartWorkflowRequest request, String idempotencyKey) {
if (idempotencyKey != null) {
// Check if this operation was already processed
String existingInstanceId = idempotencyCache.get(idempotencyKey);
if (existingInstanceId != null) {
return StartWorkflowResponse.newBuilder()
.setInstanceId(existingInstanceId)
.build();
}
}
// Process workflow start
String instanceId = processWorkflowStart(request);
if (idempotencyKey != null) {
idempotencyCache.put(idempotencyKey, instanceId);
}
return StartWorkflowResponse.newBuilder()
.setInstanceId(instanceId)
.build();
}
3. Data Model Design
Entity-Relationship Diagram
erDiagram
WORKFLOW {
string id PK
string name
string description
jsonb definition
string status
timestamp created_at
timestamp updated_at
string created_by
}
WORKFLOW_INSTANCE {
string id PK
string workflow_id FK
string status
jsonb input_data
jsonb output_data
timestamp created_at
timestamp updated_at
timestamp started_at
timestamp completed_at
}
WORKFLOW_TASK {
string id PK
string workflow_instance_id FK
string task_type
string status
jsonb input_data
jsonb output_data
timestamp created_at
timestamp updated_at
timestamp started_at
timestamp completed_at
integer retry_count
string error_message
}
WORKFLOW_TASK_DEPENDENCY {
string id PK
string from_task_id FK
string to_task_id FK
string dependency_type
}
WORKFLOW_EVENT {
string id PK
string workflow_instance_id FK
string event_type
jsonb payload
timestamp created_at
}
WORKFLOW {
"string id" --> "WORKFLOW_INSTANCE.workflow_id"
"string id" --> "WORKFLOW_TASK.workflow_instance_id"
}
WORKFLOW_INSTANCE {
"string id" --> "WORKFLOW_TASK.workflow_instance_id"
"string id" --> "WORKFLOW_EVENT.workflow_instance_id"
}
WORKFLOW_TASK {
"string id" --> "WORKFLOW_TASK_DEPENDENCY.from_task_id"
"string id" --> "WORKFLOW_TASK_DEPENDENCY.to_task_id"
}
Table Schema Definitions
-- Workflows table
CREATE TABLE workflows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
definition JSONB NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(255) NOT NULL,
INDEX idx_workflows_status (status),
INDEX idx_workflows_created_by (created_by),
INDEX idx_workflows_created_at (created_at)
);
-- Workflow instances table
CREATE TABLE workflow_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
input_data JSONB,
output_data JSONB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
INDEX idx_workflow_instances_workflow_id (workflow_id),
INDEX idx_workflow_instances_status (status),
INDEX idx_workflow_instances_created_at (created_at),
INDEX idx_workflow_instances_started_at (started_at)
);
-- Workflow tasks table
CREATE TABLE workflow_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_instance_id UUID NOT NULL REFERENCES workflow_instances(id) ON DELETE CASCADE,
task_type VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
input_data JSONB,
output_data JSONB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
retry_count INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
INDEX idx_workflow_tasks_workflow_instance_id (workflow_instance_id),
INDEX idx_workflow_tasks_status (status),
INDEX idx_workflow_tasks_task_type (task_type),
INDEX idx_workflow_tasks_created_at (created_at),
INDEX idx_workflow_tasks_started_at (started_at)
);
-- Task dependencies table
CREATE TABLE workflow_task_dependencies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_task_id UUID NOT NULL REFERENCES workflow_tasks(id) ON DELETE CASCADE,
to_task_id UUID NOT NULL REFERENCES workflow_tasks(id) ON DELETE CASCADE,
dependency_type VARCHAR(50) NOT NULL DEFAULT 'SEQUENTIAL',
INDEX idx_workflow_task_dependencies_from_task_id (from_task_id),
INDEX idx_workflow_task_dependencies_to_task_id (to_task_id),
CONSTRAINT chk_no_self_dependency CHECK (from_task_id != to_task_id)
);
-- Workflow events table
CREATE TABLE workflow_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_instance_id UUID NOT NULL REFERENCES workflow_instances(id) ON DELETE CASCADE,
event_type VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
INDEX idx_workflow_events_workflow_instance_id (workflow_instance_id),
INDEX idx_workflow_events_event_type (event_type),
INDEX idx_workflow_events_created_at (created_at)
);
Primary Key Strategy
UUIDs for all primary keys - Selected over auto-increment or ULID due to:
- Distributed system safety: UUIDs eliminate potential conflicts in distributed deployments
- No sequential predictability: Prevents enumeration attacks and makes system less guessable
- Cross-service consistency: UUIDs can be generated client-side without coordination
- Sharding-friendly: UUIDs distribute well across sharded databases
Index Design
| Index Name | Fields | Type | Purpose |
|---|---|---|---|
| idx_workflows_status | status | Standard | Fast filtering by workflow status |
| idx_workflows_created_by | created_by | Standard | Efficient user-based workflow queries |
| idx_workflows_created_at | created_at | Standard | Time-range filtering for audit |
| idx_workflow_instances_workflow_id | workflow_id | Standard | Fast lookup by workflow definition |
| idx_workflow_instances_status | status | Standard | Efficient status-based filtering |
| idx_workflow_instances_created_at | created_at | Standard | Audit and time-range queries |
| idx_workflow_instances_started_at | started_at | Standard | Efficient query by execution start |
| idx_workflow_tasks_workflow_instance_id | workflow_instance_id | Standard | Fast task lookup by instance |
| idx_workflow_tasks_status | status | Standard | Efficient status-based filtering |
| idx_workflow_tasks_task_type | task_type | Standard | Fast filtering by task type |
| idx_workflow_tasks_created_at | created_at | Standard | Audit and time-range queries |
| idx_workflow_tasks_started_at | started_at | Standard | Efficient query by execution start |
| idx_workflow_task_dependencies_from_task_id | from_task_id | Standard | Fast dependency resolution |
| idx_workflow_task_dependencies_to_task_id | to_task_id | Standard | Fast reverse dependency lookups |
| idx_workflow_events_workflow_instance_id | workflow_instance_id | Standard | Fast event lookup by instance |
| idx_workflow_events_event_type | event_type | Standard | Efficient event type filtering |
| idx_workflow_events_created_at | created_at | Standard | Audit and time-range queries |
Data Partitioning / Sharding Strategy
Time-based sharding on workflow_instances.created_at:
- Partition by month to support time-range queries and data lifecycle management
- Each partition contains instances created in that calendar month
- Allows for efficient historical data queries and archiving
Soft Delete vs. Hard Delete Decision
Soft delete with status field - Chosen for:
- Auditability: Historical tracking of workflow instances and tasks
- Recovery capability: Ability to restore workflows if accidentally deleted
- Compliance: Required for regulatory compliance in many industries
- Data integrity: Prevents cascading deletes that could break dependencies
Audit Trail Design
All entities have created_at, updated_at timestamps and created_by fields to track:
- Who created or modified the entity
- What changed (via updated_at timestamp)
- When changes occurred (timestamp fields)
- Audit log is maintained in the database through timestamp and user fields
Data Archival Strategy and Retention Policy
30-day retention policy with archival to S3:
- Active data: Last 30 days of workflow instances and tasks remain in primary database
- Archival process: Daily batch job moves completed workflow instances older than 30 days to S3
- Metadata retention: Workflow definitions and task metadata retained in DB for query purposes
- Query strategy: Recent data served from DB, older data from S3 with metadata lookups in DB
- Recovery: Archived data can be re-ingested if needed for compliance or analysis
// Example Java entity class for workflow instance with audit fields
public class WorkflowInstance {
private String id;
private String workflowId;
private String status;
private Map<String, Object> inputData;
private Map<String, Object> outputData;
private Instant createdAt;
private Instant updatedAt;
private Instant startedAt;
private Instant completedAt;
private String createdBy;
// Getters and setters
}
4. Class Design & Service Layer
UML Class Diagram
classDiagram
class WorkflowEngine {
-workflowRepository
-eventPublisher
-scheduler
+startWorkflow(WorkflowDefinition)
+pauseWorkflow(String)
+resumeWorkflow(String)
+cancelWorkflow(String)
+getWorkflowStatus(String)
}
class WorkflowDefinition {
-id
-name
-description
-tasks
-startTaskId
-status
+validate()
}
class Task {
-id
-name
-type
-status
-dependencies
-timeoutSeconds
-retryPolicy
+execute()
+validate()
}
class TaskExecution {
-id
-taskId
-workflowId
-status
-startTime
-endTime
-result
-error
+updateStatus(TaskStatus)
+setResult(Object)
}
class WorkflowInstance {
-id
-definitionId
-status
-currentTaskId
-createdAt
-updatedAt
+getStatus()
+setCurrentTask(String)
+updateStatus(WorkflowStatus)
}
class RetryPolicy {
-maxRetries
-backoffSeconds
-retryableErrors
}
class EventPublisher {
+publishEvent(WorkflowEvent)
}
class TaskScheduler {
+scheduleTask(String, long)
+cancelScheduledTask(String)
}
class WorkflowService {
-workflowEngine
-taskExecutionService
+createWorkflow(WorkflowDefinition)
+executeNextTask(String)
+handleTaskCompletion(String, TaskResult)
}
class TaskExecutionService {
-taskRepository
-workflowRepository
+executeTask(TaskExecution)
+updateTaskResult(String, TaskResult)
}
WorkflowEngine --> WorkflowService : delegates to
WorkflowEngine --> TaskScheduler : schedules tasks
WorkflowEngine --> EventPublisher : publishes events
WorkflowService --> WorkflowInstance : manages instances
WorkflowService --> TaskExecutionService : orchestrates task execution
TaskExecutionService --> Task : executes tasks
TaskExecutionService --> WorkflowInstance : updates workflow state
WorkflowDefinition *-- Task : contains tasks
WorkflowInstance --> WorkflowDefinition : references definition
TaskExecution --> Task : executes task definition
TaskExecution --> WorkflowInstance : belongs to workflow
class WorkflowEvent {
-type
-workflowId
-taskId
-timestamp
-payload
}
class TaskResult {
-success
-data
-error
-executionTimeMs
}
class WorkflowStatus {
PENDING
RUNNING
PAUSED
COMPLETED
FAILED
CANCELLED
}
class TaskStatus {
PENDING
RUNNING
COMPLETED
FAILED
RETRYING
}
State Machine Diagram for WorkflowInstance
stateDiagram-v2
[*] --> PENDING
PENDING --> RUNNING : start()
RUNNING --> PAUSED : pause()
PAUSED --> RUNNING : resume()
RUNNING --> COMPLETED : all_tasks_completed()
RUNNING --> FAILED : task_failed()
PAUSED --> CANCELLED : cancel()
RUNNING --> CANCELLED : cancel()
FAILED --> CANCELLED : cancel()
Service Class Structure and Responsibilities
The service layer is organized into three main components:
- WorkflowEngine: The core orchestration engine that manages workflow lifecycle and task scheduling
- WorkflowService: Business logic layer for workflow operations and task execution coordination
- TaskExecutionService: Low-level task execution and result handling
Service Interface Contracts
public interface WorkflowEngine {
WorkflowInstance startWorkflow(WorkflowDefinition definition);
void pauseWorkflow(String workflowId);
void resumeWorkflow(String workflowId);
void cancelWorkflow(String workflowId);
WorkflowStatus getWorkflowStatus(String workflowId);
}
public interface WorkflowService {
WorkflowInstance createWorkflow(WorkflowDefinition definition);
void executeNextTask(String workflowId);
void handleTaskCompletion(String taskId, TaskResult result);
}
public interface TaskExecutionService {
TaskResult executeTask(TaskExecution taskExecution);
void updateTaskResult(String taskId, TaskResult result);
}
Business Logic Encapsulation Patterns
Strategy Pattern for Task Execution
public interface TaskExecutor {
TaskResult execute(TaskExecution task);
}
public class HttpTaskExecutor implements TaskExecutor {
private final HttpClient httpClient;
@Override
public TaskResult execute(TaskExecution task) {
try {
// Execute HTTP request based on task configuration
HttpResponse response = httpClient.execute(task.getConfiguration());
return TaskResult.success(
response.getBody(),
response.getStatusCode(),
System.currentTimeMillis() - task.getStartTime()
);
} catch (Exception e) {
return TaskResult.failure(e.getMessage(), e);
}
}
}
public class DatabaseTaskExecutor implements TaskExecutor {
private final DataSource dataSource;
@Override
public TaskResult execute(TaskExecution task) {
try (Connection conn = dataSource.getConnection()) {
// Execute database operation
PreparedStatement stmt = conn.prepareStatement(task.getConfiguration());
ResultSet rs = stmt.executeQuery();
return TaskResult.success(
processResultSet(rs),
"SUCCESS",
System.currentTimeMillis() - task.getStartTime()
);
} catch (SQLException e) {
return TaskResult.failure(e.getMessage(), e);
}
}
}
public class TaskExecutorFactory {
private final Map<String, TaskExecutor> executors = new HashMap<>();
public TaskExecutorFactory() {
executors.put("HTTP", new HttpTaskExecutor());
executors.put("DATABASE", new DatabaseTaskExecutor());
// Add more executor types as needed
}
public TaskExecutor getExecutor(String taskType) {
return executors.getOrDefault(taskType, new DefaultTaskExecutor());
}
}
Critical-Path Method Implementation
public class WorkflowEngineImpl implements WorkflowEngine {
private final WorkflowRepository workflowRepository;
private final TaskExecutionService taskExecutionService;
private final EventPublisher eventPublisher;
private final TaskScheduler scheduler;
@Override
@Transactional
public WorkflowInstance startWorkflow(WorkflowDefinition definition) {
// Validate workflow definition
if (!definition.validate()) {
throw new IllegalArgumentException("Invalid workflow definition");
}
// Create workflow instance
WorkflowInstance workflow = new WorkflowInstance();
workflow.setId(UUID.randomUUID().toString());
workflow.setDefinitionId(definition.getId());
workflow.setStatus(WorkflowStatus.PENDING);
workflow.setCreatedAt(System.currentTimeMillis());
// Save workflow instance
workflowRepository.save(workflow);
// Publish workflow started event
eventPublisher.publishEvent(new WorkflowEvent(
"WORKFLOW_STARTED",
workflow.getId(),
null,
System.currentTimeMillis(),
Map.of("definitionId", definition.getId())
));
// Schedule first task
Task startTask = definition.getTasks().stream()
.filter(t -> t.getId().equals(definition.getStartTaskId()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Start task not found"));
scheduleTask(workflow.getId(), startTask);
return workflow;
}
private void scheduleTask(String workflowId, Task task) {
// Create task execution record
TaskExecution taskExecution = new TaskExecution();
taskExecution.setId(UUID.randomUUID().toString());
taskExecution.setTaskId(task.getId());
taskExecution.setWorkflowId(workflowId);
taskExecution.setStatus(TaskStatus.PENDING);
taskExecution.setStartTime(System.currentTimeMillis());
// Save task execution
taskExecutionService.save(taskExecution);
// Schedule for execution (with potential delay)
long delay = task.getDelaySeconds() != null ? task.getDelaySeconds() * 1000 : 0;
scheduler.scheduleTask(taskExecution.getId(), delay);
// Publish task scheduled event
eventPublisher.publishEvent(new WorkflowEvent(
"TASK_SCHEDULED",
workflowId,
task.getId(),
System.currentTimeMillis(),
Map.of("taskId", task.getId(), "delayMs", delay)
));
}
@Override
public void pauseWorkflow(String workflowId) {
WorkflowInstance workflow = workflowRepository.findById(workflowId)
.orElseThrow(() -> new ResourceNotFoundException("Workflow not found"));
if (workflow.getStatus() != WorkflowStatus.RUNNING) {
throw new IllegalStateException("Cannot pause workflow in " + workflow.getStatus());
}
workflow.setStatus(WorkflowStatus.PAUSED);
workflow.setUpdatedAt(System.currentTimeMillis());
workflowRepository.save(workflow);
eventPublisher.publishEvent(new WorkflowEvent(
"WORKFLOW_PAUSED",
workflowId,
null,
System.currentTimeMillis(),
Map.of("pausedAt", System.currentTimeMillis())
));
}
@Override
public void resumeWorkflow(String workflowId) {
WorkflowInstance workflow = workflowRepository.findById(workflowId)
.orElseThrow(() -> new ResourceNotFoundException("Workflow not found"));
if (workflow.getStatus() != WorkflowStatus.PAUSED) {
throw new IllegalStateException("Cannot resume workflow in ":
workflow.getStatus());
}
workflow.setStatus(WorkflowStatus.RUNNING);
workflow.setUpdatedAt(System.currentTimeMillis());
workflowRepository.save(workflow);
// Reschedule pending tasks
List<TaskExecution> pendingTasks = taskExecutionService.findPendingByWorkflow(workflowId);
for (TaskExecution task : pendingTasks) {
scheduler.scheduleTask(task.getId(), 0);
}
eventPublisher.publishEvent(new WorkflowEvent(
"WORKFLOW_RESUMED",
workflowId,
null,
System.currentTimeMillis(),
Map.of("resumedAt", System.currentTimeMillis())
));
}
@Override
public void cancelWorkflow(String workflowId) {
WorkflowInstance workflow = workflowRepository.findById(workflowId)
.orElseThrow(() -> new ResourceNotFoundException("Workflow not found"));
workflow.setStatus(WorkflowStatus.CANCELLED);
workflow.setUpdatedAt(System.currentTimeMillis());
workflowRepository.save(workflow);
// Cancel any pending tasks
taskExecutionService.cancelPendingByWorkflow(workflowId);
eventPublisher.publishEvent(new WorkflowEvent(
"WORKFLOW_CANCELLED",
workflowId,
null,
System.currentTimeMillis(),
Map.of("cancelledAt", System.currentTimeMillis())
));
}
@Override
public WorkflowStatus getWorkflowStatus(String workflowId) {
return workflowRepository.findById(workflowId)
.map(WorkflowInstance::getStatus)
.orElseThrow(() -> new ResourceNotFoundException("Workflow not found"));
}
}
Transaction Boundary Design
Transaction boundaries are defined at:
- Workflow Creation: Complete workflow instance creation and initial task scheduling
- Task Execution: Single task execution with result persistence
- Workflow State Updates: Status transitions and workflow completion
- Event Publishing: Event persistence with transactional consistency
Each operation that modifies state or publishes events is wrapped in a transaction to ensure atomicity.
Saga / Orchestration Pattern for Distributed Transactions
For distributed workflows, we implement a saga pattern where:
- Each task execution is a local transaction
- Workflow engine coordinates compensation logic for failed steps
- Events are published to trigger downstream systems
public class WorkflowSaga {
private final List<SagaStep> steps;
private final String workflowId;
public void execute() {
List<SagaStep> executedSteps = new ArrayList<>();
try {
for (SagaStep step : steps) {
step.execute();
executedSteps.add(step);
}
} catch (Exception e) {
// Compensate for executed steps
compensate(executedSteps);
throw new WorkflowExecutionException("Workflow failed", e);
}
}
private void compensate(List<SagaStep> steps) {
// Execute compensation logic in reverse order
for (int i = steps.size() - 1; i >= 0; i--) {
steps.get(i).compensate();
}
}
}
Domain Object / Value Object Design
public class WorkflowDefinition {
private String id;
private String name;
private String description;
private List<Task> tasks;
private String startTaskId;
private WorkflowStatus status;
// Validation logic
public boolean validate() {
return name != null && !name.isEmpty() &&
tasks != null && !tasks.isEmpty() &&
startTaskId != null;
}
}
public class Task {
private String id;
private String name;
private String type; // HTTP, DATABASE, etc.
private TaskStatus status;
private List<String> dependencies;
private Integer timeoutSeconds;
private RetryPolicy retryPolicy;
private Map<String, Object> configuration;
public boolean validate() {
return id != null && name != null && type != null;
}
}
public class TaskResult {
private boolean success;
private Object data;
private String error;
private long executionTimeMs;
public static TaskResult success(Object data, String status, long executionTimeMs) {
return new TaskResult(true, data, null, executionTimeMs);
}
public static TaskResult failure(String error, Exception exception) {
return new TaskResult(false, null, error, 0);
}
}
public enum WorkflowStatus {
PENDING, RUNNING, PAUSED, COMPLETED, FAILED, CANCELLED
}
public enum TaskStatus {
PENDING, RUNNING, COMPLETED, FAILED, RETRYING
}
5. Concurrency & Thread Safety
Concurrency Model
The workflow orchestration engine will use a reactive, non-blocking concurrency model with Java's CompletableFuture and Reactive Streams for handling asynchronous operations. This approach provides better resource utilization and scalability compared to traditional thread pools, especially under high load conditions where workflows may involve multiple external service calls or long-running operations.
The system will leverage:
- Project Reactor (Reactive Streams implementation) for managing asynchronous streams
- Thread-safe immutable domain objects to prevent race conditions in state transitions
- Single-threaded event loop pattern for workflow execution engine to ensure deterministic behavior
Race Condition Prevention Strategy
Race conditions in the orchestration engine are mitigated through:
- Immutable state objects - Workflow execution context and task metadata are immutable once created
- Atomic operations with CAS (Compare-And-Swap) for updating workflow state counters and status flags
- Database-level optimistic locking using version fields to detect concurrent modifications
- Synchronized access to shared mutable state via fine-grained locks or lock-free data structures (e.g.,
ConcurrentHashMap,AtomicReference)
Optimistic vs. Pessimistic Locking Selection
Optimistic locking is selected for workflow state management because:
- Workflows are typically read-heavy with infrequent writes
- Concurrent access is expected to be low to moderate (most workflows execute sequentially)
- The system uses versioned entities and retry logic for handling conflicts
- Pessimistic locking would introduce unnecessary blocking overhead and reduce throughput
Implementation uses database version fields in the workflow_instance table with retry logic on optimistic lock failures.
Deadlock Prevention Strategy
Deadlock prevention is handled through:
- Fixed ordering of resource acquisition - All workflow state updates follow a consistent order (e.g., always acquire workflow lock before task locks)
- Timeout-based lock acquisition - Locks are acquired with a 5-second timeout to prevent indefinite waits
- Hierarchical lock management - Workflow-level locks are acquired before task-level locks to avoid circular dependencies
- Idempotency checks - Duplicate workflow execution attempts are detected and handled gracefully
Idempotency Implementation for Concurrent Duplicate Requests
Idempotency is implemented using:
- Idempotency keys in API requests - Clients provide unique identifiers for each workflow execution request
- Database-level uniqueness constraints on the
workflow_execution_idfield inworkflow_instancetable - Cache-based idempotency tracking - Recent workflow execution IDs are cached in Redis with TTL of 24 hours to handle duplicate requests within a window
- Atomic operation for idempotency check - The system first checks if a workflow with the same ID already exists before creating new instance
public class WorkflowOrchestrator {
private final IdempotencyService idempotencyService;
private final WorkflowRepository workflowRepository;
public CompletableFuture<WorkflowInstance> executeWorkflowAsync(
String idempotencyKey,
WorkflowDefinition definition) {
return CompletableFuture.supplyAsync(() -> {
// Check if this execution is already in progress or completed
Optional<WorkflowInstance> existing = idempotencyService.checkIdempotency(idempotencyKey);
if (existing.isPresent()) {
return existing.get();
}
// Create new workflow instance
WorkflowInstance workflow = createWorkflow(definition);
// Store in database with idempotency key tracking
workflowRepository.save(workflow);
idempotencyService.markIdempotent(idempotencyKey, workflow.getId());
return workflow;
});
}
}
Thread Safety in Core Components
- Workflow Engine Thread Safety - The core workflow execution engine uses immutable state objects and thread-safe collections for managing concurrent workflow instances
- Task Queue Thread Safety - Task scheduling and execution queues are implemented using
ConcurrentLinkedQueuewith proper synchronization - Event Publisher Thread Safety - Event publishing uses a thread-safe event bus with proper synchronization to prevent concurrent event processing issues
Concurrency Patterns Used
- Producer-Consumer Pattern - For handling workflow task execution with bounded thread pools
- Observer Pattern - For event-driven workflow state updates and notifications
- Command Pattern - For encapsulating workflow execution steps as executable commands that can be safely executed in parallel
Data Consistency Across Concurrent Operations
To ensure data consistency:
- Database transactions are used for atomic workflow state updates
- Read-replica separation - Read operations use read replicas while write operations go to primary
- Eventual consistency model - For distributed state updates, system uses eventual consistency with proper conflict resolution
- Saga pattern - For distributed workflow operations that span multiple services, ensuring compensating actions can be applied safely in concurrent environments
Performance Considerations
The concurrency model is designed to:
- Minimize context switching overhead
- Maximize throughput under concurrent load
- Reduce memory footprint through immutable objects
- Provide predictable latency characteristics for workflow execution
This approach ensures that the orchestration engine can handle hundreds of concurrent workflows efficiently while maintaining data integrity and preventing race conditions.
6. Sequence Diagrams for Key Flows
Happy Path Sequence Diagram
sequenceDiagram
participant U as User
participant API as Workflow Engine API
participant WF as Workflow Service
participant DB as Database
participant MQ as Message Queue
participant Task as Task Executor
U->>API: Create Workflow (JSON)
API->>WF: Validate & Store Workflow
WF->>DB: Save Workflow Definition
WF->>MQ: Publish WorkflowCreated Event
WF-->>U: 201 Created
U->>API: Start Workflow Execution
API->>WF: Initiate Execution
WF->>DB: Create Execution Instance
WF->>MQ: Publish WorkflowStarted Event
loop Task Execution
WF->>MQ: Publish TaskScheduled Event
MQ->>Task: Consume Task Event
Task->>Task: Execute Task Logic
Task->>MQ: Publish TaskCompleted Event
MQ->>WF: Consume TaskCompleted Event
WF->>DB: Update Execution State
end
WF->>MQ: Publish WorkflowCompleted Event
MQ->>WF: Consume WorkflowCompleted Event
WF->>DB: Update Final State
Error / Edge Case Sequence Diagram
sequenceDiagram
participant U as User
participant API as Workflow Engine API
participant WF as Workflow Service
participant DB as Database
participant MQ as Message Queue
participant Task as Task Executor
U->>API: Create Workflow (Invalid JSON)
API->>WF: Validate Request
WF-->>U: 400 Bad Request
U->>API: Start Workflow Execution
API->>WF: Initiate Execution
WF->>DB: Create Execution Instance
WF->>MQ: Publish WorkflowStarted Event
MQ->>Task: Consume Task Event
Task->>Task: Simulate Task Failure
Task->>MQ: Publish TaskFailed Event
WF->>DB: Update Execution State to Failed
WF->>MQ: Publish WorkflowFailed Event
MQ->>WF: Consume WorkflowFailed Event
WF->>DB: Finalize Execution State
U->>API: Get Workflow Status
API->>WF: Query Execution Status
WF->>DB: Fetch Execution Data
WF-->>U: 500 Internal Server Error (if DB error)
Async Flow Sequence Diagram
sequenceDiagram
participant U as User
participant API as Workflow Engine API
participant WF as Workflow Service
participant DB as Database
participant MQ as Message Queue
participant Task as Task Executor
U->>API: Submit Async Workflow Request
API->>WF: Process Async Request
WF->>DB: Store Async Workflow Definition
WF->>MQ: Publish AsyncWorkflowSubmitted Event
MQ->>Task: Consume Async Task Events
Task->>Task: Process Async Task Logic
Task->>MQ: Publish AsyncTaskCompleted Event
loop Retry Loop
MQ->>Task: Consume AsyncTaskFailed Event
Task->>Task: Retry Logic with Backoff
Task->>MQ: Publish AsyncTaskCompleted Event
end
MQ->>WF: Consume AsyncWorkflowCompleted Event
WF->>DB: Update Async Execution State
U->>API: Poll for Async Workflow Status
API->>WF: Query Async Execution State
WF->>DB: Fetch Async Execution Data
WF-->>U: 200 OK with Status
U->>API: Cancel Async Workflow
API->>WF: Handle Cancel Request
WF->>MQ: Publish AsyncWorkflowCancelled Event
MQ->>Task: Consume Cancel Event
Task->>Task: Terminate Running Tasks
WF->>DB: Update Cancelled State
7. Caching Design
Cache Strategy Selection
For workflow orchestration engine, I choose cache-aside pattern with write-through behavior for critical workflow state data and cache-aside for less critical metadata.
Justification:
- Workflow execution state is highly consistent and frequently accessed
- Write-through ensures cache consistency for workflow status, current step, and execution context
- Cache-aside allows flexibility in caching metadata like workflow definitions without strict consistency requirements
Cache Key Design
Cache keys follow a structured namespace pattern:
workflow:{workflowId}:{type}:{subKey}
Example key structure:
// For workflow execution state
"workflow:wf-12345:execution:status"
"workflow:wf-12345:execution:currentStep"
// For workflow definition metadata
"workflow:wf-12345:definition:properties"
"workflow:wf-12345:definition:tasks"
TTL Strategy
| Data Type | TTL | Reason |
|---|---|---|
| Workflow Execution State | 30 minutes | Active workflows typically complete within this window |
| Workflow Definition Metadata | 1 hour | Definitions change infrequently, acceptable stale data |
| Task Execution Context | 5 minutes | Short-lived task state for retry logic |
| Workflow Instance Cache | 2 hours | For quick workflow restart/recovery scenarios |
Cache Eviction Policy
Primary policy: LRU (Least Recently Used) with TTL fallback for:
- Workflow execution state (LRU + 30min TTL)
- Task context (LRU + 5min TTL)
Secondary policy: TTL-based eviction for:
- Workflow definitions (1 hour TTL)
- Instance metadata (2 hours TTL)
Cache Stampede Prevention
Implement probabilistic early expiry with jitter:
public class CacheStampedePrevention {
private static final double STAMPED_EXPIRY_JITTER = 0.1; // 10% jitter
public String getWithStampedeProtection(String cacheKey,
Supplier<String> dataLoader) {
String cachedValue = cache.get(cacheKey);
if (cachedValue != null) {
return cachedValue;
}
// Add jitter to expiration time
int ttlWithJitter = (int) (getTTL(cacheKey) *
(1 - STAMPED_EXPIRY_JITTER + Math.random() * 2 * STAMPED_EXPIRY_JITTER));
// Use distributed lock for cache population
if (distributedLock.acquire(cacheKey + ":lock", 5000)) {
try {
// Double-check pattern
String doubleChecked = cache.get(cacheKey);
if (doubleChecked != null) {
return doubleChecked;
}
String data = dataLoader.get();
cache.put(cacheKey, data, ttlWithJitter);
return data;
} finally {
distributedLock.release(cacheKey + ":lock");
}
}
// Fallback to direct cache read with retry
return cache.get(cacheKey);
}
}
Distributed Cache Architecture
Redis Cluster Configuration:
- 3 master nodes with 3 replicas each (6 total nodes)
- Sharding by workflow ID hash to ensure consistent caching
- Redis Sentinel for high availability and automatic failover
Cache Invalidation Strategy
Event-driven invalidation:
@Component
public class WorkflowCacheInvalidator {
@EventListener
public void handleWorkflowEvent(WorkflowEvent event) {
String workflowId = event.getWorkflowId();
switch (event.getType()) {
case WORKFLOW_STARTED:
invalidateWorkflowState(workflowId);
break;
case WORKFLOW_COMPLETED:
invalidateWorkflowState(workflowId);
invalidateWorkflowDefinition(workflowId);
break;
case TASK_EXECUTED:
invalidateTaskContext(workflowId, event.getTaskId());
break;
case WORKFLOW_UPDATED:
invalidateWorkflowDefinition(workflowId);
break;
}
}
private void invalidateWorkflowState(String workflowId) {
cache.evict("workflow:" + workflowId + ":execution:*");
}
private void invalidateWorkflowDefinition(String workflowId) {
cache.evict("workflow:" + workflowId + ":definition:*");
}
}
Cache invalidation hierarchy:
- Immediate invalidation on workflow state transitions
- TTL-based expiration for metadata that doesn't require immediate consistency
- Explicit purging during workflow definition updates
Cache Read-Through Pattern Implementation
@Service
public class WorkflowCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final WorkflowRepository workflowRepository;
public WorkflowState getWorkflowState(String workflowId) {
String cacheKey = "workflow:" + workflowId + ":execution:status";
// Try cache first
WorkflowState cachedState = (WorkflowState) redisTemplate.opsForValue().get(cacheKey);
if (cachedState != null) {
return cachedState;
}
// Cache miss - fetch from database
WorkflowState dbState = workflowRepository.findWorkflowState(workflowId);
if (dbState != null) {
// Cache for 30 minutes
redisTemplate.opsForValue().set(cacheKey, dbState, 30, TimeUnit.MINUTES);
return dbState;
}
return null;
}
}
8. Repository / Data Access Layer
Repository Pattern Implementation
The repository pattern is implemented to abstract data access logic and provide a clean separation between domain objects and persistence concerns. Each entity has its own repository interface with standardized CRUD operations.
public interface WorkflowRepository {
Optional<Workflow> findById(UUID id);
List<Workflow> findByStatus(WorkflowStatus status);
Workflow save(Workflow workflow);
void delete(UUID id);
long countByStatus(WorkflowStatus status);
}
public interface WorkflowInstanceRepository {
Optional<WorkflowInstance> findById(UUID id);
List<WorkflowInstance> findByWorkflowId(UUID workflowId);
WorkflowInstance save(WorkflowInstance instance);
void delete(UUID id);
}
ORM vs. Raw Query Decisions
For this workflow engine, we use JPA/Hibernate as the ORM for most operations due to its maturity and ability to handle complex object graphs. However, raw SQL queries are used for performance-critical batch operations and analytics.
// ORM usage for standard operations
@Query("SELECT w FROM Workflow w WHERE w.status = :status")
List<Workflow> findByStatus(@Param("status") WorkflowStatus status);
// Raw SQL for batch operations
@Modifying
@Query("UPDATE workflow_instances SET status = :newStatus WHERE status = :oldStatus AND last_modified < :cutoff")
int updateInstanceStatusBatch(
@Param("newStatus") WorkflowInstanceStatus newStatus,
@Param("oldStatus") WorkflowInstanceStatus oldStatus,
@Param("cutoff") LocalDateTime cutoff
);
Read vs. Write Separation
We implement CQRS (Command Query Responsibility Segregation) for the workflow engine to separate read and write operations:
// Write side - handles mutations
public class WorkflowWriteRepository {
public WorkflowInstance save(WorkflowInstance instance) {
return workflowInstanceRepository.save(instance);
}
public void updateInstanceStatus(UUID instanceId, WorkflowInstanceStatus status) {
workflowInstanceRepository.updateStatus(instanceId, status);
}
}
// Read side - optimized for queries
public class WorkflowReadRepository {
public List<WorkflowInstanceView> getActiveInstances() {
return workflowInstanceViewRepository.findActive();
}
public WorkflowInstanceSummary getSummary(UUID instanceId) {
return workflowInstanceViewRepository.findById(instanceId);
}
}
Connection Pool Configuration
Connection pool is configured with HikariCP for optimal performance:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
N+1 Query Prevention Strategy
We use eager loading and batch fetching to prevent N+1 queries:
@Query("SELECT w FROM Workflow w JOIN FETCH w.instances WHERE w.id = :id")
Workflow findByIdWithInstances(@Param("id") UUID id);
@Query("SELECT wi FROM WorkflowInstance wi JOIN FETCH wi.steps WHERE wi.workflowId = :workflowId")
List<WorkflowInstance> findByWorkflowWithSteps(@Param("workflowId") UUID workflowId);
Batch Operation Design
Batch operations are implemented with bulk inserts and updates to reduce database round trips:
public class BatchWorkflowInstanceRepository {
@Transactional
public void batchCreate(List<WorkflowInstance> instances) {
// Use JDBC batch operations for better performance
String sql = "INSERT INTO workflow_instances (id, workflow_id, status, created_at) VALUES (?, ?, ?, ?)";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
for (WorkflowInstance instance : instances) {
ps.setObject(1, instance.getId());
ps.setObject(2, instance.getWorkflowId());
ps.setString(3, instance.getStatus().name());
ps.setTimestamp(4, Timestamp.valueOf(instance.getCreatedAt()));
ps.addBatch();
}
ps.executeBatch();
} catch (SQLException e) {
throw new DataAccessException("Failed to batch create instances", e);
}
}
@Transactional
public int batchUpdateStatus(List<UUID> instanceIds, WorkflowInstanceStatus newStatus) {
String sql = "UPDATE workflow_instances SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (";
sql += String.join(",", Collections.nCopies(instanceIds.size(), "?")) + ")";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, newStatus.name());
for (int i = 0; i < instanceIds.size(); i++) {
ps.setObject(i + 2, instanceIds.get(i));
}
return ps.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to batch update status", e);
}
}
}
Transaction Management
All data access operations are wrapped in proper transaction boundaries:
@Service
@Transactional
public class WorkflowService {
@Autowired
private WorkflowRepository workflowRepository;
@Autowired
private WorkflowInstanceRepository instanceRepository;
public WorkflowInstance createInstance(Workflow workflow) {
WorkflowInstance instance = new WorkflowInstance(workflow);
return instanceRepository.save(instance);
}
@Transactional(readOnly = true)
public List<WorkflowInstance> getActiveInstances() {
return instanceRepository.findByStatus(WorkflowInstanceStatus.ACTIVE);
}
}
9. Asynchronous Processing & Messaging
Event-driven boundaries in this feature
The workflow orchestration engine will use a hybrid synchronous/asynchronous approach. Critical path operations like workflow creation, execution state transitions, and task completion will be handled synchronously to ensure immediate consistency. Non-critical operations like audit logging, notifications, and analytics will be handled asynchronously to improve system responsiveness and decouple components.
Key async triggers:
- Workflow execution completion
- Task state changes (start, complete, fail)
- Retry attempts for failed tasks
- External system integration events
Message schema design with sample payload
{
"version": "1.0",
"eventId": "uuid-1234567890",
"eventType": "workflow.completed",
"timestamp": "2023-10-27T10:30:00Z",
"payload": {
"workflowId": "wf-abc123",
"status": "COMPLETED",
"result": {
"output": "success",
"durationMs": 1200
}
},
"source": "workflow-engine",
"correlationId": "corr-xyz987"
}
Dead letter queue strategy and poison message handling
Poison messages will be routed to a dead letter queue (DLQ) after 3 failed delivery attempts. The DLQ will be monitored by an alerting system and manual intervention process.
public class MessageDeliveryHandler {
private static final int MAX_DELIVERY_ATTEMPTS = 3;
public void handleWithRetry(String message, int attempts) {
try {
// Process message
processMessage(message);
} catch (Exception e) {
if (attempts < MAX_DELIVERY_ATTEMPTS) {
// Retry logic with exponential backoff
retryWithBackoff(message, attempts + 1);
} else {
// Route to DLQ
routeToDeadLetterQueue(message, e);
}
}
}
}
At-least-once vs. exactly-once delivery
At-least-once delivery is selected for this system due to the complexity of maintaining exactly-once semantics across distributed services and the tolerance for duplicate processing in workflow scenarios. The system implements idempotency at the business logic level to handle duplicates gracefully.
Retry policy for consumers
Retry policy:
- Max attempts: 5
- Backoff formula: exponential with jitter (100ms to 2s)
- Non-retryable errors: validation failures, permanent service errors
public class RetryPolicy {
private static final int MAX_ATTEMPTS = 5;
private static final long BASE_DELAY_MS = 100;
private static final long MAX_DELAY_MS = 2000;
public boolean shouldRetry(int attempt, Exception error) {
if (attempt >= MAX_ATTEMPTS) return false;
// Don't retry validation or permanent errors
if (isNonRetryableError(error)) {
return false;
}
return true;
}
public long getDelay(int attempt) {
long delay = (long) Math.pow(2, attempt) * BASE_DELAY_MS;
return Math.min(delay + randomJitter(), MAX_DELAY_MS);
}
private long randomJitter() {
return (long) (Math.random() * 100);
}
}
Outbox pattern for reliable event publishing
Outbox pattern is implemented to ensure message delivery reliability. Events are persisted to an outbox table before being published to the message broker, with a separate background process responsible for publishing.
public class OutboxService {
private final EventRepository eventRepository;
private final MessageBroker messageBroker;
@Transactional
public void publishEvent(Event event) {
// Store in outbox table
OutboxEvent outboxEvent = new OutboxEvent(event);
eventRepository.save(outboxEvent);
// Publish to message broker
messageBroker.publish(outboxEvent.getPayload());
}
}
Sequence diagram for async flows
sequenceDiagram
participant WorkflowEngine
participant EventStore
participant MessageBroker
participant NotificationService
WorkflowEngine->>EventStore: Store event in outbox
EventStore-->>WorkflowEngine: Acknowledge
WorkflowEngine->>MessageBroker: Publish event to queue
MessageBroker-->>WorkflowEngine: Event queued
MessageBroker->>NotificationService: Deliver event
NotificationService-->>MessageBroker: Acknowledge receipt
10. Error Handling Design
Error Classification Hierarchy
Errors are categorized into three main levels:
- Domain Errors - Business logic violations (e.g., workflow not found, invalid state transition)
- Infrastructure Errors - System-level issues (e.g., database connection failure, cache timeout)
- Validation Errors - Input validation failures (e.g., missing required fields, malformed JSON)
public abstract class WorkflowException extends RuntimeException {
private final ErrorCode errorCode;
public WorkflowException(ErrorCode code, String message) {
super(message);
this.errorCode = code;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
public class WorkflowNotFoundException extends WorkflowException {
public WorkflowNotFoundException(String workflowId) {
super(ErrorCode.WORKFLOW_NOT_FOUND, "Workflow with ID " + workflowId + " not found");
}
}
public class WorkflowValidationException extends WorkflowException {
public WorkflowValidationException(String message) {
super(ErrorCode.VALIDATION_ERROR, message);
}
}
Error Propagation Strategy
Errors are propagated up the call stack with appropriate wrapping to preserve context. Critical system errors (infrastructure) are logged at ERROR level and re-thrown, while domain/business errors are wrapped in appropriate custom exceptions for client consumption.
Retry Strategy
For transient infrastructure errors (e.g., DB connection timeouts, external service timeouts), exponential backoff with jitter is implemented:
public class RetryableOperation {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
private static final double JITTER_FACTOR = 0.2;
public <T> T executeWithRetry(Supplier<T> operation) {
int attempts = 0;
while (attempts < MAX_RETRIES) {
try {
return operation.get();
} catch (InfrastructureException e) {
if (attempts >= MAX_RETRIES - 1) throw e;
long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempts)
* (1 + JITTER_FACTOR * (Math.random() - 0.5)));
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RetryException("Retry interrupted", ie);
}
attempts++;
}
}
throw new RetryException("Max retries exceeded");
}
}
Circuit Breaker Design
For external service calls, a circuit breaker pattern is implemented with:
- Failure Threshold: 5 consecutive failures within 10 seconds
- Timeout: 30 seconds before allowing half-open state
- Half-Open State Behavior: Only one request allowed to test if service is recovered
public class CircuitBreaker {
private final int failureThreshold;
private final long timeoutMs;
private final AtomicInteger failureCount = new AtomicInteger(0);
private final AtomicLong lastFailureTime = new AtomicLong(0);
private volatile CircuitState state = CircuitState.CLOSED;
public enum CircuitState {
CLOSED, OPEN, HALF_OPEN
}
public <T> T execute(Supplier<T> operation) {
switch (state) {
case CLOSED:
return executeWithCircuitClosed(operation);
case OPEN:
throw new CircuitBreakerOpenException();
case HALF_OPEN:
return executeWithCircuitHalfOpen(operation);
}
throw new IllegalStateException("Invalid circuit state");
}
}
Fallback Behavior Design
For key dependencies like external APIs or caches, fallbacks are implemented:
public class FallbackService {
public WorkflowResult getWorkflowWithFallback(String workflowId) {
try {
return workflowService.getWorkflow(workflowId);
} catch (ExternalServiceException e) {
// Log the failure
logger.warn("External service failed, using fallback for workflow {}", workflowId);
// Return cached or default value
return getCachedWorkflow(workflowId);
}
}
}
Error Logging Format
Structured logging includes trace context and error metadata:
{
"timestamp": "2023-10-15T14:30:45.123Z",
"trace_id": "abc123def456",
"span_id": "xyz789uvw012",
"user_id": "user-12345",
"entity_id": "workflow-98765",
"error_code": "WORKFLOW_NOT_FOUND",
"error_message": "Workflow with ID workflow-98765 not found",
"stack_trace": "...",
"http_status": 404
}
Alerting Thresholds
Critical alerts are configured for:
- 5xx errors: > 10 errors per minute (warn), > 50 errors per minute (critical)
- Circuit breaker tripped: Alert on first occurrence
- Retry failures: > 5 consecutive retry failures in 5 minutes (critical)
Error Handling in Async Flows
For asynchronous processing, error handling includes:
- Dead Letter Queue (DLQ) for failed messages
- Poison message detection - messages that fail consistently are moved to DLQ
- Retry policies with exponential backoff for transient failures
public class AsyncErrorHandler {
public void handleAsyncFailure(WorkflowEvent event, Exception error) {
if (isPoisonMessage(event)) {
moveMessageToDLQ(event);
return;
}
if (isRetryable(error)) {
scheduleRetry(event, calculateBackoffDelay());
} else {
logErrorAndNotify(event, error);
}
}
}
11. Security Design
Input Validation and Sanitization Strategy
All inputs are validated using a combination of:
- Regex patterns for format validation (e.g., email, UUID)
- Length constraints to prevent buffer overflow attacks
- Type validation using strict schema validation (e.g., JSON Schema, Protobuf)
- Custom validators for domain-specific constraints (e.g., workflow name length, step count limits)
- HTML sanitization using OWASP Java HTML Sanitizer for any user-provided content that might be rendered
SQL Injection Prevention
- Prepared statements are used exclusively for all database queries
- Parameterized queries with no string concatenation in SQL generation
- ORM framework (Hibernate) is used to abstract raw SQL and enforce parameterization
- Input normalization - all identifiers are validated against known safe character sets
Authentication Middleware Design
- JWT-based authentication for API access with 1-hour expiration and refresh token support
- OAuth2 integration via Auth0 for enterprise SSO (if required)
- Authentication middleware intercepts all requests and validates token signature, expiration, and scope
- Token revocation via Redis-based blacklisting for immediate session termination
Authorization Enforcement
- Role-Based Access Control (RBAC) model with roles:
ADMIN,USER,VIEWER - Permission checking at service layer using
@PreAuthorizeannotations in Spring Security - Resource-level authorization - workflows are scoped to users or teams with explicit read/write permissions
- Fine-grained access control for workflow steps using resource ownership model
Sensitive Data Encryption
- At rest: AES-256 encryption using AWS KMS for secrets and sensitive workflow data
- In transit: TLS 1.3 with HSTS header enforcement for all API endpoints
- Secrets storage: Sensitive configuration values (API keys, passwords) are encrypted and stored in AWS Secrets Manager
- Encryption key rotation: 90-day automatic rotation with proper key backup and recovery procedures
PII Handling and Masking Strategy
- PII data (user IDs, email addresses) are masked in logs using redaction patterns
- Log fields containing PII are excluded from structured logging or masked with
*** - Audit trail logs only include necessary identifiers (workflow ID, user ID) without full PII
- Data retention policy ensures PII is deleted after 30 days of workflow completion
Secrets Management
- AWS Secrets Manager for storing and rotating API keys, database credentials, and third-party tokens
- Secrets rotation every 90 days with automatic failover to new secrets
- Access control using IAM policies that restrict access to specific secrets by service role
- Secrets caching with TTL of 5 minutes to reduce API calls to secrets manager
Audit Logging Design
- Audit events capture: timestamp, user ID, action type, resource ID, IP address, session ID
- Event schema includes fields:
timestamp,userId,action,resourceType,resourceId,ipAddress,userAgent - Retention policy logs kept for 90 days with automatic archiving to S3 Glacier
- Compliance logs are stored in a separate encrypted database with read-only access for compliance teams
- Log integrity verified using HMAC-SHA256 signatures to prevent tampering
// Example of input validation in a workflow creation endpoint
@PostMapping("/workflows")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<Workflow> createWorkflow(
@Valid @RequestBody CreateWorkflowRequest request,
@AuthenticationPrincipal Jwt principal) {
// Validate workflow name format
if (!request.getName().matches("^[a-zA-Z0-9_-]{1,64}$")) {
throw new ValidationException("Workflow name must be 1-64 alphanumeric characters, dashes, or underscores");
}
// Sanitize user-provided description
String sanitizedDescription = HtmlSanitizer.sanitize(request.getDescription());
// Validate and encrypt sensitive configuration
String encryptedConfig = encryptionService.encrypt(request.getConfiguration());
// Create workflow with validated and sanitized data
Workflow workflow = workflowService.createWorkflow(
request.getName(),
sanitizedDescription,
encryptedConfig,
principal.getSubject()
);
return ResponseEntity.ok(workflow);
}
12. Testing Strategy
Unit Testing Scope and Coverage Targets
- Target coverage: 85% line coverage, 90% branch coverage for core business logic
- Scope: All service classes, domain objects, value objects, and utility functions
- Test patterns:
- Parameterized tests for boundary conditions (e.g., max workflow steps, timeout values)
- Mock-based testing for external dependencies (database, message broker, HTTP clients)
- State machine transition testing with edge cases (invalid state transitions, missing required fields)
Integration Testing Strategy
- Database integration:
- Use testcontainers for PostgreSQL/MySQL to validate repository operations and transaction boundaries
- Test data isolation using unique schema per test or database transaction rollback
- External service integration:
- WireMock for HTTP endpoints to simulate external task execution services
- Embedded Redis for cache layer integration testing (if applicable)
- Cross-service integration:
- Test event-driven flows using embedded Kafka/RabbitMQ in test containers
- Validate saga coordination and compensation logic through end-to-end test scenarios
Contract Testing
- Framework: Pact (consumer-driven contract testing)
- Scope:
- Define contracts between workflow engine and task execution services
- Test API compatibility for task status updates, retry requests, and error notifications
- Versioning strategy:
- Contract versioning aligned with major API versions
- Automated contract verification in CI pipeline before deployment
Performance Testing Tool and Key Scenarios
- Tool: k6 (JavaScript-based load testing framework)
- Key scenarios:
- High-concurrency workflow creation and execution (1000 concurrent workflows)
- Long-running workflows with 50+ steps and 30s step timeouts
- Retry-heavy workflows under network latency stress (50ms to 200ms)
- Metrics tracked:
- p95/p99 response times for workflow creation and status polling
- Throughput (workflows/sec) under different load levels
- Memory and CPU utilization during peak load
Test Data Management Strategy
- Fixtures:
- Predefined JSON fixtures for common workflow templates, task definitions, and execution states
- Factory pattern for generating test data with realistic distribution (e.g., 30% of workflows have 10 steps, 70% under 5 steps)
- Seeding:
- Use database migration scripts to seed test environments with reference data (task types, status enums)
- Test data cleanup strategy using database transaction rollback or schema truncation
- Data isolation:
- Use unique identifiers (UUIDs) to prevent test data collision in shared environments
- Implement test-specific database schemas or namespaces when running parallel tests
13. Observability Design
Structured Logging Schema
All logs must follow a consistent schema to enable effective search, filtering, and analysis:
{
"timestamp": "2023-10-27T14:30:00.000Z",
"level": "INFO|WARN|ERROR",
"service": "workflow-engine",
"trace_id": "12345678-1234-1234-1234-123456789012",
"span_id": "abcdef12-3456-7890-abcd-ef1234567890",
"user_id": "user_12345",
"workflow_id": "wf_98765",
"operation": "start_workflow",
"message": "Workflow started successfully",
"duration_ms": 150,
"error_code": "WORKFLOW_START_FAILED",
"error_details": {
"message": "Invalid workflow definition",
"stack_trace": "..."
}
}
PII masking rules:
- User IDs and workflow IDs are logged as hashed values (e.g.,
user_12345→user_hashed) - Sensitive fields like passwords or tokens are completely omitted from logs
- All log fields must be validated for length and format before logging
Distributed Tracing Instrumentation
Using OpenTelemetry with custom spans for key operations:
Span Types and Attributes:
-
Workflow Execution Span
- Operation:
workflow.execute - Attributes:
workflow.id: workflow instance IDworkflow.definition_id: workflow template IDworkflow.status: PENDING, RUNNING, COMPLETED, FAILED
- Operation:
-
Task Execution Span
- Operation:
task.execute - Attributes:
task.id: unique task identifiertask.type: e.g., "HTTP", "DB_QUERY", "EMAIL"task.status: PENDING, RUNNING, COMPLETED, FAILED
- Operation:
-
Event Processing Span
- Operation:
event.process - Attributes:
event.type: "workflow_started", "task_completed"event.source: "message_queue", "api"
- Operation:
Metrics Design
RED Method Metrics (Rate, Errors, Duration):
| Metric Name | Type | Labels | Description |
|---|---|---|---|
workflow_executions_total | Counter | status, workflow_type | Total number of workflow executions |
workflow_execution_errors_total | Counter | error_code, workflow_type | Errors during workflow execution |
workflow_execution_duration_seconds | Histogram | status, workflow_type | Execution time in seconds |
task_executions_total | Counter | status, task_type | Total number of task executions |
task_execution_errors_total | Counter | error_code, task_type | Errors during task execution |
task_execution_duration_seconds | Histogram | status, task_type | Task execution time in seconds |
event_processing_total | Counter | event_type, status | Events processed successfully or failed |
queue_backlog_size | Gauge | queue_name | Current size of message queues |
active_workflow_instances | Gauge | workflow_type | Currently running workflows |
Additional Metrics:
workflow_engine_active_threads(Gauge): Current thread pool usagemessage_queue_processing_rate(Counter): Events processed per secondretry_count_total(Counter): Number of retries attempted
Health Check Endpoint Design
GET /health
GET /ready
GET /live
GET /startup
Response Format:
{
"status": "healthy|degraded|unhealthy",
"checks": {
"database": {
"status": "healthy",
"timestamp": "2023-10-27T14:30:00.000Z"
},
"message_queue": {
"status": "healthy",
"timestamp": "2023-10-27T14:30:00.000Z",
"backlog": 0
},
"cache": {
"status": "healthy",
"timestamp": "2023-10-27T14:30:00.000Z"
}
},
"timestamp": "2023-10-27T14:30:00.000Z"
}
Health Check Logic:
/live: Basic liveness check (process running)/ready: Readiness check (dependencies available)/health: Comprehensive health including database, cache, and queue status/startup: Validates that all required services are initialized
Alerting Thresholds
| Metric | Warning Level | Critical Level | Escalation Path |
|---|---|---|---|
workflow_execution_errors_total | > 10 errors/minute | > 50 errors/minute | On-call SRE team |
workflow_execution_duration_seconds (p99) | > 5s | > 30s | On-call SRE team |
queue_backlog_size | > 1000 messages | > 5000 messages | On-call SRE team |
message_queue_processing_rate | < 50 events/sec | < 10 events/sec | On-call SRE team |
workflow_engine_active_threads | > 80% of max threads | > 95% of max threads | On-call SRE team |
database_connection_pool_utilization | > 80% | > 95% | On-call SRE team |
Alerting Rules Example (Prometheus-style):
# Alert for high error rate in workflow execution
ALERT WorkflowErrorRateHigh
IF rate(workflow_execution_errors_total[5m]) > 10
FOR 2m
ANNOTATIONS {
summary = "High workflow execution error rate",
description = "Workflow engine is experiencing {{ $value }} errors per minute"
}
# Alert for long-running workflows
ALERT LongRunningWorkflow
IF workflow_execution_duration_seconds{quantile="0.99"} > 30
FOR 5m
ANNOTATIONS {
summary = "Long-running workflow detected",
description = "Workflow execution took more than 30 seconds"
}
On-call escalation path:
- Primary on-call engineer (5-minute response)
- SRE team (15-minute response)
- Engineering leadership (30-minute response)
14. Open Questions, Risks & Trade-offs
Decisions requiring product / engineering alignment
- Workflow versioning strategy: Should workflows support versioning with backward compatibility, or treat each new workflow definition as a completely separate entity? This impacts how we handle schema evolution and migration.
- Execution persistence model: Should workflow executions be stored in a single durable store or split between transient execution state and persistent metadata stores? This affects system complexity and recovery behavior.
- Maximum workflow runtime limits: What's the acceptable maximum runtime for a workflow? This impacts whether we need to support long-running workflows or can rely on timeouts and restart strategies.
- Workflow state persistence granularity: Should each workflow instance maintain a complete history of all steps, or only the current execution state with minimal audit trail? This affects storage costs and debugging capabilities.
Known trade-offs and justifications
| Trade-off | Option 1 | Option 2 | Justification |
|---|---|---|---|
| State persistence vs. performance | Store full workflow state in database | Store only current execution in memory with snapshots | Option 1 provides better fault tolerance but increases storage and I/O costs; Option 2 offers better performance but requires more complex recovery logic |
| Event-driven vs. polling | Use event-driven architecture for workflow steps | Poll for step completion status | Option 1 provides better scalability and responsiveness but increases system complexity; Option 2 is simpler to implement but less scalable |
| Workflow definition vs. execution | Single unified model for workflow definition and execution | Separate models with translation layer | Option 1 simplifies the system but makes it harder to evolve definitions independently; Option 2 provides better separation of concerns but adds complexity |
| Idempotency enforcement | Enforce idempotency at the workflow engine level | Leave idempotency responsibility to individual steps | Option 1 provides consistent behavior but increases engine complexity; Option 2 offers flexibility but risks inconsistent state |
Risks and mitigation strategies
-
Workflow execution timeout: Risk of workflows running indefinitely due to missing completion signals or deadlocks in steps.
- Mitigation: Implement strict timeout policies with configurable max execution durations and automatic termination of stuck workflows.
-
Data consistency in distributed workflows: Risk of inconsistent state across multiple services when a workflow spans multiple systems.
- Mitigation: Use saga pattern with compensating transactions and maintain detailed audit logs for debugging and recovery.
-
Resource exhaustion under high load: Risk of system resource exhaustion when handling large numbers of concurrent workflow executions.
- Mitigation: Implement rate limiting, circuit breakers, and resource pool management with proper monitoring and alerting.
-
Complexity of workflow definition language: Risk that the DSL becomes too complex for users to understand and maintain.
- Mitigation: Provide comprehensive documentation, examples, and tooling for workflow definition and validation.
Technical debt acknowledged
- Hardcoded workflow step implementations: Current design assumes all steps are implemented within the engine, which creates tight coupling and makes it difficult to support external step services.
- Limited workflow composition patterns: The current design doesn't support advanced composition patterns like sub-workflows or conditional branching without significant refactoring.
- Monolithic architecture for scaling: The engine is currently designed as a monolithic service, which makes horizontal scaling more difficult than it needs to be.
- Inconsistent error handling across steps: Different step types may have inconsistent error handling and retry behaviors, which can lead to unpredictable workflow behavior.
- Limited monitoring and observability: The system lacks comprehensive metrics and tracing for workflow execution, making debugging and performance optimization difficult.