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
  // ─── 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
  // 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

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:

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

Error Handling

Handler Failure

If an action fails, the handler aborts:

flow
on> :checkout from @customer
  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
  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 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.