FlyWeight Design Pattern in Swift


Greetings, traveler!

The Flyweight pattern is a structural design pattern that helps reduce memory usage by sharing common data between multiple objects.

It is useful in cases where you need to create a large number of similar objects, and storing the same internal data repeatedly would be wasteful.

The key idea

Flyweight splits state into two parts:

  • Intrinsic state — the data that is shared and can be reused (stored inside the flyweight)
  • Extrinsic state — the data that changes per usage and should be provided from the outside (not stored inside the flyweight)

In practice, Flyweight is usually implemented with a factory (or manager) that caches flyweight objects and reuses them when possible.

Implementation Example

Let’s say we’re building a game store. A store can handle thousands of purchases, but the number of unique games in the catalog is limited.

For example, Doom might be purchased hundreds of times. Storing the same game metadata for every purchase would repeat identical data again and again.

Instead, we’ll store shared game data as a Flyweight and keep purchase-specific details separately.

Flyweight

enum GameID: String {
    case doom = "Doom"
    case elderScrolls = "The Elder Scrolls"
    case baldursGate = "Baldur's Gate"
}

This enum represents our shared configuration (intrinsic state).

Now let’s define the flyweight itself:

final class GameCatalogEntryFlyweight {
    let id: GameID

    init(id: GameID) {
        self.id = id
    }
}

Factory

The factory returns cached flyweights.

protocol GameCatalogEntryFactory {
    func entry(for id: GameID) -> GameCatalogEntryFlyweight
}

final class DefaultGameCatalogEntryFactory: GameCatalogEntryFactory {
    private var cache: [GameID: GameCatalogEntryFlyweight] = [:]

    func entry(for id: GameID) -> GameCatalogEntryFlyweight {
        if let cached = cache[id] {
            return cached
        }

        let flyweight = GameCatalogEntryFlyweight(id: id)
        cache[id] = flyweight
        return flyweight
    }
}

Note: this cache will only grow. That’s fine for a small, controlled set of keys (like GameID), but in real apps you may want an eviction strategy. NSCache can be a good fit.

Extrinsic State

A deal contains a lot of data that should not be stored inside the flyweight.

struct PurchaseContext {
    let id: Int
    let price: Double
    let customerName: String
}

Storefront (Purchases storage)

Now let’s connect purchases with flyweights:

final class Storefront {
    private let factory: GameCatalogEntryFactory
    private var purchases: [Int: (game: GameCatalogEntryFlyweight, context: PurchaseContext)] = [:]

    init(factory: GameCatalogEntryFactory) {
        self.factory = factory
    }

    func purchase(game id: GameID, context: PurchaseContext) {
        let game = factory.entry(for: id)
        purchases[context.id] = (game: game, context: context)
    }

    func report() {
        for (_, purchase) in purchases.sorted(by: { $0.key < $1.key }) {
            let title = purchase.game.id.rawValue
            let price = purchase.context.price
            let customer = purchase.context.customerName

            print("Purchase \(purchase.context.id): \(title), customer: \(customer), price: \(price)")
        }
    }
}

Usage

let factory = DefaultGameCatalogEntryFactory()
let store = Storefront(factory: factory)

store.purchase(
    game: .doom,
    context: .init(id: 1, price: 19.99, customerName: "John")
)

store.purchase(
    game: .doom,
    context: .init(id: 2, price: 14.99, customerName: "Alice")
)

store.purchase(
    game: .baldursGate,
    context: .init(id: 3, price: 59.99, customerName: "Kate")
)

store.report()

Even though we created three purchases, we only keep two flyweight instances (.doom and .baldursGate). The rest of the data stays outside and is stored per purchase.

In this example, a game “flyweight” represents only the shared part — the catalog entry (Doom, The Elder Scrolls, Baldur’s Gate, etc.). Many different purchases can reference the same flyweight instance, because the game itself doesn’t change between transactions. All purchase-specific data, such as the order id, customer name, and price, is stored separately as external context. This way the app can process thousands of purchases without allocating thousands of identical game metadata objects.

Conclusion

Flyweight is a practical pattern when you deal with a large number of objects and you can clearly separate reusable internal data from per-instance context.

Most of the time, the implementation comes down to:

  • defining the intrinsic part (the flyweight)
  • caching it in a factory
  • passing extrinsic state from the outside

This pattern isn’t needed in everyday code, but when memory usage starts to matter and the number of objects explodes, Flyweight can be a clean and reliable option.

This concludes the discussion of this pattern. The following article will discuss another structural design pattern, the Proxy.