Middleware Stages: Publish / Callback / Execute
Summary
Postboy middleware runs around message processing at specific stages. A stage tells you what kind of operation is happening right now and therefore what kind of checks or observations make sense.
Postboy defines three stages:
Publish — fire-and-forget message publishing (events).
Callback — request/response style message flow (asynchronous result).
Execute — synchronous executor invocation (immediate result).
Stages are the primary dimension for scoping middleware behavior (often even more important than filtering by message type).
When to use / When not to use
Use stages when you
Want different policies for different mechanisms (e.g., “block execute in readonly mode, but allow publish”).
Need accurate observability (timing and outcomes differ per stage).
Want to keep middleware minimal: stage-first filtering prevents running heavy logic unnecessarily.
Don’t overuse stage branching when
Your middleware is already narrowly scoped by message types and stage doesn’t add clarity.
You are encoding business workflow decisions (those belong in handlers/executors).
Core idea: stage describes the operation boundary
A stage is not “where the message is declared” or “what folder it lives in”. A stage is “which Postboy API path is being used right now”.
If you imagine Postboy as a bus with multiple “lanes”:
Publish lane: broadcast an event, no result.
Callback lane: publish a request and later produce a result.
Execute lane: run a synchronous operation and return a value now.
Middleware sees which lane is currently active.
Stage definitions
1) Publish stage
What it represents: fire-and-forget broadcasting.
Typical usage:
Logging: “event X was fired”
Metrics: count published events per type
Policy: reject certain event types in certain environments (rare; usually events should be safe)
What to expect conceptually:
No meaningful “result” value for middleware
after.The system may have many subscribers; publish is generally about fan-out.
Design note: Publish is usually the least appropriate place for strict business policies, because events often represent “something already happened”. If you need a hard guard, consider guarding the command/executor that causes the event instead.
2) Callback stage
What it represents: request/response message flow where a result is produced asynchronously.
Typical usage:
Observability around request lifecycles: start/stop timing when a callback completes
Policy gates: block certain callback requests early (e.g., “feature disabled”)
Standardized tracing: attach correlation ids for request-like flows
What to expect conceptually:
A callback has a “finish” moment; completion is a first-class part of this mechanism.
“Result” exists as an idea, but it may not be immediate at the time the request is fired.
Your middleware must be explicit about what it measures: “request accepted” vs “request completed”.
Practical guidance: If you measure duration, define which duration you mean:
“Time to accept callback request” (cheap, immediate)
“Time to complete callback result” (end-to-end)
Your middleware should document which one it implements.
3) Execute stage
What it represents: synchronous executor call that returns a value immediately (or throws).
Typical usage:
Policy: enforce “no dangerous executors in production” or “readonly mode”
Logging: record operation name + arguments summary (be careful with sensitive data)
Metrics: duration, error rate, call counts per executor type
What to expect conceptually:
A concrete result is available for
after(context, result)on success.Timing is straightforward (start in
before, stop inafter).
Design note: Execute is the best stage for strict guards because it typically represents an intentional “do something now” operation.
A stage-centric flow diagram
@startuml title Stages as lanes: Publish vs Callback vs Execute
actor User participant "Postboy" as Bus participant "Middleware chain" as MW participant "Subscribers/Handlers" as H
== Publish == User -> Bus: fire(message) Bus -> MW: before(Publish, message) Bus -> H: broadcast to subscribers Bus -> MW: after(Publish, message)
== Callback == User -> Bus: fireCallback(message) Bus -> MW: before(Callback, message) Bus -> H: deliver request to subscriber(s) note over H: subscriber eventually finishes the callback Bus -> MW: after(Callback, message, result?) 'meaning depends on implementation Bus --> User: result stream completes
== Execute == User -> Bus: exec(executor) Bus -> MW: before(Execute, executor) Bus -> H: run handler / function H --> Bus: result value Bus -> MW: after(Execute, executor, result) Bus --> User: returns result
@enduml
Scoping strategy: stage-first, then message filters
A practical pattern for middleware:
First check stage:
If stage is not relevant, return false in
canHandle.
Then check message category/type:
Apply to a subset of messages.
This keeps middleware:
predictable
cheaper at runtime
easier to reason about
Common scenarios mapped to stages
Logging
Publish: “event emitted”
Callback: “request started / request completed” (be explicit)
Execute: “operation invoked / operation succeeded”
Metrics
Publish: counters (volume)
Callback: counters + end-to-end latency (if you track completion)
Execute: latency + error rate (synchronous boundaries)
Guards / policies
Publish: rarely (mostly allow)
Callback: often (feature flags, access policy)
Execute: often (critical infrastructure operations, read-only mode)
Pitfalls / Troubleshooting
“My middleware sees Callback stage but no usable result”
Cause: callback “result” is not necessarily an immediate return value at the point of firing. Fix:
Decide what you want to observe:
acceptance of request
completion of response
Implement accordingly and document the semantics.
“I blocked Publish and now the system is inconsistent”
Cause: blocking events may hide “facts” that other components rely on. Fix:
Prefer guarding the cause (command/executor/callback request) rather than blocking events after the fact.
“My stage checks are scattered across before/after”
Fix:
Centralize stage filtering in
canHandleso both hooks remain simple.
Best practices
Use stage as your first scoping tool.
For Callback stage, explicitly document what “after” represents for your middleware (accepted vs completed).
For Execute stage, keep guards early and deterministic; treat sensitive data carefully in logs/metrics.
Next steps
[Middleware lifecycle and contract (before/after/canHandle/dispose)][Interrupt/cancel semantics: what happens when middleware blocks an operation][How-to: filter middleware to specific messages][How-to: logging/tracing middleware][Middleware API reference]