This post aims to give you a clear, intuitive perspective on the Decorator pattern. As a software developer, you have likely implemented this pattern countless times without realizing it.
Its main advantage lies in its flexibility: because a decorator acts as an independent add-on, you can seamlessly attach or remove capabilities without disrupting the core system.
The Decorator pattern
Here is a refined, clear, and professional rewrite of the explanation of the Decorator Pattern, tailored for Swift developers:
The core idea of this pattern is to dynamically wrap or stack behavior around a central component. Crucially, wrapping an object with layers of new functionality must never alter or break its core identity or fundamental behavior.
Everyday Analogies:
Clothing: A person wearing a t-shirt is still a person. Adding a sweater adds a layer of protection, and putting a coat over that adds even more, but at their core, they remain the same person.
In astronomy: the Decorator pattern is best visualized as a Rocky Planet Core (the Decoratee) wrapped by concentric cosmic layers, such as an Atmosphere or a Ring System (the Decorators). Just as adding an atmosphere provides radiation filtering or adding rings reflects light, each outer layer enhances the primary celestial body with new features without altering its fundamental identity at the center
Coffee: Plain coffee becomes a café au lait when you add milk, and a cappuccino when you sprinkle cocoa powder on top—yet at its core, it remains coffee.
Implementation in Swift:
To preserve this core behavior in code, the Decorator pattern relies on two fundamental Swift principles:
Conformance to the Same Protocol: Both the core object (decoratee) and the wrapper (decorator) must conform to the exact same protocol. This ensures they are fully interchangeable to any client code.
Dependency Injection: The decorator receives the decoratee instance (typically via initializer injection) and delegates work to it. Any client interacting with the wrapper is seamlessly interacting with the underlying object, augmented by the new behavior.
When we turn into Swift:
protocol Component {
func execute() -> String
}
struct Decoratee: Component {
func execute() -> String {
return "Base execution"
}
}
class Decorator: Component {
private let component: Component
init(component: Component) {
self.component = component
}
func execute() -> String {
return component.execute()
}
} The compositiona and usage will be following:
let coreObject : Component = Decoratee()
let decoratedObject : Component = Decorator(component: coreObject)
let result = decoratedObject.execute()
print(result) Roles
Despite its simplicity, the Decorator pattern typically manifests in four distinct functional roles based on how it interacts with the inputs and outputs of the decoratee:
Monitor: The decorator observes execution without modifying any input or output data (e.g., Loggers, Profilers, or Analytics trackers).
Guard / Interceptor: The decorator intercepts and modifies, validates, or blocks the input before it reaches the decoratee (e.g., Cache lookups, Rate limiters, DDoS filters, or Failed login counters).
Fallback / Output Formatter: The decorator transforms or supplies the output after the decoratee executes, leaving inputs untouched (e.g. Currency formatters, or Response mappers).
Transformer: The decorator actively alters both the input and output data (e.g., Encryption layers, where incoming data is encrypted and outgoing data is decrypted).
Across all four roles, the underlying principle remains unchanged: the core identity and essential business logic of the decoratee are never altered—even during data transformation, where the underlying information remains the same despite its changed representation.
Monitor
In this role no transformation is performed, is the most inoquoos, where you do not notice any behaviour but at the end is there, we can add a decorator for profiling any heavy task, but just adding on debug mode but not on release.
import Foundation
// Interface & Decoratee
protocol DataService {
func fetch(query: String) async throws -> String
}
struct RemoteService: DataService {
func fetch(query: String) async throws -> String {
try await Task.sleep(for: .milliseconds(600))
return "Data for '\(query)'"
}
}
// Performance Monitor Decorator
struct PerformanceMonitor: DataService {
let wrapped: DataService
var threshold: Duration = .milliseconds(500)
func fetch(query: String) async throws -> String {
let clock = ContinuousClock()
let elapsed = try await clock.measure { _ = try await wrapped.fetch(query: query) }
let ms = elapsed.components.seconds * 1000 + Int64(elapsed.components.attoseconds / 1_000_000_000_000_000)
print("⏱️ Execution took \(ms)ms \(elapsed > threshold ? "⚠️ [THRESHOLD EXCEEDED]" : "")")
return try await wrapped.fetch(query: query)
}
} By using a Factory pattern, you can seamlessly configure a decorator—such as injecting a logger or profiler—whenever the debug flag is enabled.
enum ServiceFactory {
static func makeDataService(isDebug: Bool) -> DataService {
let baseService = RemoteService()
if isDebug {
return PerformanceMonitor(wrapped: baseService)
}
return baseService
}
}
let isDebugMode = true
let serviceA: DataService = ServiceFactory.makeDataService(isDebug: isDebugMode) Guard / Interceptor
This decorator acts as a guard / interceptor by evaluating incoming inputs and triggering a specific action—such as returning cached data—whenever a condition is met. The core behavior remains intact: the caller receives the requested data, completely agnostic of whether it originated from the cache or the underlying service.
Here is an example:
import Foundation
// 1. Component
protocol DataService {
func fetchData(key: String) -> String
}
// 2. Decoratee (Core network fetch)
struct NetworkDataService: DataService {
func fetchData(key: String) -> String {
print("🌐 Fetching from network...")
return "Data for \(key)"
}
}
// 3. Decorator (Caching layer)
final class CacheDecorator: DataService {
private let decoratee: DataService
private var cache: [String: String] = [:]
init(decoratee: DataService) {
self.decoratee = decoratee
}
func fetchData(key: String) -> String {
if let cached = cache[key] {
print("⚡️ Returning from cache")
return cached
}
let data = decoratee.fetchData(key: key)
cache[key] = data
return data
}
} When executed, both calls to fetchData return identical results. From the caller’s perspective, the behavior is indistinguishable; under the hood, however, different paths were taken: the first call fetched fresh data from the remote source, while the second served it directly from the cache.
let service: DataService = CacheDecorator(decoratee: NetworkDataService())
print(service.fetchData(key: "user")) // 🌐 Fetching from network... -> Data for user
print(service.fetchData(key: "user")) // ⚡️ Returning from cache -> Data for user Fallback / Output formatter
In this scenario, the decorator acts exclusively on the output. While the formatted responses look different, they represent the exact same underlying value—a concept demonstrated by the following Currency Decorator example. Although the decorator returns different strings depending on the selected currency (e.g., USD or EUR), the monetary value remains identical after conversion.
import Foundation
// 1. Component Protocol
protocol PriceService {
func getFormattedPrice(for productID: String) async -> String
}
// 2. Concrete Component (Base: USD Format -> $100.50)
struct BaseUSDPriceService: PriceService {
private let rawPrices: [String: Decimal] = ["PROD-1": 100.50]
func getFormattedPrice(for productID: String) async -> String {
let price = rawPrices[productID] ?? 0.00
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencySymbol = "$"
formatter.currencyGroupingSeparator = ","
formatter.currencyDecimalSeparator = "."
return formatter.string(from: price as NSDecimalNumber) ?? "$\(price)"
}
}
// 3. Decorator (Output Formatter: Converts USD -> EUR Format -> 100,50 €)
struct EURCurrencyDecorator: PriceService {
let wrapped: PriceService
let usdToEurRate: Decimal = 0.92 // Exchange rate conversion
func getFormattedPrice(for productID: String) async -> String {
// 1. Get raw formatted output from wrapped USD service
let usdString = await wrapped.getFormattedPrice(for: productID)
// 2. Extract numeric value from USD string
let cleanedString = usdString.replacingOccurrences(of: "$", with: "")
.replacingOccurrences(of: ",", with: "")
guard let usdAmount = Decimal(string: cleanedString) else { return "0,00 €" }
// 3. Convert currency and reformat (Symbol on the RIGHT)
let eurAmount = usdAmount * usdToEurRate
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
formatter.decimalSeparator = ","
formatter.groupingSeparator = "."
let formattedNumber = formatter.string(from: eurAmount as NSDecimalNumber) ?? "\(eurAmount)"
// Action on Exit: Transformed into EUR format
return "\(formattedNumber) €"
}
} To support a new currency, simply create another decorator that handles the conversion from USD to that specific currency.
// Base Service (USD)
let usdService: PriceService = BaseUSDPriceService()
let usdPrice = await usdService.getFormattedPrice(for: "PROD-1")
print("USD Base Output: \(usdPrice)")
// Output: $100.50
// Decorator (EUR)
let eurService: PriceService = EURCurrencyDecorator(wrapped: usdService)
let eurPrice = await eurService.getFormattedPrice(for: "PROD-1")
print("EUR Decorator Output: \(eurPrice)")
// Output: 92,46 € Transformer
In this scenario, the decorator transforms inputs on the way in and reverses the operation on outputs on the way out. This dual role is well illustrated by a cryptography layer: while the client always interacts with clear, unencrypted data, the decorator automatically ciphers it before storage and deciphers it upon retrieval.
import Foundation
import CryptoKit
// 1. Interface & Raw Storage (Decoratee)
protocol StorageService {
func save(id: String, payload: Data)
func load(id: String) -> Data?
}
class RawStorageService: StorageService {
private var db = [String: Data]()
func save(id: String, payload: Data) {
db[id] = payload
print("💾 [Storage] Saved \(payload.count) raw bytes for '\(id)'")
}
func load(id: String) -> Data? {
print("📖 [Storage] Reading raw bytes for '\(id)'")
return db[id]
}
}
// 2. Transformer Decorator (Encrypts on save, Decrypts on load)
struct CryptoDecorator: StorageService {
let wrapped: StorageService
let key: SymmetricKey
func save(id: String, payload: Data) {
// ENTRANCE: Encrypt Plaintext -> Ciphertext before storing
guard let sealedBox = try? AES.GCM.seal(payload, using: key),
let ciphertext = sealedBox.combined else { return }
print("🔐 [Crypto] Encrypted \(payload.count) bytes -> \(ciphertext.count) bytes")
wrapped.save(id: id, payload: ciphertext)
}
func load(id: String) -> Data? {
// EXIT: Fetch Ciphertext -> Decrypt back to Plaintext
guard let ciphertext = wrapped.load(id: id),
let box = try? AES.GCM.SealedBox(combined: ciphertext),
let plaintext = try? AES.GCM.open(box, using: key) else { return nil }
print("🔓 [Crypto] Decrypted \(ciphertext.count) bytes back to original plaintext")
return plaintext
}
} Its usage is as follows:
let key = SymmetricKey(size: .bits256)
let storage: StorageService = CryptoDecorator(wrapped: RawStorageService(), key: key)
let originalData = "Secret User Token".data(using: .utf8)!
// 1. Save (Encrypts on Entry)
storage.save(id: "user_session", payload: originalData)
// 2. Load (Decrypts on Exit)
if let loadedData = storage.load(id: "user_session"),
let decryptedString = String(data: loadedData, encoding: .utf8) {
print("Received: '\(decryptedString)'")
} Compositon of decocators
Last but not least, composing multiple decorators is one of the core strengths of the Decorator pattern. Because every decorator implements the exact same interface as the component it wraps, you can seamlessly chain them together—nesting behavior like Russian dolls.
Here is a complete usage example combining two separate decorators around a single storage component:
let key = SymmetricKey(size: .bits256)
let rawStorage = RawStorage()
// Composition Chain: Client -> MonitorDecorator -> CryptoDecorator -> RawStorage
let pipeline: StorageService = MonitorDecorator(
wrapped: CryptoDecorator(
wrapped: rawStorage,
key: key
)
)
let payload = "Secret Data".data(using: .utf8)!
// Execution sequence:
// 1. Monitor starts timer -> Calls Crypto
// 2. Crypto encrypts payload -> Calls RawStorage
// 3. RawStorage saves to memory
// 4. Returns back through layers -> Monitor logs total duration
pipeline.save(id: "config", payload: payload)
let loaded = pipeline.load(id: "config") Its usage is as follows:
let key = SymmetricKey(size: .bits256)
let storage: StorageService = CryptoDecorator(wrapped: RawStorageService(), key: key)
let originalData = "Secret User Token".data(using: .utf8)!
// 1. Save (Encrypts on Entry)
storage.save(id: "user_session", payload: originalData)
// 2. Load (Decrypts on Exit)
if let loadedData = storage.load(id: "user_session"),
let decryptedString = String(data: loadedData, encoding: .utf8) {
print("Received: '\(decryptedString)'")
} Conclusions
Although the Decorator pattern can adopt different functional roles, confusing them for separate patterns is easy. At its core, the essence remains unchanged: adding or removing features dynamically alters capabilities, but never alters the fundamental nature of the underlying behavior.
References
There are two primary historical references linked to the formal documentation of the Decorator pattern:
1994 (GoF Book): Design Patterns: Elements of Reusable Object-Oriented Software — Addison-Wesley published the definitive text defining the pattern.
1991 (ET++ Framework / PhD Thesis): Erich Gamma’s doctoral dissertation at the University of Zurich, where early architectural precursors were documented prior to the GoF book.