Postboy Help

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

  • after is 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

  • canHandle is 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 canHandle cheap 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.

@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]

29 июня 2026