How-to: Enable Middleware Only for a Subset of Messages
Problem
You want to attach a middleware to Postboy, but you do not want it to run for every message. Instead, it should apply only to:
specific message types (e.g., only commands),
specific stages (Publish vs Callback vs Execute),
specific message instances (by id),
or a combination of the above.
Prerequisites
You already know how to attach middleware globally to a Postboy bus.
You understand the middleware lifecycle hooks:
canHandle/before/after/dispose.You can identify messages by at least one stable attribute:
stage (always available),
message type/category (recommended),
message id (useful for diagnostics and tests).
Approach
Use canHandle(context) as a filter. If canHandle returns false:
beforeis not calledafteris not calledthe middleware has zero effect for that operation
This is the intended, cheap way to scope middleware.
Steps
1) Decide your scoping rules (write them down)
Before writing conditions, define what you actually mean. Examples:
“Run only for Execute stage”
“Run only for Callback stage and only for Query messages”
“Run only for two specific executor types”
“Run only for message ids that match a test scenario”
This prevents accidental “half-scoped” middleware that runs more broadly than intended.
2) Filter by stage first
Stage filtering is usually the highest-value filter:
It is cheap.
It prevents the middleware from running on unrelated lanes.
Rule of thumb:
Guards for synchronous operations: Execute stage.
Observability for requests: Callback stage (but define what you measure).
Simple event counters: Publish stage.
3) Add message-type/category filtering
After stage filtering, narrow down to the intended message family.
Recommended strategies (choose one):
Strategy A: Filter by message class/type
If your application uses distinct classes for messages, filter by instance checks or known type metadata.
Benefits:
Stable.
Expressive.
Tradeoff:
Requires your message taxonomy to be consistent.
Strategy B: Filter by “role” (CQRS-style)
If you enforce naming/roles (Command/Query/Event/Executor), filter by those conventions.
Benefits:
Good for large systems.
Aligns with documentation and conventions.
Tradeoff:
Convention-based filtering can be brittle if the project does not enforce it.
4) (Optional) Filter by message id (fine-grained, mostly for tests)
Filtering by message id is useful when:
you need deterministic targeting in integration tests,
you want to observe exactly one message instance without affecting others.
Tradeoff:
In production, message ids are usually too granular for long-lived middleware rules.
Id-based filters can silently “stop working” if the ids are regenerated.
5) Verify the filter does what you think
Run three checks:
A message that SHOULD match:
middleware
beforerunsand
afterruns on success
A message that SHOULD NOT match:
middleware does not run at all (no logs, no metrics, no decisions)
A message that matches stage but NOT message type:
middleware should still not run
Verification (expected outcome)
Expected behavior
Matching operations:
canHandlereturns truemiddleware participates and can log/measure/interrupt (depending on design)
Non-matching operations:
canHandlereturns falsemiddleware is skipped entirely
no overhead besides evaluating the cheap predicate
Suggested debug signal (temporary)
While validating scoping rules, add a temporary debug log:
stage
message type/category
message id
canHandle decision
Remove or disable it after the filter is proven.
Troubleshooting
“Middleware still runs for everything”
Likely causes:
canHandlealways returns true.Your filter checks are placed inside
beforeinstead ofcanHandle.You filtered by the wrong attribute (e.g., confusing message type vs executor type).
Fix steps:
Move all scoping logic into
canHandle.Ensure the first filter is stage-based.
Add a temporary log line that prints the values you compare.
“Middleware never runs”
Likely causes:
You filtered for the wrong stage (Execute vs Callback vs Publish).
Your type/category check never matches.
You attached middleware to a different bus instance.
Fix steps:
Temporarily make
canHandlereturn true and confirm hooks fire.Then reintroduce filters one by one (stage first, then type).
Confirm the code path uses the same Postboy instance.
“I filtered by message id and it’s flaky”
Cause:
Message ids are per-instance and may change between runs/tests.
Fix:
Prefer filtering by message type for production.
For tests, capture the target message id at Arrange time and inject it into the middleware.
Diagram: filtering point in the middleware lifecycle
@startuml title canHandle as the primary filter gate
participant "Postboy" as Bus participant "Middleware" as MW participant "Handlers/Subscribers" as H
Bus -> MW: canHandle(context)? alt false note over MW: skipped\n(no before/after) Bus -> H: run operation (no middleware impact) else true Bus -> MW: before(context) Bus -> H: run operation Bus -> MW: after(context, result?) end
@enduml
Best practices
Put all scoping in
canHandle(not inbefore).Filter by stage first, then by message type/category.
Keep
canHandlecheap and side-effect free.Avoid id-based scoping in production unless you have a strong reason (it is usually too granular).
Next steps
[Middleware stages: Publish / Callback / Execute][Middleware lifecycle and contract (before/after/canHandle/dispose)][How-to: implement a guard/policy middleware (Interrupt)][How-to: logging/tracing middleware without noise][Middleware API reference]