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
// ─── 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
// 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
Complete Execution Flow
┌─────────────────────────────────────────────────────────────┐
│ Event Received │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Load Aggregate State │ │
│ │ (replay past events) │ │
│ └───────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Evaluate Guards │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ │ │
│ [pass] [fail] │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Execute Actions │ │ Skip Handler │ │
│ │ (synchronously) │ │ │ │
│ └────────┬─────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Queue Emissions │ │
│ │ (asynchronously) │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Persist Event │ │
│ │ (to event store)│ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Process Queued │ │
│ │ Emissions │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Transaction Boundaries
Each event handler is a transaction:
on> :transfer_funds from @user
? 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.
Error Handling
Handler Failure
If an action fails, the handler aborts:
on> :checkout from @customer
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
emit :start_report_generation to @report_worker
report moves to #generating
// Avoid - blocking the handler
on> :generate_report from @user
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