Software Design

Swift Decorator Pattern: Layering Features Without Inheritance

When developing iOS apps, you sometimes want to add just logging or caching while keeping the core behavior intact. Start subclassing for that, and before long the inheritance tree becomes a mess.

4 min read
Cover image for Swift Decorator Pattern: Layering Features Without Inheritance

When developing iOS apps, you sometimes want to add just logging or caching while keeping the core behavior intact. Start subclassing for that, and before long the inheritance tree becomes a mess.

When features are added one by one to a network client, it is easy to end up with a class named something like CachingLoggingRetryingClient.

Today, let’s organize the Swift Decorator Pattern that solves this problem: layering features like an onion, without inheritance.

The Decorator Pattern wraps an object in another object that follows the same protocol, adding behavior layer by layer without touching the original code.

Here’s the conclusion first.

  1. Define common behavior in a protocol first
  2. A decorator conforms to that protocol and stores another value of the same protocol type inside
  3. It performs its own work, such as logging or caching, then delegates the rest to the wrapped object
  4. Keep wrapping as needed to build up behavior

What Is the Decorator Pattern? How Is It Different from Inheritance?

Inheritance expresses an “is-a” relationship. A child inherits everything from its parent.

The problem is combinations. If you create clients with logging, caching, or both through inheritance, each combination becomes another class.

A decorator expresses a “wrapped-in” relationship.

Because the wrapper and wrapped object share the same interface, they look identical from the outside. The client code stays unchanged no matter how many layers you add.

In short, inheritance fixes relationships at compile time, while decorators can be composed freely at runtime.


Implementing the Swift Decorator Pattern in Code

Let’s create a DataLoader that loads data. First, here is the protocol everyone will follow.

protocol DataLoader {
    func load(id: String) -> Data?
}

// Basic implementation that does the actual work
struct NetworkLoader: DataLoader {
    func load(id: String) -> Data? {
        print("Request \(id)  to the network")
        return Data("payload-\(id)".utf8)
    }
}

Up to this point, it is just an ordinary protocol and implementation.

Now for the key part. The decorator conforms to DataLoader and contains another DataLoader inside.

// Logging decorator
struct LoggingLoader: DataLoader {
    let wrapped: DataLoader   // Wrapped target

    func load(id: String) -> Data? {
        print("[Log] load  start: \(id)")
        let result = wrapped.load(id: id)  // Delegate the actual work
        print("[Log] load  complete: \(id)")
        return result
    }
}

LoggingLoader only does its own job—writing logs—and passes the actual loading to wrapped.

This structure means you do not need to care what is wrapped. It only needs to be DataLoader.

A structure that does its own job and delegates the rest to the inner object
A structure that does its own job and delegates the rest to the inner object

Layering Features Step by Step

Let’s add a caching decorator and actually layer the decorators.

final class CachingLoader: DataLoader {
    let wrapped: DataLoader
    private var cache: [String: Data] = [:]

    init(wrapping loader: DataLoader) { self.wrapped = loader }

    func load(id: String) -> Data? {
        if let hit = cache[id] { return hit }        // Return immediately if present in the cache
        let data = wrapped.load(id: id)              // Delegate when absent
        cache[id] = data
        return data
    }
}

The assembly step is where the Decorator Pattern really shines.

let loader = LoggingLoader(
    wrapped: CachingLoader(wrapping: NetworkLoader())
)

Read it from the inside out. We wrapped the network loader with caching, then wrapped that with logging.

When a call arrives, the flow is logging → cache check → network if absent.

Calls descend one layer at a time through the wrappers
Calls descend one layer at a time through the wrappers
Nested from the inside out as network, caching, and logging
Nested from the inside out as network, caching, and logging

To change the order, just change the wrapping order. Neither NetworkLoader nor CachingLoader requires a code change.

If a feature is unnecessary, simply remove that layer.


Inheritance or Decorators: Which Should You Use?

Neither is always the right answer. Here is a situation-by-situation guide.

Situation Recommendation
Clear “is-a” relationship Inheritance
Freely combine and remove features Decorator
Toggle features at runtime Decorator
When the original code cannot be modified Decorator
Many possible combinations Decorator

Decorators work especially well for cross-cutting features rather than core behavior, such as logging, caching, retries, and adding authentication headers.

Forcing decorators where they do not fit can create many layers and make the call stack deeper during debugging. Keep that trade-off in mind.


Two Frequently Asked Questions

Q. Should I use a struct or a class?

If the decorator must own state, such as a cache, a class is convenient. For simple delegation, a struct is enough. That is why only caching is a class in the example above.

Q. Do many wrapper layers cause performance problems?

Because calls pass through each layer, there is a tiny overhead. Compared with network or disk I/O, however, it is negligible for most apps.


The Decorator Pattern ultimately comes down to defining one protocol, creating an object that contains the same protocol, and delegating through it.

Next time you find yourself digging through an inheritance tree to add a feature, consider wrapping it in one more layer. Your code will become much lighter. I recommend moving today’s example directly into a playground and trying it 🙂