Structural Patterns «Prev Next»

Lesson 5Flyweight Pattern and Motivation
ObjectiveExplain the design pressure that motivates Flyweight.

Flyweight Pattern and Motivation

Flyweight addresses a specific resource problem: an application needs a very large number of logical objects, and those objects repeat substantial state. Creating one complete physical object for every logical item consumes enough memory to limit capacity or cause unacceptable allocation and garbage collection work. The pattern separates reusable intrinsic state from context-specific extrinsic state, then shares one immutable flyweight for each intrinsic value.

A text editor illustrates the idea. A document can contain millions of character positions, but many positions use the same glyph, font, and style. The glyph description can be shared while position, selection, and document membership remain outside it. In the traffic simulation, many cars may share a vehicle description such as type, dimensions, and normal velocity, while arrival time, direction, queue position, and current movement belong to each simulation event.

Measure Before You Build

Flyweight is an optimization pattern, so the decision should start with measurement, not intuition. Before introducing a factory and a shared-instance cache, estimate:

  • The number of logical objects the application will create.
  • The bytes duplicated per object across those instances.
  • The number of distinct intrinsic values actually in use.
  • The expected lifetime of the objects and of the cache itself.

If one million objects duplicate 200 bytes but use only 20 distinct descriptions, sharing has clear potential. If a program creates 500 short-lived objects, the factory and lookup structure may cost more than the duplication it removes.

Measure with a profiler or a representative load test, not source field sizes alone. In Java, object headers, reference width, alignment, and collection implementation affect actual memory. In C++, layout, padding, ownership, and allocator behavior matter. Record allocation rate, retained heap, pause behavior, lookup latency, and cache growth before and after the change, and repeat the measurement whenever object volume, runtime configuration, or the platform memory model changes materially.

Modern runtimes already apply related techniques worth knowing as reference points: Java interns some strings and caches selected boxed values; database drivers pool connections (a resource-management pattern, not a pure Flyweight); game engines share meshes and textures while each scene entity keeps its own position and animation state; browser rendering engines share font and style data across many layout objects. The useful question is always which data is truly identical and safe to share.

Intrinsic and Extrinsic State

Intrinsic state is independent of one use and safe to share. It becomes part of the flyweight and usually must be immutable. Extrinsic state varies by context and is supplied when the client asks the flyweight to perform an operation. The separation must follow meaning, not convenience. A vehicle model may be intrinsic; its current lane is extrinsic. A glyph outline may be intrinsic; its position on a page is extrinsic.

Moving too much state outside creates complicated method signatures and can weaken encapsulation. Moving changing state inside makes shared instances unsafe. A useful test is to imagine two clients using the same flyweight at the same time. If one client's operation can change what the other observes, the supposed intrinsic state is not safely shared.

Canonical Creation Through a Factory

Clients should not call the flyweight constructor directly. A factory builds a canonical key, checks a map, and returns the existing instance or creates one. Canonicalization is essential: keys that differ only by case, whitespace, units, or object identity can accidentally represent the same value and defeat sharing. A small immutable key type makes equality and hashing explicit.

Java can use ConcurrentHashMap.computeIfAbsent when concurrent creation is required, provided construction has no unsafe side effects. C++ can store shared_ptr<const Flyweight> values or use an owner with a clearly documented lifetime. The choice between strong and weak references depends on whether the catalog is bounded and whether unused flyweights should be reclaimed.

Why Immutability Matters

Immutability turns sharing from a convention into a property of the type. Declare Java fields final, defensively copy mutable constructor arguments, and avoid returning internal collections. In C++, expose const operations and prefer value members or immutable shared ownership. A setter on a shared object is a warning because one caller can silently alter every logical item that references it.

Immutable flyweights are naturally safe for concurrent reads. The factory may still need synchronization, and extrinsic state remains the client's responsibility. Keeping those concerns separate makes race analysis and tests substantially simpler.

A Minimal Java Example

The traffic simulation scenario used throughout this lesson maps directly to code. VehicleType is the immutable flyweight; the factory guarantees one instance per distinct type; VehicleEvent carries the extrinsic state and supplies it back to the flyweight on each call.

// Immutable flyweight: safe to share across every event that uses it.
public final class VehicleType {
    private final String category;
    private final double lengthMeters;
    private final double normalAcceleration;

    VehicleType(String category, double lengthMeters, double normalAcceleration) {
        this.category = category;
        this.lengthMeters = lengthMeters;
        this.normalAcceleration = normalAcceleration;
    }

    // Behavior takes extrinsic state as parameters rather than storing it.
    public double estimateArrivalOffset(double currentSpeed, double distanceRemaining) {
        double adjustedSpeed = currentSpeed + normalAcceleration;
        return distanceRemaining / adjustedSpeed;
    }
}

// Factory: guarantees canonical, shared instances per intrinsic key.
public final class VehicleTypeFactory {
    private final ConcurrentHashMap<String, VehicleType> catalog = new ConcurrentHashMap<>();

    public VehicleType get(String category, double lengthMeters, double normalAcceleration) {
        String key = category.trim().toLowerCase(); // canonicalize before lookup
        return catalog.computeIfAbsent(key,
            k -> new VehicleType(category, lengthMeters, normalAcceleration));
    }
}

// Client: extrinsic state lives here, not in the shared flyweight.
public final class VehicleEvent {
    private final VehicleType type;      // shared
    private final String vehicleId;      // extrinsic
    private double currentSpeed;         // extrinsic, mutable per event
    private double distanceRemaining;    // extrinsic, mutable per event

    public VehicleEvent(VehicleType type, String vehicleId,
                         double currentSpeed, double distanceRemaining) {
        this.type = type;
        this.vehicleId = vehicleId;
        this.currentSpeed = currentSpeed;
        this.distanceRemaining = distanceRemaining;
    }

    public double estimateArrivalOffset() {
        return type.estimateArrivalOffset(currentSpeed, distanceRemaining);
    }
}

Two events for different cars can reference the same VehicleType instance safely because nothing on that instance ever changes after construction; only the arguments passed in at call time differ.

Benefits Beyond Raw Memory

Reduced allocation can improve cache locality and lower garbage collection pressure. Canonical values can make configuration consistent because every client receives one validated description. Expensive parsing or preprocessing can be performed once per intrinsic value. These are possible benefits, not guarantees. A hash lookup on every operation and scattered extrinsic state can reduce throughput or locality.

Flyweight also communicates domain structure. It makes the distinction between a reusable description and one occurrence explicit. This can improve testing: factory tests verify canonical identity, flyweight tests verify immutable behavior, and simulation tests vary extrinsic state without constructing a new description for each case.

Boundaries: Concurrency and Distribution

An immutable flyweight can be shared safely across threads because no client can change its intrinsic state. The factory or cache still needs a safe publication strategy, such as a concurrent map or initialization completed before worker threads begin. If creation is expensive, atomic cache operations should prevent several threads from constructing equivalent values at once.

Flyweight is normally process-local. Sharing an object reference across containers or servers is impossible without introducing a distributed cache, database, or serialization boundary — and those technologies bring their own consistency, latency, eviction, and failure concerns. Flyweight reasoning still helps identify which data is intrinsic, but crossing the process boundary is a distributed-systems decision, not a transparent extension of the pattern. Share within the smallest boundary that satisfies the measured need.

The pattern is also distinct from object pooling. A pool lends reusable, typically mutable resources and later receives them back. A flyweight is a shared, immutable value used concurrently with extrinsic context. Confusing the two can create unsafe mutation and unclear ownership.

The intrinsic/extrinsic boundary should be visible in the API: a shared glyph owns its font family and outline while the caller supplies position and color per rendering call; a shared vehicle description owns model dimensions and type while the simulation supplies location, speed, and direction. Passing contextual state explicitly keeps the flyweight immutable and makes the cost of each operation visible. Treat the factory itself as an owned cache with a documented lifecycle: define the key's equality rules, decide whether entries may be evicted, and expose metrics that reveal hit rate and retained memory.

A Traffic Simulation Decision Example

Suppose a simulation models 750,000 vehicle events but only twelve vehicle descriptions. A complete event object that repeats name, dimensions, category, normal acceleration, and rendering data wastes memory and repeats validation. A VehicleType flyweight can hold those immutable properties. Each VehicleEvent retains its identifier, approach, lane, arrival time, current speed, and queue position. The event calls behavior with its own context rather than mutating the shared type.

The factory key might contain a normalized category and configuration version. That version is important: silently returning an old description after a configuration update produces incorrect simulation results. The application can create a new catalog for each run, include the version in the key, or invalidate entries at a controlled boundary. The selected lifecycle should be visible in metrics such as catalog size, hit rate, creation count, and retained bytes.

Failure Modes That Weaken the Motivation

Several mistakes turn a well-intentioned Flyweight into a liability:

  • A factory that accepts arbitrary keys can grow without limit and become a memory leak.
  • A mutable flyweight can let one request silently affect another.
  • An expensive key or contended global map can move the bottleneck from allocation to lookup.
  • Large extrinsic context copied on every call can erase the expected savings.
  • Identity comparisons can become misleading, since two logical objects intentionally reference the same physical instance.

Mitigate these risks with bounded input domains, immutable values, canonical keys, explicit factory ownership, and performance tests under representative concurrency. If the results do not improve the measured constraint, remove the pattern — optimization code carries a continuing maintenance cost and must earn its place with production-relevant evidence.

Flyweight separates shared intrinsic state from extrinsic state supplied by clients.
Clients obtain canonical flyweights and provide context-specific state for each use.

Summary

Flyweight trades a small amount of lookup and design complexity for a large reduction in duplicated state — but only when the numbers justify it. Start by measuring object count, distinct intrinsic values, and duplicated bytes. If sharing is worthwhile, separate intrinsic state (immutable, owned by the flyweight) from extrinsic state (mutable, owned by the client), route creation through a canonical factory, and keep the two concerns visible in the API rather than blurred together. The twelve-description, 750,000-event traffic simulation is a case where the tradeoff clearly pays off; a program with a few hundred short-lived objects is usually a case where it does not.


SEMrush Software 5 SEMrush Banner 5