Middleware in v3.5+: What Changed and How to Adopt It Safely
Summary
Starting with Postboy v3.5, the library introduces middleware: a pipeline interception mechanism around message processing. Middleware enables cross-cutting behaviors—logging, tracing, metrics, and policy enforcement—without modifying individual handlers/subscribers.
This page explains:
what middleware adds to the system,
what you should consider when upgrading/adopting it,
and safe rollout steps.
Applies to
Postboy v3.5 and newer.
If you are on an older version line, middleware APIs are not available.
What changed in v3.5+
New capability: pipeline interception
Middleware introduces a consistent “around” hook model for message processing:
filter:
canHandle(context)pre-hook:
before(context)→ Continue / Interruptpost-hook:
after(context, result?)(success-only)cleanup:
dispose()
This enables system-wide concerns to be implemented once and applied broadly.
New vocabulary: stages
Middleware executes per operation stage:
Publish
Callback
Execute
Stage is a primary scoping dimension for middleware.
New failure mode: cancellation by middleware
Middleware can explicitly cancel an operation by returning Interrupt in before. This is a deliberate control-flow feature for policies and guards.
Cancellation:
happens before handler/subscriber execution,
short-circuits the middleware chain,
raises a cancellation error with diagnostic metadata (stage, message id, middleware name, reason).
Compatibility and behavior notes (what to watch for)
1) Middleware changes “what can stop an operation”
Before v3.5, your system’s “stop points” were typically:
handler/subscriber errors,
missing registrations,
application-level checks.
In v3.5+, middleware can stop execution earlier, before handlers see the message. This is powerful for policy enforcement, but it also introduces a new class of issues if a guard is too broad.
2) after() is not guaranteed
The after hook runs only after a successful completion. It does not run on cancellations and may not run on failures depending on where failures occur.
If you add middleware that stores per-operation state (timing, correlation maps), ensure it cannot leak if after does not run.
3) Ordering becomes part of runtime behavior
Middleware runs in registration order. The first Interrupt “wins”. If you attach multiple guard or observability middleware instances, ordering affects:
which middleware cancels,
whether attempt logs exist for cancelled operations,
which spans/metrics are recorded.
Adoption guide (safe rollout steps)
Step 1: Start with “read-only” middleware
First middleware should be passive (no Interrupt):
structured logging (attempt + success),
metrics (attempts + success latencies),
tracing (start/end spans for Execute stage).
Why:
You learn how stages map to your app’s operations.
You can confirm middleware wiring without changing behavior.
Step 2: Scope aggressively (stage-first)
Use canHandle to keep blast radius small:
start with Execute only
then expand to Callback if you need request-level visibility
use message-type/category filters to avoid instrumenting internal/infrastructure operations (if any)
Step 3: Add guards only when you have diagnostics
Before introducing Interrupt-based policies, ensure:
middleware names are stable and searchable
cancellation reasons are short and safe
you have logs/metrics to see “attempts” vs “successes” vs “cancellations”
Step 4: Roll out policies in “monitor mode” first (optional)
If you are unsure about the impact of a policy:
log “would interrupt” decisions without actually interrupting
confirm the set of affected operations
then enable Interrupt
Step 5: Update tests and teardown practices
If you attach middleware in tests:
ensure middleware is removed/disposed between tests
avoid global attachment unless your harness resets the bus
If you add timing/correlation maps:
add tests ensuring they do not grow unbounded (especially under cancellation scenarios).
Migration notes (from pre-3.5 thinking)
Where previous patterns map now
Ad-hoc logging inside handlers → move to logging middleware.
Scattered feature-flag checks → consolidate in guard middleware (Execute/Callback).
Cross-cutting metrics in each handler → centralize in metrics middleware.
What should remain in handlers
domain workflow logic,
domain validation that must return typed responses,
use-case-specific orchestration.
Middleware is infrastructure, not domain.
Diagram: pre-v3.5 vs v3.5+ flow (conceptual)
@startuml title Pre-v3.5 vs v3.5+ (conceptual)
actor Caller participant "Postboy" as Bus participant "Middleware chain" as MW participant "Handlers/Subscribers" as H
== Pre-v3.5 == Caller -> Bus: start operation Bus -> H: run handler/subscribers H --> Caller: result/stream
== v3.5+ == Caller -> Bus: start operation Bus -> MW: before (can interrupt) alt interrupted Bus --> Caller: cancelled (error) else continue Bus -> H: run handler/subscribers Bus -> MW: after (success-only) Bus --> Caller: result/stream end
@enduml
Common upgrade pitfalls (and fixes)
Pitfall: “New middleware broke production by blocking messages”
Cause:
guard middleware
canHandletoo broad or stage misunderstood.
Fix:
stage-first scoping
add message-type filters
add “monitor mode” before enabling Interrupt
Pitfall: “Metrics/logging missing for cancelled operations”
Cause:
you only record in
after.
Fix:
record attempts in
beforeplace observability middleware before guards if you want to see cancelled attempts
Pitfall: “Memory usage increased after adding timing middleware”
Cause:
per-operation maps cleaned only in
after.
Fix:
use bounded state (TTL/LRU)
do not assume
afteralways runsclean long-lived resources in
dispose
Next steps
[Middleware: overview][Middleware lifecycle and contract][Middleware stages][Decision & Cancellation reference][Best practices][Troubleshooting]