Best Practices
A handful of habits covers almost every postboy pitfall: unique message IDs, one owner per registration, registration before subscription, and explicit teardown. Each rule below links to the topic that covers it in depth.
Keep one registration per message ID
The bus routes by the static ID string, and the store keeps exactly one registration per ID. Re-registering an ID — from another feature, another namespace, or a second up() — overrides the earlier registration with only a console warning. Register each message class once, in one place, and treat a duplicate registration as a bug. See Registration and Caveats.
Group a feature's registrations in one registrator
Put all of a feature's record* calls into a single PostboyAbstractRegistrator subclass so that one down() disconnects everything the feature owns. Scattered Connect* calls are individually correct but collectively untraceable — teardown gets skipped and registrations leak. See Lifecycle Management.
Register first, subscribe after
fire on an unregistered ID throws, and sub on an unregistered ID throws as well. Register in up() or module initialization, and subscribe only afterwards — dependent services get this ordering for free via registerServices. See How It Works and Lifecycle Management.
Match the subject type to the semantics
Use recordReplay when late subscribers need the latest value on subscribe, and recordBehavior for state that must always have a current value. Plain recordSubject is for events where missing history is fine. See Lifecycle Management and Events.
Prefer registrators over bare Connect* in application code
Bare postboy.exec(new ConnectMessage(...)) leaves the ID untracked; you must remember the DisconnectMessage yourself. Reserve it for app-lifetime wiring, prototypes, and tests. Anything with a feature lifetime belongs in a registrator or namespace. See Lifecycle Management and Namespaces.
Put validation in before, side effects in after
Middleware runs for every fire, fireCallback, and exec. Return an Interrupt decision from before to cancel an invalid operation before it runs; use after for logging and metrics, where you can also see the executor's result. See Middleware and Middleware Recipes.
Catch CancelError where cancellation is expected
An interrupted operation throws CancelError from fire, fireCallback, and exec. If cancellation is a normal outcome for a call site, catch it there and inspect error.details (stage, middleware, messageId, reason). Distinguish it with error instanceof CancelError or error.name === 'PostboyCancelError'. See Middleware Cancellation.
Guard calls whose registration is not guaranteed
fire throws for an unregistered message ID and exec throws for an unregistered executor ID. Where the wiring is owned elsewhere (a lazy feature, a plugin), wrap the call in try/catch or make the dependency explicit. See Executors and Caveats.
Treat locked messages as a no-op
A type locked via LockMessage is silently skipped by fire and fireCallback: registration stays intact and exec is unaffected. Do not build error handling around locks — treat absence of delivery as an expected state. See Infrastructure Messages.
Tear down deliberately
Subjects are not auto-completed by themselves: call registrator.down(), exec(new EliminateNamespace(name)), or postboy.dispose() when the owner is destroyed, and pair every up() with a down(). dispose() also disposes every registered middleware, and the bus must be re-created afterwards. See Lifecycle Management and Namespaces.
At a glance
One registration per message ID; duplicates override.
One registrator per feature;
down()cleans everything.Register in
up()or module init; subscribe after.recordReplayfor latest value,recordBehaviorfor state.Validation in
before, side effects inafter.Catch
CancelErroronly where cancellation is expected.Guard
exec/firewhere registration is not guaranteed.Locked means no-op, not error.
Always tear down:
down(),EliminateNamespace, ordispose().
Next steps
Lifecycle Management and Namespaces — the lifecycle model.
Middleware, Middleware Cancellation — pipeline rules.
Caveats — the full pitfalls catalog.
Testing — verifying lifecycle behavior with
@artstesh/postboy-testing.