Postboy Help

Message Roles

Everything that travels through a Postboy bus is a message. All messages share one abstract base, PostboyMessage, and specialise into three roles: events (PostboyGenericMessage) for fire-and-forget broadcasts, callbacks (PostboyCallbackMessage<T>) for asynchronous request/response, and executors (PostboyExecutor<T>) for synchronous commands. Choosing the right role for each interaction keeps its intent visible and its contract typed.

The base contract: PostboyMessage

PostboyMessage is abstract and never instantiated directly. It gives every message:

  • id — a getter returning the class's static ID. The bus routes by this string, not by class identity.

  • metadata — a mutable PostboyMessageMetadata object (correlationId, causationId, tags, plus any custom keys) used for tracing and enrichment.

  • setMetadata(partial) — merges a partial object into the metadata and returns the same instance:

const message = new UserCreatedEvent('user-42') .setMetadata({tags: new Set(['audit'])});

The static ID rule

Every concrete message class declares its own identity:

import {PostboyGenericMessage} from '@artstesh/postboy'; export class UserCreatedEvent extends PostboyGenericMessage { static readonly ID = 'app.user.created'; constructor(public readonly userId: string) { super(); } }

Three rules apply:

  • The field must be static readonly on the class itself. A class without one fails the built-in checkId guard with "<ClassName> should have a static ID field".

  • The value must be unique per class. Routing is keyed by ID, so two classes sharing a string receive each other's messages.

  • Never inherit a parent's ID. TypeScript inherits static fields, so a subclass without its own ID silently reuses the parent's string and collides with it. Declare a fresh ID in every subclass.

The three roles

Role

Base class

Verbs

Result

Intent

Event

PostboyGenericMessage

fire, sub, once

none

"Something happened"

Callback

PostboyCallbackMessage<T>

fireCallback

Observable<T>

"I need a result back"

Executor

PostboyExecutor<T>

exec

T, synchronously

"Run this now and return the value"

In depth: Events, Callbacks, Executors.

When to use which

  • Use an event when zero or more listeners may react and no result is expected: domain events, UI notifications, state broadcasts.

  • Use a callback when the sender depends on a response that arrives asynchronously: queries, remote calls, long-running operations with progress.

  • Use an executor when the result must be available immediately and synchronously: mapping, validation, formatting, cache lookups.

  • Do not use an executor for asynchronous work. exec returns the value directly, so a Promise would come back unresolved; model async work as a callback message instead.

Naming conventions

Postboy does not enforce names. A CQRS-inspired convention scales well:

Suffix

Base class

Contract

…Event

PostboyGenericMessage

pure notification; no result; any number of subscribers

…Query

PostboyCallbackMessage<T>

read request; always finishes with data

…Command

PostboyCallbackMessage<T> or PostboyGenericMessage

instruction; callback form when it returns a value, generic form when it does not

…Executor

PostboyExecutor<T>

synchronous operation returning immediately

Message lifecycle

From construction to delivery, every message passes the same stages:

  1. Construction — the class is instantiated with its payload; metadata can be attached with setMetadata.

  2. Dispatch — the sender calls fire, fireCallback, or exec. Unregistered IDs throw on fire and exec; a locked type is silently not dispatched.

  3. Middleware — every dispatch runs the pipeline's before(...) hook; an Interrupt decision throws CancelError and the message is dropped before delivery.

  4. Resolution — the bus resolves the registered subject or handler by id.

  5. Delivery — subscribers receive the instance; a callback responder calls finish(...); an executor handler returns its value. The after(...) hook runs once the result is known.

  6. Outcome — side effects (event), a completed Observable<T> (callback), or the returned value (executor).

Middleware details: Middleware.

Pitfalls

  • Reusing one ID string across two classes — messages cross wires with no warning.

  • Letting a subclass inherit a parent's ID instead of declaring its own.

  • Awaiting postboy.exec(...) — it returns T, not a Promise.

  • Mutating metadata after dispatch — enrich at construction or in middleware, not once the message is in flight.

Next steps

07 September 2026