Structural Patterns «Prev Next»

Lesson 6Flyweight Suitability and Applicability
ObjectiveDetermine when Flyweight is appropriate and when it is not.

Flyweight Suitability and Applicability

Flyweight is suitable when a measured resource problem is caused by numerous logical objects duplicating a much smaller set of reusable values. It is not a default way to implement every value object or cache. The pattern exchanges a simple self-contained object for a shared intrinsic object, a lookup mechanism, and extrinsic context supplied by the client. That exchange is worthwhile only when the saved allocation and memory exceed the new complexity and lookup cost.

Conditions That Support Flyweight

The strongest case has all of the following characteristics:

  1. The application retains or repeatedly creates a very large number of logical objects.
  2. A profiler shows that duplicated state contributes materially to memory, allocation, or pause costs.
  3. The number of distinct intrinsic values is far smaller than the logical object count.
  4. Intrinsic state can be immutable and independent of one client's context.
  5. Extrinsic state can be supplied without creating unwieldy operations or excessive copying.
  6. Clients do not require a unique physical identity for every logical occurrence.

Text rendering, map symbols, game terrain, repeated product descriptions, and the vehicle types in this course simulation can satisfy these conditions. Each case still requires measurements with realistic data. A familiar example does not prove that a particular implementation has a problem.

Conditions That Argue Against It

Do not apply Flyweight when object count is modest, state is mostly unique, or objects are already compact values. Avoid sharing when the candidate intrinsic state changes per logical object, because synchronization or copying will undermine the design. If clients depend on reference identity, canonical instances may also change observable behavior.

The pattern is a poor fit when externalizing state makes every operation accept a large context object that duplicates the original data elsewhere. A global factory with arbitrary keys can retain more memory than it saves. A high contention lookup can reduce throughput. In these situations, ordinary value objects, records, arrays, compact identifiers, or data-oriented storage may be clearer and faster.

Identity, Equality, and Immutability

Flyweight intentionally allows several logical occurrences to reference one physical object. Domain equality must therefore be defined explicitly. Two vehicle events are not the same event merely because they share one VehicleType. Compare event identifiers for occurrence identity and compare type values for description equality. Tests that use == only to prove factory canonicalization should not leak that assumption into unrelated domain rules.

Immutability is the safest sharing boundary. In Java, use final fields, defensive copies, and unmodifiable returned values. In C++, prefer const objects with explicit smart-pointer ownership. If a property can change during a run, either make it extrinsic or create a different versioned flyweight. Allowing a setter because it is convenient turns shared state into hidden global mutation.

Factory and Lifecycle Suitability

The key space should be bounded or governed by an explicit eviction policy. A catalog of twelve vehicle types can safely live for one simulation. A catalog keyed by untrusted user strings may grow indefinitely. Decide whether the factory is owned by one request, simulation, tenant, application process, or class loader. Avoid a static global map unless that lifetime and isolation are truly required.

Concurrency changes factory implementation, not the pattern's applicability. A thread-safe map can prevent duplicate creation, but construction must remain safe and failures must not leave invalid entries. Track catalog size, hit rate, misses, and creation latency. These signals show whether sharing occurs as predicted and whether the lifecycle remains healthy.

Work Through the Decision With Evidence

Assume a traffic simulation retains 500,000 events. A heap profile shows that repeated vehicle descriptions account for 90 megabytes, while only sixteen distinct descriptions exist. Moving those descriptions into immutable flyweights is promising. Define a canonical key, build a process-local factory, and retain direction, lane, arrival time, and speed in each event. Run the same scenario before and after the change and compare retained heap, allocation rate, execution time, and result correctness.

Now change the facts. Suppose there are 2,000 events and nearly every vehicle has unique configuration data. The distinct-value ratio is poor, and a map lookup adds complexity without meaningful sharing. A compact immutable VehicleDescription stored directly in each event may be the better design. Applicability comes from the data distribution and workload, not from the domain noun.

Compare Simpler Alternatives

String interning or enum constants can solve small, closed cases without a general factory. Database normalization can avoid repeating persistent values, though loaded application objects may still duplicate them. Integer IDs can reference a compact table. Arrays or column-oriented layouts can improve memory locality for simulations and analytics. Object pooling addresses expensive reusable resources rather than immutable shared values.

Choose the least complicated alternative that satisfies the measured constraint. Document why it was selected, the expected cardinality, ownership, and the threshold at which the decision should be revisited. Include a test that confirms canonical reuse and a performance fixture that uses representative scale. If later measurements show negligible savings or unbounded catalog growth, the design should be changed rather than defended because it uses a recognized pattern. The final decision should remain understandable to a maintainer who sees the factory but did not participate in the original optimization work. Record the baseline and expected benefit beside the design. That record keeps later performance discussions tied to reproducible evidence.

A Quantitative Applicability Checklist

Applicability should be evaluated with numbers rather than with the vague observation that the application creates "many objects." Estimate how many logical objects exist at peak load, how much intrinsic data is duplicated, how many distinct intrinsic values occur, and how much extrinsic state must remain per logical object. The potential saving is roughly the duplicated intrinsic state removed from each logical object, minus references, cache entries, and lookup overhead.

For example, suppose a map displays 500,000 symbols, but the symbols use only 40 immutable icon definitions. Sharing those 40 definitions can remove hundreds of thousands of duplicate color tables and vector paths. If the same map contains only 500 symbols or nearly every symbol has a unique definition, Flyweight adds indirection without producing a meaningful saving.

  • Profile retained memory and allocation rate under representative load.
  • Count distinct candidate keys, not only total logical objects.
  • Confirm that intrinsic state is immutable or effectively immutable.
  • Measure lookup cost and contention in the proposed factory.
  • Define cache lifetime, eviction, and observability before production use.

Suitability in Managed and Native Languages

In Java, every logical object can hold a reference to its shared flyweight, and garbage collection can reclaim flyweights that are no longer strongly reachable. A plain HashMap may be sufficient during single-threaded initialization. A concurrent application may need ConcurrentHashMap.computeIfAbsent, careful key design, and limits that prevent an unbounded cache.

In C++, ownership must be explicit. A factory may return std::shared_ptr<const Flyweight>, keep values directly in a container, or use another lifetime strategy appropriate to the application. Stable references matter because container reallocation can invalidate pointers for some container choices. Const correctness helps enforce the promise that intrinsic state cannot be modified by one client and observed by every other client.

When a Value Object or Data-Oriented Layout Is Better

Flyweight is not the only way to reduce memory. A compact immutable value object may already be inexpensive. Records, structs, primitive arrays, string interning, database normalization, and column-oriented storage can eliminate duplication with less behavioral complexity. Data-oriented designs can place frequently accessed values in contiguous arrays, improving cache locality without introducing a shared object for every concept.

Use Flyweight when clients benefit from a shared object that owns meaningful behavior as well as intrinsic state. Prefer a simpler data representation when the candidate is merely a small tuple of values. This keeps the object model honest and avoids applying a class-heavy pattern where a map or compact value would communicate the design more clearly.

Cloud and Multi-Tenant Cautions

An in-process flyweight cache is local to one service instance. Autoscaling creates additional caches, and a deployment or restart discards them. A distributed cache can share canonical values across instances, but it changes the problem into remote data access with timeouts, serialization, versioning, and eviction. Do not make a request path depend on a distributed flyweight without a failure policy.

Multi-tenant systems also require isolation. A cache key must include every dimension that affects intrinsic state, including tenant, locale, permissions, and version where appropriate. Accidentally sharing data that is only superficially equal can expose private information or apply the wrong policy. The safety test is stronger than equality: the value must be valid for every client that receives the shared instance.

Versioning is part of that validity rule. If a shared specification changes, decide whether existing contexts continue to use the old version or resolve a new key. Replacing a cached value in place can make one request observe two different definitions during its lifetime. Immutable versioned keys provide a clearer migration path and make rollback possible.

Track the active versions so obsolete flyweights can be retired safely.

Measure retirement behavior during rolling deployments too.


SEMrush Software 6 SEMrush Banner 6