Skip to content

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).

flow
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:

flow
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 @analytics

The emitted events are:

  1. Queued for later processing
  2. Processed by target machines independently
  3. Not blocking the current handler

For advanced queue configuration including priority levels, retry policies, and dead letter queues, see Event Queues.

Complete Execution Flow

Event ReceivedLoad Aggregate State(replay past events)Evaluate Guards[pass][fail]Execute Actions(synchronously)Skip HandlerQueue Emissions(asynchronously)Persist EventProcess Emissions

Transaction Boundaries

Each event handler is a transaction:

flow
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 @notification

If $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 2

Across 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 order

Persistence

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-1000

This 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 processes

This 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:

flow
on :process_payment from @payment
  ? not already_processed($payment_id)
    charge card
    $processed_payments adds $payment_id
  // If already processed, silently succeed

This allows safe event retry without double-processing.

Async API Responses

For long-running operations, return immediately while processing continues in the background:

flow
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_worker

The caller receives a 202 response immediately. When processing completes, a callback notifies them:

flow
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:

flow
on :checkout from @customer (api)
  validate cart           // succeeds
  process payment         // FAILS
  order moves to #paid    // never runs

The event is NOT persisted. The aggregate state doesn't change.

Retry Strategies

Failed events can be retried:

flow
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 @order

Dead 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:

flow
// 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 long

Batch Processing

For high-volume scenarios, process in batches:

flow
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

Released under the MIT License.