Skip to content

Actor Communication

EventFlow is built on the Actor Model. Understanding actor communication patterns is key to designing effective systems.

Actor Model Basics

In the Actor Model:

  • Each actor (machine) has isolated state
  • Actors communicate only through messages (events)
  • Messages are processed one at a time
  • Message delivery is asynchronous
Actor A[state][mailbox]Actor B[state][mailbox]event

No Shared State

Actors never directly access each other's state:

flow
// ❌ Not possible - cannot read another machine's context
? @payment.$balance > 100
  process order

// ✅ Correct - ask through events
emit :check_balance to @payment

on :balance_sufficient from @payment
  process order

on :balance_insufficient from @payment
  reject order

Asynchronous Communication

Events are asynchronous - the sender doesn't wait:

flow
on> :checkout from @customer
  emit :payment_request to @payment    // Sent, not waiting
  order moves to #awaiting_payment     // Continues immediately

  // Later, when payment responds...

on :payment_success from @payment
  order moves to #paid

Communication Patterns

Request-Response

The most common pattern - one machine asks, another answers:

flow
machine: @order

  on> :checkout from @customer
    emit :authorize_payment to @payment
    order moves to #authorizing

  on :payment_authorized from @payment
    order moves to #paid

  on :payment_declined from @payment
    order moves to #payment_failed

machine: @payment

  on :authorize_payment from @order
    validate card
    ? card is valid
      emit :payment_authorized to @order
    ?
      emit :payment_declined to @order

Fire and Forget

Send an event without expecting a response:

flow
on> :order_completed from @system
  emit :log_event to @analytics        // No response expected
  emit :send_email to @notification    // No response expected
  order moves to #completed

Scatter-Gather (Fan-Out/Fan-In)

Send to multiple machines, collect responses:

flow
on> :validate_order from @customer
  // Fan-out
  emit :check_payment to @payment
  emit :check_inventory to @inventory
  emit :check_fraud to @fraud
  order moves to #validating

// Fan-in
on :payment_ok from @payment
  $payment_valid becomes true
  check_all_validations

on :inventory_ok from @inventory
  $inventory_valid becomes true
  check_all_validations

on :fraud_ok from @fraud
  $fraud_valid becomes true
  check_all_validations

// Helper check
? $payment_valid and $inventory_valid and $fraud_valid
  order moves to #validated

Chain (Pipeline)

Events flow through a sequence of machines:

flow
// @order -> @payment -> @fraud -> @inventory -> @shipping

machine: @order
  on> :checkout from @customer
    emit :process to @payment

machine: @payment
  on :process from @order
    charge card
    emit :verify to @fraud

machine: @fraud
  on :verify from @payment
    check fraud signals
    emit :reserve to @inventory

machine: @inventory
  on :reserve from @fraud
    reserve stock
    emit :ship to @shipping

machine: @shipping
  on :ship from @inventory
    create shipment
    emit :order_shipped to @order

Saga Pattern

Coordinated transactions with compensation:

flow
machine: @order_saga

  on> :start_checkout from @customer
    emit :reserve_inventory to @inventory
    saga moves to #reserving

  on :inventory_reserved from @inventory
    emit :charge_payment to @payment
    saga moves to #charging

  on :payment_charged from @payment
    emit :confirm_order to @order
    emit :order_confirmed to @customer
    saga moves to #completed

  // Compensation on failure
  on :payment_failed from @payment
    emit :release_inventory to @inventory
    emit :checkout_failed to @customer
    saga moves to #failed

  on :inventory_unavailable from @inventory
    emit :checkout_failed to @customer
    saga moves to #failed

Event Routing

By Source

Handle the same event differently based on sender:

flow
on :approval from @manager
  order moves to #manager_approved

on :approval from @director
  order moves to #director_approved
  skip_further_approvals

Without Source (Any Sender)

Accept from any machine:

flow
on :status_update
  update internal status
  log status change

Timeouts and Retries

EventFlow doesn't have built-in timeout syntax, but you can design timeout handling:

flow
machine: @order

  on> :checkout from @customer
    emit :payment_request to @payment
    $payment_requested_at becomes now
    order moves to #awaiting_payment

  on :payment_success from @payment
    order moves to #paid

  on :payment_timeout
    ? order is in #awaiting_payment
      $retry_count increases by 1
      ? $retry_count < 3
        emit :payment_request to @payment
      ?
        order moves to #payment_failed
        emit :checkout_failed to @customer

// Separate scheduled check
on :check_payment_timeouts
  triggered: every 5 minutes

  for each order in #awaiting_payment
    ? $payment_requested_at older than 10 minutes
      emit :payment_timeout to @order

Error Handling

Explicit Error Events

flow
machine: @payment

  on :payment_request from @order
    ? card is valid
      process payment
      ? payment successful
        emit :payment_success to @order
      ?
        emit :payment_error to @order
          with $error_code, $error_message
    ?
      emit :invalid_card to @order

Handling Errors

flow
machine: @order

  on :payment_success from @payment
    order moves to #paid

  on :payment_error from @payment
    log error $error_message
    ? $retry_count < 3
      $retry_count increases by 1
      emit :payment_request to @payment
    ?
      order moves to #payment_failed
      emit :order_failed to @customer

  on :invalid_card from @payment
    emit :update_payment_method to @customer
    order moves to #needs_payment_method

Best Practices

Define Clear Contracts

Document what events each machine sends and receives:

flow
machine: @payment

// RECEIVES:
//   :payment_request from @order
//     - order_id: string
//     - amount: number
//     - currency: string
//
// EMITS:
//   :payment_success to @order
//     - transaction_id: string
//   :payment_failed to @order
//     - error_code: string
//     - error_message: string

Keep Messages Small

Only include necessary data:

flow
// Good - minimal data
emit :order_completed to @notification
  with $order_id, $customer_email

// Avoid - too much data
emit :order_completed to @notification
  with $entire_order_object, $all_customer_data, $full_history

Use Idempotent Handlers

Design event handlers to be safely retried:

flow
on :process_payment from @order
  ? not already_processed($transaction_id)
    process the payment
    mark as processed
  // If already processed, do nothing

Released under the MIT License.