Skip to main content

Envelopes

Every message traveling through the actor system is wrapped in an Envelope before it enters a mailbox. The envelope adds routing metadata and distributed-tracing identifiers so message flows are traceable across actor boundaries and across process boundaries in the worker pool.

The design

When you call tell() or ask(), the runtime constructs an Envelope around your message. The envelope is immutable — a final readonly class — and carries:

  • message — the original user message object.
  • sender and targetActorPath values identifying the two parties. These survive serialization, unlike live ActorRef objects.
  • senderRef — optional live ActorRef to the sender. Present when the runtime can supply it; null for anonymous sends.
  • requestId — a unique ID for this specific tell() or ask() call.
  • correlationId — ties together all messages belonging to the same logical request chain (fan-out messages share the correlation ID of the originating message).
  • causationId — identifies the direct parent message that triggered this one, enabling a causal DAG for distributed tracing.
  • metadataarray<string, string> for arbitrary key-value pairs (trace headers, tenant IDs, etc.).

When Envelope::of() creates a new envelope from scratch, requestId, correlationId, and causationId all start as the same generated ID. Child messages produced in response to a parent message should propagate correlationId and set causationId to the parent's requestId.

Tradeoffs

You gainYou give up
Full message provenance for every in-flight messageOne heap allocation per message send
Serialization-safe routing (paths, not live refs)Slightly more payload when crossing process boundaries
Pluggable tracing without touching user message classes

When you encounter it

Most application code never touches Envelope directly — tell() and ask() handle construction. You work with envelopes directly in three situations:

  • Custom transports — the worker-pool WorkerTransport interface passes Envelope objects directly across Swoole threads. Implementing a transport means constructing and consuming envelopes.
  • Serialization adaptersEnvelopeSerializer wraps message serialization, and its implementations accept and produce Envelope instances.
  • Tracing instrumentation — intercepting the mailbox layer to stamp correlationId/causationId headers onto outbound HTTP requests or log entries.
src/Transport/MyCustomTransport.php
<?php

declare(strict_types=1);

use Monadial\Nexus\Core\Mailbox\Envelope;
use Monadial\Nexus\Core\Actor\ActorPath;

// Reconstruct an envelope from wire bytes in a custom transport
$envelope = Envelope::of($message, $senderPath, $targetPath)
->withCorrelationId($incomingCorrelationId)
->withCausationId($incomingRequestId)
->withMetadata(['x-tenant' => $tenantId]);

$transport->send($targetWorkerId, $envelope);

See also