Learn how wrappers and middleware add cross-cutting concerns around Clojure handlers and functions without tangling the main business path.
Middleware: A wrapper layer that intercepts a request, handler, or response path to add cross-cutting behavior such as logging, authentication, or metrics.
Wrappers and middleware are close cousins of decorator, but they deserve their own lesson because they are one of the most common real-world structural patterns in Clojure applications. In web systems especially, the middleware pipeline is often where authentication, correlation IDs, content negotiation, sessions, and metrics are attached.
In Ring-style systems, a handler is a function from request to response. That makes middleware easy to express:
1(defn wrap-request-id [handler]
2 (fn [request]
3 (let [request-id (or (get-in request [:headers "x-request-id"])
4 (str (random-uuid)))
5 request' (assoc request :request/id request-id)
6 response (handler request')]
7 (assoc-in response [:headers "x-request-id"] request-id))))
This wrapper does one thing: ensure request and response correlation. The core handler remains focused on application behavior.
The same structural idea works around:
The pattern is broader than Ring. Ring just makes it very visible.
Order matters because each wrapper changes what later layers see.
Common examples:
Bad ordering can make debugging difficult or hide useful context.
Strong middleware typically has one clear concern:
Once one wrapper starts doing several unrelated tasks, the pipeline becomes harder to understand and reuse.
It is tempting to push more and more application logic into wrappers because they run “before everything else.” Resist that. Middleware is the right place for transport and cross-cutting behavior, not for core domain workflows.
If a rule depends on business meaning rather than generic request handling, it usually belongs deeper than the middleware stack.
Even when each wrapper is small, the assembled stack can become opaque. A little local documentation near the composition point helps a lot:
That makes debugging and refactoring safer because the contract is no longer implicit.
If the team cannot explain the order or purpose of the wrappers, production debugging will suffer.
Middleware that silently rewrites successful results, error forms, or headers in surprising ways makes the system harder to reason about.
When middleware becomes the place where domain decisions live, the architecture starts to blur.
Use middleware and wrappers for transport and cross-cutting concerns that genuinely belong around the main handler. Keep each layer small, name the stack clearly, and make the order intentional. In Clojure, good middleware makes the core handler simpler rather than more magical.