Middleware Anti-patterns (What to Avoid)
Summary
Middleware is a powerful interception layer around Postboy operations. Anti-patterns happen when middleware becomes:
invisible business logic,
an unbounded resource sink,
a source of order-dependent behavior,
or a place where failures and cancellations become impossible to diagnose.
This page lists the most common anti-patterns, their symptoms, why they are harmful, and what to do instead.
When to use / When not to use (quick reminder)
Middleware is appropriate for
observability (logging/tracing/metrics),
cross-cutting policies (guards),
operational constraints (maintenance/read-only).
Middleware is not appropriate for
business workflow logic,
implicit message rewriting,
long-running orchestration.
Anti-pattern 1: “God middleware” (does everything)
What it looks like
One middleware:
logs,
validates,
authorizes,
retries,
transforms payloads,
opens connections,
and contains app-specific branching logic.
Symptoms
Hard to test.
Hard to reason about failures.
Small change breaks unrelated behavior.
Why it’s harmful
Mixed responsibilities cause unintended coupling.
Debugging becomes “read the whole middleware”.
Do instead
Split into multiple middleware instances by concern.
Keep middleware small and composable (guard vs logging vs metrics).
Centralize shared utilities in helpers, not in a single mega-middleware.
Anti-pattern 2: Business logic hidden in middleware
What it looks like
Middleware decides:
which command should run,
whether to redirect to another message,
how to compute results,
or which domain invariants apply.
Symptoms
Domain flow is not visible in handlers/executors.
Developers search in handlers but the real behavior is elsewhere.
“Spooky action at a distance” when a new middleware is added.
Why it’s harmful
It makes the domain layer non-authoritative.
It increases cognitive load and reduces maintainability.
Do instead
Put domain logic in handlers/executors.
Keep middleware for policies and observation only.
If you must enforce domain constraints globally, make them explicit and documented as a policy (and keep them narrowly scoped).
Anti-pattern 3: Relying on after() for cleanup (leaks on cancel)
What it looks like
Middleware allocates per-operation state in before and only cleans it in after.
Symptoms
Memory grows over time.
Leaks appear specifically when operations are cancelled by guards.
Maps keyed by message id never shrink.
Why it’s harmful
afteris not guaranteed to run on cancellation.Cancellations are expected behavior for guards.
Do instead
Avoid per-operation allocations when possible.
Use bounded state (TTL/LRU) for tracking.
Put long-lived cleanup in
dispose.Treat “no after()” as a normal outcome.
Anti-pattern 4: High-cardinality metrics labels
What it looks like
Metrics tagged with:
message id,
user id,
request ids,
dynamic payload values.
Symptoms
Metrics backend slows down or becomes expensive.
Dashboards become unusable.
Cardinality warnings appear.
Why it’s harmful
High-cardinality series explode storage and CPU costs.
Do instead
Tag metrics with low-cardinality dimensions:
stage
stable message label (type/category)
outcome (success/cancel)
Keep message id in logs, not in metrics.
Anti-pattern 5: Overbroad canHandle (middleware runs everywhere)
What it looks like
canHandle returns true for everything, and filtering is attempted inside before or after.
Symptoms
Middleware overhead is present even where it’s useless.
Unexpected logs/metrics from unrelated messages.
Increased latency on hot paths.
Why it’s harmful
canHandleis the intended filter gate.Running everywhere increases the blast radius of bugs.
Do instead
Filter stage-first in
canHandle(Publish vs Callback vs Execute).Then filter by message subset (type/category).
Keep
canHandlecheap and side-effect free.
Anti-pattern 6: Hidden ordering dependencies
What it looks like
Middleware A assumes middleware B has already run (or will run later), but this is not documented or enforced.
Symptoms
Changing registration order changes behavior.
Cancellations become inconsistent (“sometimes cancelled by A, sometimes by B”).
Tracing/logging loses correlation intermittently.
Why it’s harmful
Middleware is evaluated in registration order; order is a real part of behavior.
Implicit dependencies are fragile.
Do instead
Keep middleware independent whenever possible.
If ordering matters, explicitly document it:
in a “middleware chain” module
in the middleware docs
Prefer combining tightly coupled behaviors into a single middleware only when separation introduces unavoidable ordering coupling.
Anti-pattern 7: Throwing arbitrary errors instead of using Interrupt for policy blocks
What it looks like
Middleware uses thrown errors as control flow for “forbidden” decisions instead of returning Interrupt.
Symptoms
Errors look like failures rather than intentional policy blocks.
Inconsistent diagnostics and handling across the app.
Harder to build consistent “blocked” responses.
Why it’s harmful
Policy blocks are expected outcomes; they should be distinguishable from failures.
Do instead
For policy decisions: return Interrupt.
For unexpected failures in middleware: throw (and treat as a real error).
Ensure cancellation reasons are explicit and safe.
Anti-pattern 8: Payload logging without safety controls
What it looks like
Middleware logs full message objects by default.
Symptoms
Secrets/PII in logs.
Very large logs, high cost.
Performance regressions.
Why it’s harmful
Security risk and compliance issues.
Increased runtime overhead.
Do instead
Default to minimal structured fields:
stage, message label, message id, outcome, duration
Add an explicit “debug payload logging” flag and keep it off by default.
Redact sensitive fields if payload logging is enabled.
Anti-pattern 9: Middleware that mutates messages or changes semantics
What it looks like
Middleware modifies message properties before handlers run, effectively changing meaning.
Symptoms
Handlers behave differently depending on which middleware is attached.
Bugs appear only in some environments.
Replays/tests are inconsistent.
Why it’s harmful
It breaks the principle that a message is an explicit unit of intent.
It makes the system non-transparent.
Do instead
Keep messages immutable by convention.
If transformation is required, do it explicitly in app code:
create a new message
execute/publish it intentionally
If you must attach metadata, store it outside the message in a trace/context mechanism you own.
Diagram: anti-pattern vs recommended split
@startuml title Anti-pattern: hidden domain logic vs recommended separation
participant "Postboy bus" as Bus participant "Middleware" as MW participant "Handlers (domain)" as H
== Anti-pattern == Bus -> MW: before() note over MW: decides business flow\nmutates message\ndoes orchestration Bus -> H: handler sees modified intent
== Recommended == Bus -> MW: before() (policy + observability) Bus -> H: domain logic is explicit here Bus -> MW: after() (observability)
@enduml
Quick self-audit checklist
If any answer is “yes”, you may be in anti-pattern territory:
Does middleware contain business decisions that should be visible in handlers?
Does middleware allocate per-operation state that only after() cleans up?
Do metrics labels include message ids or user ids?
Does changing middleware order change correctness, not just observability?
Do you log payloads by default?
Does middleware modify message objects?
Next steps
[Best practices: how to design middleware safely][Recipes: copy-safe patterns][How-to: safely manage middleware resources][Interrupt/cancel semantics and diagnostics][Troubleshooting: unexpected cancels, missing after(), and ordering issues]