Middleware Recipes: Copy-Safe Patterns (Logging / Guards / Validation / Metrics)
Summary
This page contains “recipes”: small, practical middleware patterns you can copy and adapt. Each recipe includes:
context (when it’s useful),
assumptions,
tradeoffs,
common mistakes.
Recipes are intentionally framework-agnostic. They focus on Postboy stages (Publish / Callback / Execute) and the middleware contract (canHandle / before / after / dispose).
Recipe 1: Stage-first structured logging (“attempt” + “success”)
Context
You want consistent logs for operations without touching handlers. You want to correlate attempt and completion.
Assumptions
You have a logger that can log structured fields (key/value).
You can derive a stable message label (type name/category).
Pattern
In
canHandle: filter to relevant stages and message subset.In
before: log an “attempt” entry.In
after: log a “success” entry.Do not log payloads by default.
Tradeoffs
You will not automatically log cancellations in
after(because after does not run on cancel). If you need cancellation logs, log attempts inbeforeand/or log at the cancellation boundary.
Common mistakes
Logging full message objects (performance + secrets).
Not filtering by stage/message type, causing noisy logs.
Recipe 2: Guard middleware (“policy gate” using Interrupt)
Context
You must block certain operations globally (read-only mode, feature flag off, authorization policy).
Assumptions
You can decide “allowed vs blocked” synchronously in middleware.
You have a clear, stable reason string or policy id for diagnostics.
Pattern
In
canHandle: narrow to the stage(s) you intend to guard (often Execute and/or Callback).In
before: evaluate policy and return Interrupt when violated.
Tradeoffs
Guards are powerful and can surprise users if overused. Keep scope narrow and diagnostics clear.
Order matters: the first guard that interrupts “wins”.
Common mistakes
Guarding Publish stage broadly (can create hidden inconsistencies).
Making guard decisions depend on slow I/O or mutable global state without clear control.
Recipe 3: “Allowlist” middleware for sensitive executors
Context
You want to run only a known-safe set of synchronous operations in a restricted environment (e.g., production maintenance mode).
Assumptions
You can classify executors by a stable label (type name, category, explicit tag).
Pattern
Stage filter: Execute only.
Message filter: if executor label not in allowlist → Interrupt.
Tradeoffs
Requires keeping the allowlist in sync with the codebase.
Better for small/critical surfaces than for large, fast-changing domains.
Common mistakes
Using a high-cardinality or unstable label for the allowlist key.
Failing closed without proper diagnostics (“why was this blocked?”).
Recipe 4: Validation middleware (“fast reject” for obvious invalid input)
Context
You want to reject clearly invalid messages early (missing required fields, invalid enum values) in a consistent way.
Assumptions
Validation is purely syntactic/structural (not business workflow validation).
You can validate synchronously.
Pattern
Filter to the message subset you want to validate (usually commands/queries).
In
before: run validation checks.If invalid: Interrupt (and rely on cancellation diagnostics).
If valid: Continue.
Tradeoffs
If you need rich, typed validation errors as part of domain responses, consider handler-level validation instead of cancelling.
Cancellation is “all-or-nothing” and does not execute handlers.
Common mistakes
Putting domain rules into middleware (turns into hidden business logic).
Using validation middleware as a substitute for API-layer validation where user feedback must be explicit.
Recipe 5: Metrics middleware (attempts + latency for Execute)
Context
You want stable dashboards: operation volume and latency for synchronous executors.
Assumptions
You have a metrics sink (Prometheus/OpenTelemetry/statsd/etc.).
You can derive a stable message label.
Pattern
In
canHandle: filter to Execute stage (and optionally a message subset).In
before:increment attempts counter
store start time (keyed by message id, bounded)
In
after:record duration
increment success counter
Tradeoffs
Cancelled operations won’t reach
after.Avoid metric labels with high cardinality (never use message id as a metric label).
Common mistakes
Unbounded maps for start timestamps (leaks when after isn’t called).
Too many labels, causing cardinality explosion.
Recipe 6: “Trace span per operation” (thin tracing adapter)
Context
You want distributed tracing around Postboy operations.
Assumptions
Your app has a trace context mechanism.
You can start/end spans synchronously around the bus boundary (especially for Execute).
Pattern
In
before:start a span with tags: stage, message label, message id
In
after:mark span success and end
For Callback stage, explicitly define what you trace:
request accepted vs request completed
Tradeoffs
End-to-end callback completion tracing may require hooking into callback completion signals beyond simple before/after boundaries.
Common mistakes
Treating Callback “after” as always representing completion without verifying semantics.
Not propagating trace context across async boundaries (spans become disconnected).
Recipe 7: Resource-safe middleware template (cleanup-first design)
Context
Your middleware allocates resources (subscriptions, timers, caches) and must not leak.
Assumptions
You can keep long-lived resources under middleware ownership.
Pattern
Allocate long-lived resources once (constructor or first use).
Release them in
dispose.Keep per-operation state bounded; do not rely on
afterfor cleanup.
Tradeoffs
Slightly more boilerplate, much safer behavior.
Common mistakes
Creating subscriptions in
beforerepeatedly (leaks and duplicates).Clearing per-operation state only in
after(breaks on cancellation).
Recipe 8: Noise control (“only log slow operations”)
Context
You want useful logs without spamming: record only operations slower than a threshold.
Assumptions
You measure latency (typically Execute stage).
Pattern
Record start time in
before.In
aftercompute duration and log only if duration >= threshold.
Tradeoffs
You only see slow successes; cancellations and failures need separate visibility.
Common mistakes
Using this as the only observability (you lose visibility into frequent small errors/cancels).
Not filtering by message subset, resulting in slow-log overload.
Diagram: composing recipes in a safe chain
@startuml title Example middleware chain: tracing + guard + metrics + logging
actor Caller participant "Postboy" as Bus participant "Tracing MW" as Trace participant "Guard MW" as Guard participant "Metrics MW" as Metrics participant "Logging MW" as Log participant "Handlers/Subscribers" as H
Caller -> Bus: start operation Bus -> Trace: before() Bus -> Guard: before() alt Guard interrupts Bus --> Caller: cancelled else continues Bus -> Metrics: before() Bus -> Log: before() Bus -> H: run operation H --> Bus: success Bus -> Trace: after() Bus -> Metrics: after() Bus -> Log: after() Bus --> Caller: completed end
@enduml
Best practices (recipes edition)
Prefer one concern per middleware (guard vs logging vs metrics).
Filter aggressively in
canHandle(stage-first, then message subset).Treat cancellations as a first-class outcome:
decide whether to log attempts
decide where to count cancels
Keep diagnostics stable (middleware name + reason strings that are searchable).
Avoid high-cardinality metric labels (never label by message id).
Next steps
[How-to: attach middleware to Postboy][How-to: enable middleware only for a subset of messages][How-to: block execution (guard middleware)][How-to: logging and tracing messages][How-to: measure duration and collect metrics][Best practices and anti-patterns for middleware]