Software Design

Swift Middleware: Decorator + Chain of Responsibility

When building a network layer, you inevitably hit moments like this.

4 min read
Cover image for Swift Middleware: Decorator + Chain of Responsibility

When building a network layer, you inevitably hit moments like this.

A single request needs an auth token, logging, and retries on failure.

Stuff it all into one function, and before long, send() reaches 200 lines.

That is when the middleware pattern comes in.

Here is the conclusion: in Swift, the cleanest approach is to wrap each feature with the Decorator pattern, then connect the wrapped components in order with the Chain of Responsibility. They are partners, not competitors.

Today, I’ll explain why this combination works and how to wire it together with a practical code example.


What is the middleware pattern?

Middleware is an intermediate processing layer inserted between a request and a response.

If you have used Alamofire’s RequestInterceptor or Vapor’s server-side Middleware, you have already encountered the concept.

The core idea is simple.

Keep the request-processing core unchanged, while making it possible to add or remove behavior before and after it.

Build features such as authentication, logging, caching, and retries as independent pieces.

Then you can freely combine them: authentication only for one request, authentication plus caching for another.


How do Decorator and Chain of Responsibility differ?

Many people confuse the two.

In short, it comes down to this.

Category Decorator Chain of Responsibility
Purpose Layer functionality onto an existing object Pass through multiple handlers in sequence
Relationship Wrapping structure (nesting) Linked structure (chain)
Pass-through behavior Always passes to the next step May stop in the middle
Role in middleware Implementation of each feature Connect and order the features

After using both myself, I realized they are different layers of the same story, not opposites.

Decorator is about “how to attach functionality,” while Chain of Responsibility is about “the order in which the attached pieces run.”

Using them together lets each compensate for the other’s weaknesses.


Practical combination: wiring it together in code

First, define the shared middleware interface. It receives a request and passes it to the next middleware.

protocol Middleware {
    // requestprocess it, then pass it to the next step, next
    func handle(_ request: Request,
                next: (Request) async throws -> Response)
    async throws -> Response
}

Now create each feature one at a time, like a decorator. Below is logging middleware.

struct LoggingMiddleware: Middleware {
    func handle(_ request: Request,
                next: (Request) async throws -> Response)
    async throws -> Response {
        print("➡️ request: \(request.url)")
        let response = try await next(request)  // to the next step
        print("⬅️ response: \(response.status)")
        return response
    }
}

The call to next(request) is the link in the Chain of Responsibility.

It does its own job—writing the log—then passes the rest to the next middleware.

Finally, fold multiple middleware components into a single chain.

func buildChain(_ middlewares: [Middleware],
                final: @escaping (Request) async throws -> Response)
-> (Request) async throws -> Response {
    middlewares.reversed().reduce(final) { next, mw in
        { req in try await mw.handle(req, next: next) }
    }
}

The key is wrapping from the back forward with reduce.

If you insert the array in [인증, 로깅, 재시도] order, requests pass through in that order and responses come back out in reverse. Think of onion layers.

Requests move inward; responses come back out in reverse.
Requests move inward; responses come back out in reverse.
Before writing the code, I sketched the layered structure on paper.
Before writing the code, I sketched the layered structure on paper.

What are the benefits?

Here are the advantages I noticed after applying this to a real project.

  1. Adding a feature becomes adding one line to the array. Need caching? Just insert CachingMiddleware() into the array.

  2. The order is easy to change. Whether authentication runs before or after logging depends only on the array order.

  3. Testing is easier. Because each middleware is independent, you can add unit tests one at a time.

  4. The core stays clean. The 200-line send() shrinks back to under ten lines.

There are also a few things to watch out for.

Once there are too many middleware components, it becomes difficult to trace where a request travels.

So when a chain grows beyond five or six components, I put logging middleware first and record the path.


Wrapping up

This combination—building features with Decorator and ordering them with Chain of Responsibility—becomes useful beyond network layers, including event handling and validation.

Try adding today’s example to a small toy project. Peeling back the layers yourself makes the concept stick much faster. Happy refactoring!

Further reading