Research Report
November 2025
This report analyzes the architectural patterns and design decisions across modern code-first workflow engines. Based on research of major platforms including Temporal, Airflow, Prefect, Dagster, and others, we identify seven core architectural patterns and key trade-offs that define this space.
Key Finding: Despite superficial differences, all code-first workflow engines converge on similar foundational patterns—queue-based task distribution, state machine cores, and separation of orchestration from execution—while diverging significantly in their approach to developer experience, durability guarantees, and domain specialization.
Code-first workflow engines allow developers to define complex, multi-step processes using general-purpose programming languages rather than visual designers or XML configurations. These engines handle orchestration, state management, failure recovery, and coordination of distributed tasks.
This analysis covers:
Workflow engines are critical infrastructure for:
Workflow engines fundamentally operate as state machines that store workflow states and track execution history. The underlying implementation varies significantly:
Durable Execution Approach (Temporal/Cadence)
Durable execution engines like Temporal automatically preserve the full state of workflows across failures by persisting every step. This enables workflows to be recovered, replayed, or paused at any arbitrary point. The key innovation is that developers can write code that appears synchronous (including sleeping for 30 days) while the engine handles all persistence and recovery automatically.
Event-Driven Approach (Infinitic, custom engines)
Event-driven engines maintain workflow history and trigger tasks based on events, with the workflow engine maintaining state while workers remain stateless. This pattern separates concerns: the engine handles coordination while workers handle execution.
Stateless Engines (WorkflowEngine .NET, many custom implementations):
Stateful Engines (Temporal with history service):
Every practical workflow engine uses queues to dispatch tasks to workers, avoiding direct task calls to handle flow control, availability, and slowness issues. This is perhaps the most universal pattern across all workflow engines.
Core Components:
Coordinator/Orchestrator
Workers
Task Queues
Matching Service (in sophisticated systems like Temporal)
Task queues are partitioned for horizontal scaling, allowing multiple nodes to independently handle task delivery and avoid bottlenecks. A single task queue might receive tasks from thousands of workflows and be polled by hundreds of workers. Partitioning prevents hotspots and allows linear scaling.
Temporal's Approach:
Workers typically implement:
Workflow engines must transactionally update state, task queues, and timers together to avoid race conditions where state and queues become inconsistent. Consider these failure scenarios:
Without transactional guarantees, these edge cases proliferate.
Transactional Outbox Pattern (Temporal)
Temporal uses a partitioned architecture where each shard storing workflow state also stores a queue, enabling transactional commits followed by transfer to the queueing subsystem. The outbox pattern ensures that:
Single Database Approach (simpler systems)
Many workflow engines use a single database or even a single process to simplify transactional requirements. This works but doesn't scale:
Saga Pattern for Cross-Service Transactions
For workflows spanning multiple services, engines implement saga patterns to maintain eventual consistency without distributed transactions.
The fundamental choice in workflow definition approach shapes the entire developer experience:
Code-First Approach
Workflows defined in general-purpose programming languages (Python, Go, Java, TypeScript) that are executed by the engine.
Advantages:
Disadvantages:
Examples: Temporal, Prefect, Dagster, Airflow
Config-First Approach
Workflows written in YAML/JSON don't require engine redeployment when changed.
Advantages:
Disadvantages:
Examples: AWS Step Functions (JSON), many traditional BPM engines, Kubernetes workflows
Modern systems increasingly adopt hybrid approaches:
Dagster Components
Dagster Components allow mixing declarative YAML DSLs with imperative Python code for common patterns. Teams can:
Windmill's Approach
Windmill lets engineers write scripts in multiple languages (Python, TypeScript, Go) and compose them into workflows via visual DAG editor. This provides:
Distributed workflows can be coordinated through two fundamental patterns:
A central orchestrator tells participants what local transactions to execute. Temporal follows the orchestrator variant of the Saga pattern where a centralized coordinator tracks workflow progress and dispatches commands.
Characteristics:
Advantages:
Disadvantages:
Best For:
Each local transaction publishes domain events that trigger local transactions in other services. Services react to each other's operations without a central conductor, avoiding single point of failure.
Characteristics:
Advantages:
Disadvantages:
Best For:
Many real-world systems combine both:
Data orchestration engines have evolved domain-specific patterns:
Asset-Centric Model (Dagster)
Dagster treats data assets (tables, models, reports) as first-class citizens with dependencies, lineage, and state management central to orchestration. The focus shifts from "what tasks to run" to "what data should exist."
Key Concepts:
Benefits:
Task-Centric Model (Airflow, Prefect)
Traditional DAG-based approach where tasks and their dependencies are primary abstractions. Prefect's task-based model is imperative - developers define exact sequence of steps rather than describing desired end states.
Key Concepts:
Benefits:
Airflow:
Prefect:
Dagster:
Sagas enable orchestrating multi-step workflows across services, with each step providing compensating actions for rollback when failures occur. This is critical in microservices where traditional ACID transactions don't span services.
Saga Characteristics:
Sagas are coordinating objects implemented as state machines that listen for events and instruct other parts of the system. The key distinction between saga variants:
Implementation Pattern:
Example: Order Processing
Normal Flow:
1. Reserve Inventory → Compensate: Release Inventory
2. Charge Payment → Compensate: Refund Payment
3. Ship Order → Compensate: Cancel Shipment
Failure Scenario:
1. Reserve Inventory ✓
2. Charge Payment ✓
3. Ship Order ✗ (fails)
→ Execute: Refund Payment, Release Inventory
Modern workflow engines provide sophisticated retry mechanisms:
Temporal Activities:
Benefits of Durable Execution:
Modern workflow engines must scale along multiple dimensions:
1. Sharding for Parallelism
Sharding enables executing millions of tasks by distributing workflow state across many partitions. Temporal uses 10,000+ shards where each shard stores both workflow state and a queue for transactional updates.
2. Worker Pool Scaling
Workers scale independently:
3. Task Queue Separation
Multiple task queues for different workloads allow isolation of CPU-intensive vs. I/O-bound tasks with independent autoscaling. For example:
4. Database Scaling
Different approaches to scaling the persistence layer:
Workers implement sophisticated patterns:
Pollers
Execution Threads
Heartbeating
Despite significant architectural differences, code-first workflow engines converge on several fundamental patterns:
Universal adoption of message queues for task dispatch:
No successful workflow engine attempts direct synchronous task invocation at scale.
All workflow engines model execution as state transitions:
Even engines with imperative code interfaces (Temporal) internally maintain state machines.
Built-in fault tolerance is non-negotiable:
These shouldn't be the application's responsibility.
Moving beyond pure scheduling to reactive execution:
Modern workflows react to the world, not just the clock.
Rich UIs for monitoring, debugging, and understanding execution state:
Durable execution ensures every step of code execution can be tracked and viewed, enabling quick diagnosis of issues.
Coordination logic separate from business logic:
This separation is critical for testability and maintainability.
The Trade-off:
Airflow has significant operational overhead (scheduler, workers, metadata DB, message queue) but provides a vast ecosystem of operators and integrations. Prefect emphasizes simplicity with a "negative engineering" philosophy—assuming developers know how to code and removing unnecessary abstractions. Dagster takes a first-principles approach with the full development lifecycle in mind, from development to deployment to monitoring.
Implications:
Early workflow systems like AWS Simple Workflow Service (SWF) struggled with adoption despite being conceptually advanced due to challenging developer experience. The lesson: power without usability leads to low adoption.
The Spectrum:
Full Code Flexibility (Temporal): Any code in workflow, full language power
Constrained Frameworks (Airflow with TaskFlow API): Structured patterns with some flexibility
Declarative DSLs (YAML workflows): Limited to defined constructs
Modern platforms aim to provide flexibility with visibility and guardrails enterprises need to move fast without breaking things.
Historical Context:
Traditional workflow engines were often designed to run as single process and not distributed, limiting scalability. This was acceptable when workflows were departmental tools, not production infrastructure.
Modern Requirements:
Modern engines embrace distributed architecture from the start, but this adds complexity.
Historical Focus:
Airflow historically focused on scheduled batch jobs (nightly ETL, daily reports). This made sense when data was batch-oriented.
Modern Reality:
Prefect and Dagster offer superior event-native design for real-time pipelines. Airflow 3.0 (April 2025) improved event-driven capabilities but still carries batch-oriented legacy.
Trade-off:
General-Purpose Engines (Temporal, Prefect):
Domain-Specific Engines (Dagster for data):
The trend is toward general engines with domain-specific extensions.
Phase 1: Traditional BPM (2000s)
Phase 2: Code-First Data (2010s)
Phase 3: Modern Durable Execution (2015-2020)
Phase 4: Asset-Centric & Declarative (2020+)
1. Convergence on Event-Driven
All engines moving toward event-native architectures:
2. AI/ML Workload Support
Specialized features for AI/ML:
3. Hybrid Cloud & Edge
Workflows must run anywhere:
4. Developer Experience Revolution
Focus on DX improvements:
5. Platform Consolidation
Engines absorbing adjacent concerns:
Predicted Evolutions:
AI-Assisted Workflow Development
Multi-Model Orchestration
Deeper Cloud Integration
Cross-Organization Workflows
For Data/Analytics Teams:
Start with Dagster if:
Choose Airflow if:
Consider Prefect if:
For Distributed Systems/Microservices:
For General Automation:
Choose Windmill if:
Choose n8n if:
When Building Custom Workflows:
Always use queues for task distribution
Ensure transactional consistency
Make workers stateless
Implement comprehensive observability
Support local development
Design for failure
From Homegrown to Managed Engine:
Incremental Migration Path:
Many engines (especially Dagster) support observing existing systems:
Temporal/Durable Execution:
Data Orchestration:
Patterns & Architecture:
Community Resources:
| Feature | Temporal | Airflow | Prefect | Dagster |
|---|---|---|---|---|
| Primary Use Case | Distributed transactions | Data pipelines | General workflows | Data platforms |
| Definition Style | Code-first (Python, Go, Java, TS) | Code-first (Python DAGs) | Code-first (Python) | Asset-centric (Python) |
| State Model | Durable execution, full replay | Metadata DB, task state | Event-driven, state streams | Asset materialization state |
| Scheduling | Event + cron | Primarily cron | Event-native | Asset-aware, event-driven |
| Deployment | Self-hosted or cloud | Self-hosted (complex) | Cloud or self-hosted | Self-hosted or cloud |
| Scalability | 10,000+ shards | Horizontal via executors | Agent-based elastic | gRPC user code isolation |
| Best For | Mission-critical transactions | Established ETL pipelines | Rapid development | Modern data platforms |
| Learning Curve | Moderate | Steep | Gentle | Moderate |
| Operational Overhead | Moderate | High | Low | Moderate |
| Community Size | Growing | Very large | Growing | Growing rapidly |
Activity: A single unit of work in a workflow, typically a function or service call.
Asset: In data orchestration, a data object like a table or model that has dependencies and lineage.
Choreography: Coordination pattern where services react to events without central orchestrator.
Compensating Action: Undo operation for a completed workflow step, used in saga rollback.
DAG (Directed Acyclic Graph): Structure representing workflow steps and their dependencies.
Durable Execution: Programming model where code execution state persists automatically across failures.
Orchestration: Coordination pattern where central service directs other services.
Saga: Long-running transaction pattern with compensating actions for rollback.
Shard: Partition of workflow state for horizontal scaling.
Task Queue: Message queue holding work items for workers to process.
Worker: Stateless process that executes workflow tasks.
Workflow: Multi-step process with defined coordination and error handling.
from datetime import timedelta from temporalio import workflow, activity @activity.defn async def send_email(recipient: str) -> None: # Send email logic print(f"Email sent to {recipient}") @workflow.defn class SubscriptionWorkflow: @workflow.run async def run(self, user_id: str) -> str: # Day 1: Welcome email await workflow.execute_activity( send_email, user_id, start_to_close_timeout=timedelta(minutes=5) ) # Wait 7 days await workflow.sleep(timedelta(days=7)) # Day 8: Follow-up email await workflow.execute_activity( send_email, user_id, start_to_close_timeout=timedelta(minutes=5) ) return f"Campaign complete for {user_id}"
from airflow import DAG from airflow.operators.python import PythonOperator from datetime import datetime, timedelta def extract_data(): # Extract logic return "data" def transform_data(ti): data = ti.xcom_pull(task_ids='extract') # Transform logic return f"transformed_{data}" def load_data(ti): data = ti.xcom_pull(task_ids='transform') # Load logic print(f"Loading {data}") with DAG( 'etl_pipeline', schedule_interval=timedelta(days=1), start_date=datetime(2025, 1, 1), catchup=False ) as dag: extract = PythonOperator( task_id='extract', python_callable=extract_data ) transform = PythonOperator( task_id='transform', python_callable=transform_data ) load = PythonOperator( task_id='load', python_callable=load_data ) extract >> transform >> load
from dagster import asset, AssetExecutionContext @asset def raw_customers(context: AssetExecutionContext): """Extract raw customer data""" # Fetch from API return fetch_customers() @asset(deps=[raw_customers]) def cleaned_customers(context: AssetExecutionContext): """Clean and validate customer data""" # Load raw data raw = load_asset("raw_customers") # Clean data return clean_data(raw) @asset(deps=[cleaned_customers]) def customer_metrics(context: AssetExecutionContext): """Calculate customer metrics""" cleaned = load_asset("cleaned_customers") return calculate_metrics(cleaned)
from prefect import flow, task from datetime import timedelta @task(retries=3, retry_delay_seconds=60) def fetch_data(url: str): # Fetch logic with automatic retries return fetch_from_api(url) @task def process_data(data): # Processing logic return processed_data @flow(log_prints=True) def etl_flow(source_url: str): raw_data = fetch_data(source_url) processed = process_data(raw_data) return processed # Run flow if __name__ == "__main__": etl_flow("https://api.example.com/data")
Report Version: 1.0
Publication Date: November 2025
Author: Research Analysis
Status: Final
Change Log:
Contact: For questions or feedback about this report, please refer to the research methodology and sources cited.
End of Report