Postboy Help

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:

  • before is not called

  • after is not called

  • the 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:

  1. A message that SHOULD match:

  • middleware before runs

  • and after runs on success

  1. A message that SHOULD NOT match:

  • middleware does not run at all (no logs, no metrics, no decisions)

  1. A message that matches stage but NOT message type:

  • middleware should still not run

Verification (expected outcome)

Expected behavior

  • Matching operations:

    • canHandle returns true

    • middleware participates and can log/measure/interrupt (depending on design)

  • Non-matching operations:

    • canHandle returns false

    • middleware 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:

  1. canHandle always returns true.

  2. Your filter checks are placed inside before instead of canHandle.

  3. 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:

  1. You filtered for the wrong stage (Execute vs Callback vs Publish).

  2. Your type/category check never matches.

  3. You attached middleware to a different bus instance.

Fix steps:

  • Temporarily make canHandle return 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 in before).

  • Filter by stage first, then by message type/category.

  • Keep canHandle cheap 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]

29 июня 2026