Welcome to our website.

Understanding the Flyweight Pattern Through a Real Device Management Example

The Flyweight pattern becomes much easier to grasp if you look at the name literally: fly suggests sharing, and weight points to small reusable pieces. In practice, it is a way to share the common parts of many similar objects instead of storing the same data again and again.

A lot of systems end up holding huge numbers of objects in memory. Most of those objects are nearly identical, with only a few fields that actually differ from one instance to another. That is exactly where the Flyweight pattern helps.

Shared components illustration

A device inventory problem

Suppose someone on a team is responsible for managing company devices: buying test machines, checking them out, and recording returns. There are only a handful of device models at first, maybe five or six. To keep track of every individual machine precisely, the data model might be designed like this:

<table> <thead> <tr> <th>class Device { // 记录设备的基本信息 constructor(id, memory, frequency, processor, network, pixel, price, ...) { this.id = id; this.memory = memory; this.frequency = frenquency; this.processor = porcessor; this.network = network; this.pixel = pixel; this.price = price; // more attributes ... } checkout () { this.hasCheckedOut = true; this.checkoutDate = new Date(); } giveback() { this.hasCheckedOut = false; this.checkoutDate = null; } // more methods ... }</th> </tr> </thead> <tbody> <tr> <td></td> </tr> </tbody> </table>

This model is straightforward enough. Every time a device is purchased and added to inventory, the operation looks like this:

<table> <thead> <tr> <th>var device = new Device(...); DB.insert(device);</th> </tr> </thead> <tbody> <tr> <td></td> </tr> </tbody> </table>

And when someone borrows or returns a device, the workflow is roughly:

<table> <thead> <tr> <th>var device = Database.query(deviceId); // 借出 device.checkout(); // 归还 device.giveback(); DB.update(device);</th> </tr> </thead> <tbody> <tr> <td></td> </tr> </tbody> </table>

At a small scale, this feels perfectly reasonable.

When scale changes everything

As the team grows and absorbs several smaller companies, device demand rises sharply. Testing requirements also become stricter: the team now needs to cover all common domestic phone models, more than 50 kinds in total, and about 100 units of each model. That pushes the inventory to over 5,000 devices.

Now the earlier design starts to hurt.

Each device object stores both the static description of the hardware and the dynamic lending information. If each object occupies around 10 KB of memory, then 5,000 devices consume over 50 MB. Holding thousands of large, repetitive objects in memory is wasteful, especially when most of the descriptive fields are duplicated across devices of the same model.

The real issue is not the number of devices alone. It is that the same model information is copied into object after object, even though only a few properties—such as checkout status and checkout date—change per physical unit.

Splitting shared data from changing data

A better approach is to let one Device object represent one device model rather than one physical machine:

<table> <thead> <tr> <th>class Device { constructor(type, xxx, ..) { this.type = type; // 一种设备的全部基本信息 this.xxx = xxx; } }</th> </tr> </thead> <tbody> <tr> <td></td> </tr> </tbody> </table>

Then use a DevicePool to manage reusable model objects:

<table> <thead> <tr> <th>class DevicePool { // hash 表保存设备信息 devicePool = {}; create(type, xxx, ...) { if (devicePool[type]) return devicePool[type]; const device = new Device(type, xxx, ...); devicePool[type] = device; return device; } }</th> </tr> </thead> <tbody> <tr> <td></td> </tr> </tbody> </table>

The per-device checkout state and other changing information can be handled separately by DeviceManager:

<table> <thead> <tr> <th>class DeviceManager { deviceManager = {}; // 借出时,添加借出记录 checkout (id, type, xxx, ...) { const device = new DevicePool.create(type, xxx, ...); deviceManager[id] = { device: device, hasCheckedOut = true, checkoutDate = new Date(); }; } // 归还时,删除借出记录 giveback(id, type) { deviceManager[id].hasCheckedOut = false; deviceManager[id].checkoutDate = null; } }</th> </tr> </thead> <tbody> <tr> <td></td> </tr> </tbody> </table>

After this change, memory usage drops from about 50 MB to roughly 2 MB.

That dramatic reduction comes from one simple idea: device specifications for the same model do not need to be duplicated thousands of times.

What changed conceptually

The improvement works because the design separates two kinds of information:

  • Shared metadata: the properties that are identical for devices of the same type
  • Dynamic state: the properties that differ for each specific unit, such as whether it has been checked out and when

DevicePool and DeviceManager isolate these two categories cleanly. When a checkout or return happens, the system combines the shared model object with the per-device state only where needed. That avoids pointless copying in memory.

Another way to describe it is this: the repeated part becomes a reusable object, while the variable part stays outside and is attached when necessary.

The core idea behind the Flyweight pattern

The heart of the Flyweight pattern is the flyweight factory. Its job is to maintain a pool of flyweight objects.

When a client needs an object, it does not immediately create a new one. It first asks the pool whether a matching flyweight already exists. If it does, the existing object is reused. If not, a new flyweight is created, returned, and stored in the pool for future use.

Two concepts are especially important here:

  • Intrinsic state: the internal, shareable properties
  • Extrinsic state: the external properties that differ between uses or instances

By building many logical objects out of the same shared part plus a smaller set of differing values, a system can save a significant amount of space and reduce memory pressure.

Why this example works so well

This device management case is a textbook fit for Flyweight because the number of physical objects is large, while the number of distinct types is small. There may be more than 5,000 devices, but only around 50 models. That means the static description can be stored once per model rather than once per machine.

The pattern often appears in systems like this:

  • large collections of similar business objects
  • interfaces rendering many repeated visual elements
  • applications where object creation is frequent and duplicated state is expensive

In short, if many objects look almost the same and only a few fields vary, the Flyweight pattern is worth considering.

Related Posts