Postboy Help

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 in after).

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:

  1. First check stage:

  • If stage is not relevant, return false in canHandle.

  1. 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 canHandle so 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]

29 июня 2026