Execution Model
Understanding how EventFlow executes your machines is important for designing effective systems.
Synchronous Event Handling
Event handlers execute synchronously - all actions within a handler complete as a single unit (transaction).
on :checkout from @customer (api)
// ─── All of this runs synchronously ───
validate cart
calculate totals
$order_id becomes uuid()
order moves to #awaiting_payment
// ─────────────────────────────────────If any action fails, the entire handler fails and no changes are persisted.
Asynchronous Event Emission
The emit keyword queues events for asynchronous processing:
on :checkout from @customer (api)
// Synchronous actions
order moves to #awaiting_payment
// These are QUEUED, not immediately processed
emit :payment_request to @payment
emit :analytics_event to @analyticsThe emitted events are:
- Queued for later processing
- Processed by target machines independently
- Not blocking the current handler
For advanced queue configuration including priority levels, retry policies, and dead letter queues, see Event Queues.
Complete Execution Flow
Transaction Boundaries
Each event handler is a transaction:
on :transfer_funds from @user (api)
? $source_account has sufficient funds
// All of this succeeds or fails together
$source_account decreases by $amount
$destination_account increases by $amount
$transfer_id becomes uuid()
transfer moves to #completed
emit :transfer_complete to @notificationIf $destination_account increases by $amount fails, the entire handler rolls back - including the $source_account decrease.
Event Ordering
Within a Machine
Events for a single aggregate are processed in order:
@order:abc123
Event 1: :checkout → processes first
Event 2: :add_item → waits for Event 1
Event 3: :payment_ok → waits for Event 2Across Machines
Different machines process events independently:
@order:abc123 → processing :checkout
@payment:xyz789 → processing :charge (parallel)
@inventory:def → processing :reserve (parallel)Event Sourcing Details
State Reconstruction
State is rebuilt by replaying events:
Events for @order:abc123:
1. :checkout → state: #pending, $total: 100
2. :add_item → state: #pending, $total: 150
3. :payment_success → state: #paid, $total: 150
Current state = replay all events in orderPersistence
Every processed event is persisted:
┌────────────┬──────────────────┬─────────────┬─────┐
│aggregate_id│ event │ data │ seq │
├────────────┼──────────────────┼─────────────┼─────┤
│order-abc123│ :checkout │ {cart: ...} │ 1 │
│order-abc123│ :payment_success │ {txn: ...} │ 2 │
│order-abc123│ :shipped │ {track: ...}│ 3 │
└────────────┴──────────────────┴─────────────┴─────┘Snapshots (Optimization)
For aggregates with many events, snapshots improve performance:
Instead of replaying 1000 events:
1. Load snapshot at event 990
2. Replay only events 991-1000This is a runtime optimization - your EventFlow code stays the same.
Concurrency
Single Aggregate Concurrency
An aggregate processes one event at a time:
@order:abc123
├── Event A arrives → starts processing
├── Event B arrives → waits in queue
├── Event A completes →
└── Event B starts → now processesThis prevents race conditions within an aggregate.
Cross-Aggregate Concurrency
Different aggregates process in parallel:
@order:abc123 ─── processing Event A
@order:xyz789 ─── processing Event B (parallel)
@order:def456 ─── processing Event C (parallel)Idempotency
Events should be designed for idempotent handling:
on :process_payment from @payment
? not already_processed($payment_id)
charge card
$processed_payments adds $payment_id
// If already processed, silently succeedThis allows safe event retry without double-processing.
Async API Responses
For long-running operations, return immediately while processing continues in the background:
on :generate_report from @admin (api)
$report_id becomes uuid()
report moves to #queued
reply 202 async for :generate_report:
| report_id | $report_id |
| status | "queued" |
emit :process_report to @report_workerThe caller receives a 202 response immediately. When processing completes, a callback notifies them:
on :report_ready from @report_worker
send callback for :generate_report:
| report_id | $report_id |
| status | "completed" |
| url | $report_url |See Machine Responses - Async Responses for complete async patterns including webhooks and WebSocket callbacks.
Error Handling
Handler Failure
If an action fails, the handler aborts:
on :checkout from @customer (api)
validate cart // succeeds
process payment // FAILS
order moves to #paid // never runsThe event is NOT persisted. The aggregate state doesn't change.
Retry Strategies
Failed events can be retried:
on :process_payment from @order
? $retry_count < 3
attempt payment
? payment failed
$retry_count increases by 1
emit :retry_payment to @self after 5 minutes
otherwise
emit :payment_success to @order
otherwise
emit :payment_failed to @orderDead Letter Queue
Events that repeatedly fail go to a dead letter queue for manual inspection.
Performance Considerations
Keep Handlers Fast
Long-running work should be delegated:
// Good - delegate heavy work
on :generate_report from @user (api)
emit :start_report_generation to @report_worker
report moves to #generating
// Avoid - blocking the handler
on :generate_report from @user (api)
generate 100MB report inline // blocks too longBatch Processing
For high-volume scenarios, process in batches:
on :process_batch
triggered: every 5 minutes
for each order in #pending_sync
? batch_size < 100
emit :sync_order to @external_system
$batch_size increases by 1