Postboy Help

Middleware Best Practices

Summary

Middleware is an infrastructure extension point around Postboy’s message processing pipeline. It is powerful because it can observe and block operations centrally. It is also risky because it can introduce hidden behavior and cross-cutting side effects.

This page lists best practices that keep middleware:

  • predictable,

  • debuggable,

  • safe (no leaks),

  • and easy to evolve.

When to use / When not to use

Use middleware for

  • Observability: logging, tracing, metrics.

  • Policy enforcement: guards, feature flags, environment constraints.

  • Operational safety rails: read-only mode, maintenance mode, global rate limits (if applicable).

Avoid middleware for

  • Business workflow logic (“what should happen next?”).

  • Domain validation that must return a typed error/result to the caller.

  • Hidden data mutation or message rewriting that changes meaning without explicit ownership.

Best practices (the checklist)

1) Keep one concern per middleware

Do

  • Logging middleware does logging.

  • Guard middleware does policy checks.

  • Metrics middleware does metrics.

Why

  • Easier testing and troubleshooting.

  • Clear ownership and fewer accidental interactions.

Smell

  • A middleware that logs, validates, retries, transforms data, and opens connections.

2) Filter aggressively in canHandle (stage-first)

Do

  • Use canHandle(context) to scope middleware.

  • Filter by stage first, then by message subset.

Why

  • canHandle is the intended filter gate.

  • Avoids unnecessary overhead for unrelated operations.

Smell

  • Stage/type checks scattered across before and after.

3) Prefer “attempt” logging/metrics in before, “success” in after

Do

  • before: record attempts (and start timestamps/spans).

  • after: record successful completion (and durations).

Why

  • after does not run on cancellation.

  • You get consistent signals for attempts vs successes.

Smell

  • Assuming after is always called.

4) Treat cancellation as a first-class outcome

Do

  • Decide explicitly:

    • Do you want to observe cancelled attempts?

    • Where do you count cancellations (middleware vs boundary handling)?

  • Keep cancellation reasons short and searchable.

Why

  • Interrupt is an intended control-flow mechanism, not an “exceptional edge case”.

Smell

  • “Random” cancellations with no clear reason or no metadata.

5) Make ordering explicit when it matters

Middleware runs in registration order. The first Interrupt wins.

Do

  • Document ordering rules in code comments or module docs if you depend on them.

  • Put guard middleware early to fail fast.

  • Decide whether observability middleware should be:

    • before guards (see cancelled attempts), or

    • after guards (log only successful operations).

Smell

  • Swapping middleware registration order changes behavior unexpectedly.

6) Keep before() fast, deterministic, and side-effect minimal

Do

  • Evaluate policy checks quickly (in-memory checks, flags, roles already present).

  • Avoid long-running work inside before.

Why

  • before is on the hot path and runs before every guarded operation.

Smell

  • before performing network calls, file I/O, or heavy serialization.

7) Use structured, low-cardinality observability data

Do

  • Log structured fields: stage, message label, message id, outcome, duration.

  • Use stable message labels (type name/category mapping).

  • Avoid message id as a metric label (use it in logs only).

Why

  • High-cardinality metrics are expensive and can break your monitoring system.

Smell

  • Metrics tagged with request ids, message ids, user ids.

8) Do not log payloads by default

Do

  • Log only minimal identifiers by default.

  • Provide an explicit “debug mode” (feature flag) if you must log payloads.

Why

  • Security (secrets/PII) and performance.

Smell

  • Logging full message objects “for convenience”.

9) Design resource ownership and cleanup from day one

Do

  • Implement dispose if you allocate resources.

  • Make dispose safe and idempotent.

  • Avoid per-operation allocations that require after for cleanup.

Why

  • Cancellations skip after; tests and modules need deterministic cleanup.

Smell

  • Subscriptions/timers created repeatedly and never released.

10) Prefer small, explicit abstractions over “smart” middleware

Do

  • Keep middleware transparent: it should be easy to predict what it does.

  • Prefer explicit “policy middleware” rather than implicit behavior changes.

Why

  • Middleware already adds an indirection layer; keep it understandable.

Smell

  • Middleware that silently rewrites behavior based on many hidden conditions.

Decision guidance: what belongs where?

Middleware is the right place for

  • “Always around” concerns (log, measure, trace).

  • “Always enforced” policies (guards).

  • Infrastructure-only behaviors.

Handlers/executors are the right place for

  • Domain behavior and orchestration.

  • Domain validation that returns typed errors/results.

  • Business decisions that should be visible in domain code.

@startuml title Recommended responsibility split

participant "App code" as App participant "Postboy bus" as Bus participant "Middleware (infra)" as MW participant "Handlers/Subscribers (domain)" as H

App -> Bus: start operation Bus -> MW: before/canHandle (policies + observability) Bus -> H: domain logic (explicit) Bus -> MW: after (observability) Bus --> App: result / completion

@enduml

Troubleshooting: common symptoms mapped to best practices

Symptom: “It’s hard to understand why something happened”

Likely causes:

  • too many concerns in one middleware

  • hidden ordering dependencies

  • poor cancellation diagnostics

Fix:

  • split middleware by concern

  • document ordering

  • add stable policy ids/reasons

Symptom: “Performance degraded after adding middleware”

Likely causes:

  • broad canHandle

  • heavy work in before

  • payload logging

Fix:

  • stage-first filtering

  • remove heavy work from hot paths

  • structured minimal logs

Symptom: “Memory grows over time”

Likely causes:

  • per-operation maps not bounded

  • subscriptions/timers not cleaned in dispose

Fix:

  • bound state (TTL/LRU)

  • implement dispose and ensure teardown is wired

Next steps

  • [Anti-patterns: what to avoid in middleware]

  • [Recipes: copy-safe middleware patterns]

  • [How-to: safely manage middleware resources (dispose)]

  • [How-to: logging and tracing]

  • [How-to: measure duration and collect metrics]

29 июня 2026