Sitemap

How Flyweight Helps When Your App Is Drowning in Objects

12 min readMay 1, 2025

In the previous article of this series, Too Many Moving Parts? The Facade Pattern Can Help, we explored how the Facade design pattern simplifies complex systems by providing a unified interface. That pattern was about reducing complexity on the surface — making things easier to interact with. But what if your problem isn’t complexity at the interface, but sheer volume underneath?

Flyweight Design Pattern

If you’ve ever profiled a system and discovered that memory usage was ballooning just because you’re tracking thousands of similar objects — UI elements, game entities, message widgets — you’re not alone. That’s exactly the kind of situation where the Flyweight Pattern shines.

Flyweight is all about efficiently managing a large number of objects by sharing common data between them. In this article, we’ll find out what the Flyweight pattern is, how it works, and when it’s justified to use it. We’ll also look at its pros, cons, and even implement a unique Dart code example for a chat application message template scenario.

What is the Flyweight Pattern?

The Flyweight is a structural design pattern. Its intent is to minimize memory usage when you need to create a large number of similar objects. It does so by sharing common parts of state between those objects, instead of keeping all data duplicated in each one. In simpler terms, Flyweight lets you fit more objects into memory by reusing immutable data that many objects have in common.

Consider an application where hundreds or thousands of objects are present, and many of those objects share certain attributes. Storing a copy of the same data in each object would be highly inefficient. The Flyweight pattern identifies these shared pieces of state and ensures they’re stored only once, centralizing the common data. Each object then references this shared data rather than owning its own duplicate copy.

Design Flyweight Design Pattern UML — Flyweight pattern — Wikipedia

Key idea: Only the unique, context-specific information is stored separately for each object, while the shared, invariant information is stored once and reused. This division of state is the essence of the Flyweight pattern.

How Does the Flyweight Pattern Work?

The Flyweight pattern works by splitting the state of objects into two categories: intrinsic state and extrinsic state.

  • Intrinsic State (Internal): This is the invariant (unchanging) part of the object’s state that can be shared among many objects. It is stored internally in the Flyweight object. Intrinsic state is independent of the object’s context — it does not vary between different uses of the object. Because it remains constant, it’s safe to cache and reuse.
  • Extrinsic State (External): This is the variant part of the object’s state that cannot be shared. It depends on the context in which the object is used, and so it differs for each object instance. Extrinsic state is kept outside the Flyweight object and passed in whenever needed.

In the Flyweight pattern, each object (Flyweight) stores only intrinsic state internally. Whenever an operation requires extrinsic information, the client code supplies it at runtime. This means the same Flyweight object can be used in many different contexts by giving it different extrinsic data for each context. Essentially, you remove the context-specific data from the object, and only feed it into methods when needed, thereby reusing the core object for multiple contexts.

Flyweight Factory and Object Sharing

To manage Flyweight objects, typically a Factory (or cache manager) is used. The Flyweight Factory keeps a pool of flyweight objects that have been created so far. When the client code needs an object with a certain intrinsic state, it asks the factory, which either returns an existing flyweight from the pool or creates a new one if it doesn’t exist, then returns it. This ensures that only one object per unique intrinsic state exists.

Caching: The Flyweight Factory uses a cache (often a simple map or dictionary) to store flyweight instances, with a key based on the intrinsic state. This caching is really important — it is what enables reuse. Instead of calling new repeatedly for identical objects, the factory provides a shared instance. In other words, the factory ensures object sharing behind the scenes. This pattern inherently trades a bit of extra work (looking up or creating objects via the factory) for potentially huge memory savings by avoiding duplicate objects.

Immutability: Since flyweight objects are meant to be shared, they are usually designed to be immutable (their intrinsic state doesn’t change once created). This is important — if one client altered the intrinsic state inside a shared Flyweight, it would affect all other clients using that object. By keeping flyweights immutable, we ensure that sharing them is safe and transparent. If some part of state does need to change per context, that part should be extrinsic (supplied from outside rather than stored inside the object).

When is Flyweight Justified?

Flyweight is not a pattern you apply everywhere; it shines only in particular scenarios. You should consider (and justify) using the Flyweight pattern only when all of the following conditions are met:

  • Extremely Large Number of Objects You have (or anticipate) a very large number of objects in memory at the same time. For example, thousands or more. The sheer quantity is causing memory pressure. If you’re only dealing with a few dozen objects, Flyweight is overkill and adds unnecessary complexity.
  • Significant Duplicate State Data Many of these objects share identical data that can be factored out. There must be identifiable parts of the object state that are common across many objects. If each object is truly unique, Flyweight won’t help. But if, say, 80% of each object’s data is the same for hundreds of objects, that’s a good sign. E.g., hundreds of icons with the same graphic but different positions.
  • Immutability of Shared State The shared portion (intrinsic state) should not depend on context and ideally be immutable. If the “common” state can change in different situations, sharing it becomes problematic. So, the scenario should allow treating that part of the object as constant (at least for the lifetime of the flyweight).
  • Extrinsic Data Management is Feasible The context-specific state can be kept or computed externally and supplied to the objects when needed. This means your design can accommodate passing additional info around. If supplying extrinsic data all over the place is too awkward, Flyweight might not be a good fit.
  • Performance Trade-off is Acceptable Accessing data through a flyweight (and its factory) introduces a level of indirection and may require computing or looking up extrinsic state on the fly. You should be in a situation where the memory savings outweigh this overhead. For example, in a game, using Flyweight to cut memory usage by 70% might slightly increase CPU usage for managing extrinsic data, but if memory was the limiting factor, it’s worth it.

In summary, Flyweight is justified when memory is a bigger concern than a bit of extra complexity or CPU usage, and when your objects have clear separable state with lots of commonality. A typical justified use case is graphical applications (games, GUIs) or systems like document editors, where lots of similar items are present. If those conditions don’t hold (few objects, or no repetitive data), you likely shouldn’t use Flyweight because it won’t buy you much and will complicate your design.

Benefits of the Flyweight Pattern

Using the Flyweight pattern can provide several advantages in the right context:

Massive Memory Savings

By sharing intrinsic state, you drastically reduce memory consumption. Instead of N objects each carrying duplicate data, you have one shared object and N lightweight instances referencing it. This can allow your system to hold far more objects than otherwise possible.

Performance Gains via Fewer Allocations

Having fewer physical objects can reduce the overhead of object creation and garbage collection. With Flyweight, you create far fewer heavy objects. This can improve performance by lowering GC pressure and allocation costs. However, note the caveat in cons about other performance costs.

Consistency of Shared State

Since the intrinsic state is centralized, if it ever needs to be updated or fixed, you change it in one place. All objects sharing it will reflect the change. This can be seen as a benefit when you want consistency. That said, usually intrinsic state is immutable, so this is more about conceptual consistency than runtime changes.

Can improve cache locality

This is a low-level benefit — by using shared objects, frequently used data might stay in CPU cache, potentially improving speed when many objects reference the same data. This is situational but can happen if, for instance, many entities use the same shared sprite or string, it might be loaded in memory once and reused quickly.

What Can Go Wrong (and How to Handle It)

Despite its benefits, the Flyweight pattern comes with some downsides and trade-offs:

Added Complexity

Introducing Flyweight means adding a factory or caching mechanism and handling extrinsic state everywhere the object is used. This adds complexity to the codebase. The flow of data is less straightforward because part of an object’s state is now external.

Ensure the usage of Flyweight is well-encapsulated. For example, provide utility methods to assemble full object behavior so that calling code doesn’t have to manually manage extrinsic data each time.

Space-Time Trade-off

You trade memory for CPU overhead. Because extrinsic state is not inside the object, the program might need to pass that data around or compute it on the fly, which can incur runtime cost. Also, looking up objects in the Flyweight factory cache has a cost (though usually small, e.g., a hash map lookup).

If the extrinsic state is expensive to compute repeatedly, you might cache it in a separate structure or compute it once and reuse it. Also, use efficient data structures for the Flyweight lookup (like a hash map with keys that are cheap to compare). In performance-critical sections, measure to ensure the indirection isn’t a bottleneck.

Reduced Encapsulation

An object no longer contains all of its own data — some of it is external. This can be conceptually harder to understand and might break the natural encapsulation principle. The object’s behavior might be undefined without the external context.

You can encapsulate the combination of flyweight + extrinsic state behind a higher-level class or at least clearly document the relationship. Some implementations use a wrapper that represents the “full” object by holding a reference to a flyweight and the extrinsic values together, so that outside of the low-level code, other parts of the program can treat it as a normal object.

Narrow Applicability

As noted, Flyweight only helps in very specific conditions. If those conditions are not met, applying Flyweight is a net negative (complexity with no benefit). In fact, several conditions should hold true to gain benefits from Flyweight. So it’s not a universally applicable pattern.

Use Flyweight only after identifying a true need (like memory profiling shows a problem, or you clearly see huge duplication). Avoid premature optimization; if unsure, implement in a straightforward way first and introduce Flyweight if profiling indicates it’s justified.

Potential Concurrency Issues

If not designed carefully, sharing objects between multiple contexts or threads could lead to concurrency problems. For instance, if a flyweight object had any mutable state, two threads using it could interfere.

The primary mitigation is making Flyweight objects immutable (no thread can alter the intrinsic state). If some shared state must be mutable, you would need synchronization, which complicates things and might erase performance gains. Ideally, design flyweights to be value objects that are read-only once constructed.

In summary, you should weigh these cons against the pros. Often, the memory optimization is achieved at the cost of extra complexity and slight runtime overhead. That’s a good trade if memory was a limiting factor, but a bad trade if your memory usage was fine to begin with. Mitigate these issues by careful design: keep shared state simple and immutable, and encapsulate the flyweight usage behind clear APIs.

Example: Flyweight Pattern in a Chat Application

Now, we’ll implement the pattern in a practical use case. Suppose we are building a chat application, and we have various message templates for system messages. For instance, when a user joins the chat, the system might broadcast “*{user} joined the chat*". When a user leaves, "*{user} left the chat*". There might be other templated messages like "*{user} was kicked by an admin*", "Welcome {user}!", etc. These messages have a common format with a placeholder for the user’s name.

Without Flyweight, each time such a message is needed, we might create a new string or message object by concatenating or formatting the pieces. If the chat is very active, you could be creating hundreds of identical template strings with only the username different. That’s inefficient, especially if these message templates carry more data (imagine if each template had an associated icon or styling information).

With the Flyweight pattern, we can create a MessageTemplate flyweight for each unique template format, and reuse it whenever we need to generate an actual message. The intrinsic state would be the template text (and maybe some constant metadata like message type or an icon image). The extrinsic state would be the specific user name or other dynamic info to fill in.

Let’s implement this in Dart:

// Enumeration of message types (for intrinsic state identification)
enum MessageType { userJoined, userLeft, userKicked, welcome }

// The Flyweight class: MessageTemplate
class MessageTemplate {
final MessageType type;
final String templateText;
// You could also have other intrinsic data, e.g., an icon or code
// For simplicity, we'll just keep template text here.

MessageTemplate(this.type, this.templateText);

// Use the template to format a full message given the extrinsic state.
// In this case, extrinsic state is the username to insert.
String formatMessage(String userName) {
// Replace placeholder `{user}` with the actual userName
return templateText.replaceAll('{user}', userName);
}
}

// The Flyweight Factory that manages MessageTemplate instances
class MessageTemplateFactory {
// A cache to hold flyweight instances, keyed by MessageType
static final Map<MessageType, MessageTemplate> _cache = {};

// Retrieve a MessageTemplate for the given type, creating it if necessary
static MessageTemplate getTemplate(MessageType type) {
// If already created, return from cache
if (_cache.containsKey(type)) {
return _cache[type]!;
}
// Otherwise, create a new template based on type
MessageTemplate template;
switch (type) {
case MessageType.userJoined:
template = MessageTemplate(type, '{user} joined the chat');
break;
case MessageType.userLeft:
template = MessageTemplate(type, '{user} left the chat');
break;
case MessageType.userKicked:
template = MessageTemplate(type, '{user} was kicked from the chat');
break;
case MessageType.welcome:
template = MessageTemplate(type, 'Welcome, {user}!');
break;
}
// Store it in the cache and return it
_cache[type] = template;
return template;
}
}

void main() {
// Simulate some chat events with users
List<Map<String, String>> events = [
{'type': 'userJoined', 'user': 'Alice'},
{'type': 'userJoined', 'user': 'Bob'},
{'type': 'userLeft', 'user': 'Alice'},
{'type': 'userKicked', 'user': 'Dave'},
{'type': 'userJoined', 'user': 'Eve'},
{'type': 'welcome', 'user': 'Frank'},
{'type': 'userLeft', 'user': 'Bob'},
];

for (var event in events) {
// Convert string to MessageType enum
MessageType type = MessageType.values.firstWhere((e) => e.toString().split('.').last == event['type']);
String userName = event['user']!;

// Get the flyweight template for this type
MessageTemplate tpl = MessageTemplateFactory.getTemplate(type);

// Use the template (intrinsic state) with the extrinsic state (userName)
String message = tpl.formatMessage(userName);

print(message);
}

// Let's see how many distinct MessageTemplate instances were created:
print('Distinct templates created: ${MessageTemplateFactory._cache.length}');
}

In this Dart code:

  • We defined an enum MessageType to identify different template types (this helps as a key for caching).
  • MessageTemplate is our Flyweight class. Its intrinsic state is the template text (like "{user} joined the chat"). We keep it immutable (fields are final and set in constructor).
  • MessageTemplateFactory is the Flyweight Factory that maintains a static cache (_cache) of MessageTemplate instances. The getTemplate method either returns an existing template or creates and caches a new one if it's the first time that type is requested.
  • In main(), we simulate a series of events with users. For each event, we look up the corresponding MessageTemplate via the factory and then call formatMessage(userName) to fill in the extrinsic state (the user name).
  • We print out the messages to see the output, and at the end print how many distinct templates were created.

Running this would yield something like:

Alice joined the chat
Bob joined the chat
Alice left the chat
Dave was kicked from the chat
Eve joined the chat
Welcome, Frank!
Bob left the chat
Distinct templates created: 4

The output shows the messages as expected. More importantly, the final line shows that even though we processed 7 events, we only created 4 distinct templates (one for each message type used). If the chat had hundreds of “userJoined” and “userLeft” events, we would still only ever have one MessageTemplate for "joined" and one for "left" in memory. All those messages would reuse the same two template objects, simply substituting different user names.

This example demonstrates Flyweight in action: the message format is the shared intrinsic state, and the specific user name is the extrinsic state provided at the moment of usage. We avoided duplicating the template strings and any associated data for every message instance, which could be significant savings if those templates were complex objects.

Conclusion

Flyweight won’t solve every performance problem — but when memory is your bottleneck, few patterns offer a more elegant fix. The Flyweight pattern is a powerful technique when you are dealing with too many objects and not enough memory. By cleverly sharing common parts of state and externalizing the rest, Flyweight can reduce your memory footprint dramatically. We’ve seen that it works by separating intrinsic (shared) and extrinsic (unique) state, using a factory to cache and reuse objects, and it brings certain costs in complexity and design clarity.

In practice, you should use Flyweight thoughtfully — it’s most beneficial in scenarios with lots of repetitive data, and not worthwhile if your objects are mostly distinct. When used appropriately, Flyweight enables systems to scale in ways that would be impractical otherwise (imagine rendering thousands of objects).

Have you used the Flyweight pattern in your own projects? Or faced situations where it could have helped but wasn’t used? I’d love to hear how you’ve approached memory optimization in real-world systems — feel free to share your experience in the comments. If you found this article helpful, share it with your team or friends and follow me to catch the next article in the series.

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

Responses (2)