Message Lifecycle
Purpose
Every message in Postboy follows a predictable journey from creation to its final outcome. Understanding this lifecycle is essential for:
Debugging message flow
Implementing middleware correctly
Reasoning about when a message can be cancelled or blocked
Distinguishing the paths taken by different message types
The lifecycle applies uniformly to PostboyGenericMessage, PostboyCallbackMessage<T>, and PostboyExecutor<T>, though each type has its own specific completion behavior.
Lifecycle stages
A message passes through five distinct stages:
Creation
Publication
Subscription Resolution
Processing
Outcome
Each stage provides an opportunity for middleware to intervene or for the bus to alter the flow.
1. Creation
The message begins as an instance of a concrete class that extends PostboyMessage. At this point:
The message
idis established via the class's staticIDfieldPayload data is set through the constructor
Metadata may be attached using
setMetadata()
Example:
The message is now ready but has not yet entered the bus.
2. Publication
Publication is the act of handing the message to Postboy. Depending on the message type, this is done via:
postboy.fire(message)for generic messagespostboy.fireCallback(message)for callback messagespostboy.exec(executor)for executors
At this moment:
The bus validates that the message type is not locked (if locked, publication is rejected)
The message identity is recorded for subscription resolution
Middleware configured for pre‑processing is invoked
If pre‑processing middleware decides to cancel the flow, the message never reaches subscribers or handlers. Otherwise, it proceeds to the next stage.
3. Subscription Resolution
For generic and callback messages, the bus resolves which subscribers are registered for the message's id. This resolution considers:
Subscriptions bound directly to the message type
Namespace scoping (if applicable)
Whether the subscription is active or temporarily paused
For executors, this stage resolves the registered handler function for the given executor id.
If no subscriber or handler is found, the message is effectively ignored (a silent no‑op, though middleware may still observe it).
4. Processing
This is where the actual work defined by subscribers or handlers takes place.
For PostboyGenericMessage
Each subscriber receives the message and may perform side effects. The order of subscriber invocation is not guaranteed unless explicitly configured. Subscribers can be synchronous or return an Observable/Promise.
For PostboyCallbackMessage<T>
The designated handler processes the message and must finish it by calling message.finish(result). The response is then delivered back to the caller through the Observable returned by fireCallback().
For PostboyExecutor<T>
The registered executor function is invoked with the executor instance. It may return a result synchronously or asynchronously. The bus waits for completion (or cancellation) before resolving the Promise from exec().
During processing, in‑flight middleware can still observe or even cancel the operation. For example, a timeout middleware could abort a long‑running executor.
5. Outcome
The lifecycle ends in one of three ways:
Completion
The message was handled successfully, and the flow terminates normally.
For generic messages: all subscribers have been invoked (or the observable has completed).
For callback messages:
finish()was called and the response emitted.For executors: the handler returned a value (or
void) without error.
Cancellation
The message was interrupted by middleware or an explicit bus operation before reaching its natural end. Cancellation can happen:
During pre‑processing (before subscription resolution)
During processing (e.g., a timeout or manual intervention)
When cancelled, subscribers or handlers are not executed (or, if already running, they may be interrupted depending on the implementation).
Blocking (Prevention)
A message type can be locked administratively. When locked, any attempt to publish it is rejected immediately. This is useful for:
Temporarily disabling a feature
Preventing duplicate processing during maintenance
Controlling message flow during deployment
Locking operates at the type level, not the instance level, and is managed via Postboy's internal registry.
Lifecycle by message type (summary)
Stage |
| ||
|---|---|---|---|
Publication |
|
|
|
Resolution | Finds subscribers by | Finds handler by | Finds executor function by |
Processing | One‑to‑many broadcast | Single handler, calls | Direct invocation of registered function |
Outcome | Completion or cancellation | Completes with response or cancellation | Returns result or cancellation |
Interaction with middleware
Middleware participates in the lifecycle at two key points:
Before subscription resolution – can cancel or modify the message
During processing – can observe, enrich, or cancel the flow
Because all messages inherit from PostboyMessage, middleware can operate on any message type without needing to know its specific contract.
Why this lifecycle matters
A clearly defined lifecycle:
Makes middleware behavior predictable
Helps developers understand when and why a message might not reach its target
Provides clear extension points for instrumentation, logging, and error handling
Separates concerns: creation logic lives in application code, flow control in middleware, and business logic in subscribers/handlers
In short
The Postboy message lifecycle takes a message from its creation, through publication and subscription resolution, into processing, and finally to one of three outcomes: completion, cancellation, or blocking. This structured path ensures that all communication remains consistent, observable, and controllable—regardless of which message contract you choose.