Loading learning content...
Understanding the Flyweight Pattern conceptually is one thing; applying it in production systems is another. The pattern appears in more places than you might expect—from the JDK itself to game engines, from browser rendering to enterprise applications.\n\nIn this page, we'll implement complete, production-quality Flyweight examples across several domains. Each example will demonstrate the full pattern structure: flyweight interfaces, concrete flyweights, factories, and client code. We'll also examine real-world systems that use Flyweight internally, showing how the pattern scales from academic examples to systems serving millions of users.
By the end of this page, you will have seen complete Flyweight implementations for: text rendering, game particle systems, icon libraries, and database connection pools. You'll also understand Flyweight usage in real-world systems like Java's Integer cache, string interning, and browser DOM rendering.
Our first complete implementation covers the text rendering scenario we've discussed throughout this module. This is perhaps the canonical Flyweight example.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
import java.util.*;import java.util.concurrent.ConcurrentHashMap; /** * Complete Text Rendering Flyweight Implementation * * Demonstrates sharing character styles across thousands of characters * in a document rendering system. */ // =============================================// FLYWEIGHT INTERFACE// =============================================public interface CharacterGlyph { /** * Render a character at the given position. * Position is extrinsic state—passed in, not stored. */ void render(Canvas canvas, char character, int x, int y); /** * Get the advance width for a character (how far to move cursor after). */ int getAdvanceWidth(char character); /** * Get the line height for this style. */ int getLineHeight();} // =============================================// CONCRETE FLYWEIGHT// =============================================public final class TextStyle implements CharacterGlyph { // All intrinsic state — immutable after construction private final String fontFamily; private final int fontSize; private final int fontWeight; // 400 = normal, 700 = bold private final boolean italic; private final int textColor; // ARGB packed private final int backgroundColor; // ARGB packed, 0 = transparent private final boolean underline; private final boolean strikethrough; // Cached derived data (computed once from intrinsic state) private final FontMetrics metrics; private final int lineHeight; TextStyle(String fontFamily, int fontSize, int fontWeight, boolean italic, int textColor, int backgroundColor, boolean underline, boolean strikethrough) { this.fontFamily = fontFamily; this.fontSize = fontSize; this.fontWeight = fontWeight; this.italic = italic; this.textColor = textColor; this.backgroundColor = backgroundColor; this.underline = underline; this.strikethrough = strikethrough; // Pre-compute metrics for this style this.metrics = FontMetrics.create(fontFamily, fontSize, fontWeight, italic); this.lineHeight = (int)(fontSize * 1.2); } @Override public void render(Canvas canvas, char character, int x, int y) { // Apply styling if (backgroundColor != 0) { int charWidth = getAdvanceWidth(character); canvas.fillRect(x, y - fontSize, charWidth, lineHeight, backgroundColor); } // Draw character canvas.setFont(fontFamily, fontSize, fontWeight, italic); canvas.setColor(textColor); canvas.drawChar(character, x, y); // Apply decorations if (underline) { int width = getAdvanceWidth(character); canvas.drawLine(x, y + 2, x + width, y + 2, textColor); } if (strikethrough) { int width = getAdvanceWidth(character); canvas.drawLine(x, y - fontSize/3, x + width, y - fontSize/3, textColor); } } @Override public int getAdvanceWidth(char character) { return metrics.getCharWidth(character); } @Override public int getLineHeight() { return lineHeight; } // Getters for intrinsic state (needed for factory key generation) String getFontFamily() { return fontFamily; } int getFontSize() { return fontSize; } int getFontWeight() { return fontWeight; } boolean isItalic() { return italic; } int getTextColor() { return textColor; } int getBackgroundColor() { return backgroundColor; } boolean isUnderline() { return underline; } boolean isStrikethrough() { return strikethrough; }} // =============================================// FLYWEIGHT FACTORY// =============================================public final class TextStyleFactory { private static final TextStyleFactory INSTANCE = new TextStyleFactory(); private final Map<String, TextStyle> styleCache = new ConcurrentHashMap<>(); // Common style constants public static final int COLOR_BLACK = 0xFF000000; public static final int COLOR_WHITE = 0xFFFFFFFF; public static final int COLOR_TRANSPARENT = 0x00000000; public static final int WEIGHT_NORMAL = 400; public static final int WEIGHT_BOLD = 700; private TextStyleFactory() {} public static TextStyleFactory getInstance() { return INSTANCE; } /** * Get or create a TextStyle flyweight. */ public TextStyle getStyle(String fontFamily, int fontSize, int fontWeight, boolean italic, int textColor, int backgroundColor, boolean underline, boolean strikethrough) { String key = generateKey(fontFamily, fontSize, fontWeight, italic, textColor, backgroundColor, underline, strikethrough); return styleCache.computeIfAbsent(key, k -> new TextStyle(fontFamily, fontSize, fontWeight, italic, textColor, backgroundColor, underline, strikethrough) ); } // Convenience methods for common styles public TextStyle getDefaultStyle() { return getStyle("Arial", 12, WEIGHT_NORMAL, false, COLOR_BLACK, COLOR_TRANSPARENT, false, false); } public TextStyle getBoldStyle(String fontFamily, int fontSize) { return getStyle(fontFamily, fontSize, WEIGHT_BOLD, false, COLOR_BLACK, COLOR_TRANSPARENT, false, false); } public TextStyle getLinkStyle(String fontFamily, int fontSize) { return getStyle(fontFamily, fontSize, WEIGHT_NORMAL, false, 0xFF0066CC, COLOR_TRANSPARENT, true, false); } public TextStyle getCodeStyle() { return getStyle("Consolas", 11, WEIGHT_NORMAL, false, 0xFF333333, 0xFFF5F5F5, false, false); } private String generateKey(String fontFamily, int fontSize, int fontWeight, boolean italic, int textColor, int backgroundColor, boolean underline, boolean strikethrough) { return String.format("%s|%d|%d|%b|%08X|%08X|%b|%b", fontFamily, fontSize, fontWeight, italic, textColor, backgroundColor, underline, strikethrough); } // Statistics for debugging/monitoring public int getCachedStyleCount() { return styleCache.size(); } public void clearCache() { styleCache.clear(); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
// =============================================// CLIENT CODE — Document Renderer// =============================================public class DocumentRenderer { /** * Lightweight container for extrinsic state. * This is what we store per character instead of full objects. */ record GlyphPosition(char character, int x, int y, TextStyle style) { // ~16-20 bytes vs ~150+ bytes for full character object } private final TextStyleFactory styleFactory; private final List<GlyphPosition> glyphs; private final int pageWidth; private final int pageHeight; public DocumentRenderer(int pageWidth, int pageHeight) { this.styleFactory = TextStyleFactory.getInstance(); this.glyphs = new ArrayList<>(); this.pageWidth = pageWidth; this.pageHeight = pageHeight; } /** * Load rich text content and prepare for rendering. */ public void loadContent(List<RichTextRun> runs) { glyphs.clear(); int x = 0; int y = 20; // Start with top margin int lineHeight = 0; for (RichTextRun run : runs) { // Get the appropriate flyweight for this run's styling TextStyle style = styleFactory.getStyle( run.getFontFamily(), run.getFontSize(), run.isBold() ? TextStyleFactory.WEIGHT_BOLD : TextStyleFactory.WEIGHT_NORMAL, run.isItalic(), run.getTextColor(), run.getBackgroundColor(), run.isUnderlined(), run.isStrikethrough() ); lineHeight = Math.max(lineHeight, style.getLineHeight()); // Process each character in the run for (char c : run.getText().toCharArray()) { if (c == '\n') { // Line break x = 0; y += lineHeight; lineHeight = style.getLineHeight(); continue; } int charWidth = style.getAdvanceWidth(c); // Word wrap check if (x + charWidth > pageWidth) { x = 0; y += lineHeight; lineHeight = style.getLineHeight(); } // Store glyph position with reference to shared style glyphs.add(new GlyphPosition(c, x, y, style)); x += charWidth; } } System.out.printf("Loaded %,d glyphs using %d unique styles%n", glyphs.size(), styleFactory.getCachedStyleCount()); } /** * Render all glyphs to canvas. */ public void render(Canvas canvas) { for (GlyphPosition glyph : glyphs) { // Client provides extrinsic state (char, position) // Flyweight uses its intrinsic state (styling) to render glyph.style().render(canvas, glyph.character(), glyph.x(), glyph.y()); } } /** * Memory usage comparison. */ public void printMemoryAnalysis() { int glyphCount = glyphs.size(); int styleCount = styleFactory.getCachedStyleCount(); // Naive approach: full object per character long naiveBytes = glyphCount * 150L; // Estimated 150 bytes/char object // Flyweight approach: position record + shared styles long flyweightBytes = (glyphCount * 20L) + (styleCount * 200L); System.out.println("\n=== Memory Analysis ==="); System.out.printf("Glyphs: %,d%n", glyphCount); System.out.printf("Unique styles: %d%n", styleCount); System.out.printf("Naive approach: %,.1f KB%n", naiveBytes / 1024.0); System.out.printf("Flyweight approach: %,.1f KB%n", flyweightBytes / 1024.0); System.out.printf("Memory saved: %.1f%%%n", (1.0 - (double)flyweightBytes / naiveBytes) * 100); }} // =============================================// USAGE EXAMPLE// =============================================public class TextRenderingDemo { public static void main(String[] args) { DocumentRenderer renderer = new DocumentRenderer(800, 1200); // Simulate loading a document with various formatting List<RichTextRun> document = List.of( new RichTextRun("Chapter 1: Introduction\n\n", "Times New Roman", 24, true, false), new RichTextRun("This is the opening paragraph with ", "Arial", 12, false, false), new RichTextRun("bold text", "Arial", 12, true, false), new RichTextRun(" and ", "Arial", 12, false, false), new RichTextRun("italic text", "Arial", 12, false, true), new RichTextRun(".\n\n", "Arial", 12, false, false), // ... thousands more runs in a real document ); renderer.loadContent(document); renderer.printMemoryAnalysis(); // Output: // Loaded 87,432 glyphs using 7 unique styles // // === Memory Analysis === // Glyphs: 87,432 // Unique styles: 7 // Naive approach: 12,812.7 KB // Flyweight approach: 1,709.4 KB // Memory saved: 86.7% }}Particle systems in games present an excellent Flyweight opportunity: thousands of particles sharing the same visual properties (texture, blend mode) but with unique positions, velocities, and lifetimes.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
/** * Particle System Flyweight Implementation * * Demonstrates efficient handling of thousands of particles * in a game effect system. */ // =============================================// FLYWEIGHT INTERFACE// =============================================public interface ParticleType { /** * Render a particle at the given position with extrinsic state. */ void render(SpriteBatch batch, float x, float y, float scale, float alpha, float rotation); /** * Apply physics update using type-specific gravity. */ Vector2 applyGravity(Vector2 velocity, float deltaTime); /** * Get the base color for this particle type. */ int getBaseColor();} // =============================================// CONCRETE FLYWEIGHT// =============================================public final class ParticleTemplate implements ParticleType { // All intrinsic state — shared across all particles of this type private final Texture texture; private final BlendMode blendMode; private final Vector2 gravity; private final int baseColor; private final float baseSize; private final float lifetimeMin; private final float lifetimeMax; private final boolean looping; // Cached texture region for efficient rendering private final TextureRegion textureRegion; ParticleTemplate(Texture texture, BlendMode blendMode, Vector2 gravity, int baseColor, float baseSize, float lifetimeMin, float lifetimeMax, boolean looping) { this.texture = texture; this.blendMode = blendMode; this.gravity = gravity; this.baseColor = baseColor; this.baseSize = baseSize; this.lifetimeMin = lifetimeMin; this.lifetimeMax = lifetimeMax; this.looping = looping; this.textureRegion = new TextureRegion(texture); } @Override public void render(SpriteBatch batch, float x, float y, float scale, float alpha, float rotation) { batch.setBlendFunction(blendMode); // Compute final color with alpha from extrinsic state int color = ColorUtils.withAlpha(baseColor, alpha); batch.setColor(color); // Compute size from base (intrinsic) and scale (extrinsic) float size = baseSize * scale; // Draw at position (extrinsic) with rotation (extrinsic) batch.draw(textureRegion, x - size/2, y - size/2, // Center origin size/2, size/2, // Origin for rotation size, size, // Size 1f, 1f, // No additional scale rotation); // Rotation in degrees } @Override public Vector2 applyGravity(Vector2 velocity, float deltaTime) { return new Vector2( velocity.x + gravity.x * deltaTime, velocity.y + gravity.y * deltaTime ); } @Override public int getBaseColor() { return baseColor; } public float randomLifetime() { return lifetimeMin + (float)Math.random() * (lifetimeMax - lifetimeMin); } public boolean isLooping() { return looping; }} // =============================================// FLYWEIGHT FACTORY// =============================================public final class ParticleTypeFactory { private static final ParticleTypeFactory INSTANCE = new ParticleTypeFactory(); private final Map<String, ParticleTemplate> typeCache = new HashMap<>(); private final TextureManager textures; private ParticleTypeFactory() { this.textures = TextureManager.getInstance(); initializeBuiltInTypes(); } public static ParticleTypeFactory getInstance() { return INSTANCE; } private void initializeBuiltInTypes() { // Pre-register common particle types register("fire", new ParticleTemplate( textures.get("particles/flame.png"), BlendMode.ADDITIVE, new Vector2(0, 50), // Upward drift 0xFFFF6600, // Orange 32f, 0.5f, 1.5f, false )); register("smoke", new ParticleTemplate( textures.get("particles/smoke.png"), BlendMode.ALPHA, new Vector2(0, 30), 0x80888888, // Gray, semi-transparent 24f, 1.0f, 3.0f, false )); register("spark", new ParticleTemplate( textures.get("particles/spark.png"), BlendMode.ADDITIVE, new Vector2(0, -200), // Falls down 0xFFFFFF00, // Yellow 8f, 0.2f, 0.8f, false )); register("rain", new ParticleTemplate( textures.get("particles/raindrop.png"), BlendMode.ALPHA, new Vector2(20, -500), // Falls fast, slight wind 0xBB6699FF, 4f, 0.5f, 2.0f, true )); } public void register(String name, ParticleTemplate template) { typeCache.put(name, template); } public ParticleTemplate get(String typeName) { ParticleTemplate template = typeCache.get(typeName); if (template == null) { throw new IllegalArgumentException("Unknown particle type: " + typeName); } return template; } public int getTypeCount() { return typeCache.size(); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
// =============================================// EXTRINSIC STATE — Particle Instance Data// ============================================= /** * Compact representation of per-particle extrinsic state. * Uses parallel arrays (Struct-of-Arrays) for cache efficiency. */public class ParticlePool { // Maximum particles this pool can handle private final int capacity; // Extrinsic state arrays — one value per particle private final float[] x, y; // Position private final float[] vx, vy; // Velocity private final float[] scale; // Size multiplier private final float[] alpha; // Transparency private final float[] rotation; // Rotation in degrees private final float[] rotationSpeed; // Rotation velocity private final float[] age; // Current age in seconds private final float[] lifetime; // Max lifetime private final boolean[] alive; // Is this slot active? // Shared reference to the flyweight private final ParticleTemplate type; // Pool bookkeeping private int aliveCount = 0; private int nextSlot = 0; public ParticlePool(ParticleTemplate type, int capacity) { this.type = type; this.capacity = capacity; this.x = new float[capacity]; this.y = new float[capacity]; this.vx = new float[capacity]; this.vy = new float[capacity]; this.scale = new float[capacity]; this.alpha = new float[capacity]; this.rotation = new float[capacity]; this.rotationSpeed = new float[capacity]; this.age = new float[capacity]; this.lifetime = new float[capacity]; this.alive = new boolean[capacity]; } /** * Spawn a new particle with given initial state. */ public void spawn(float px, float py, float pvx, float pvy, float pscale, float protSpeed) { if (aliveCount >= capacity) { // Pool full — could expand or skip return; } // Find a free slot while (alive[nextSlot]) { nextSlot = (nextSlot + 1) % capacity; } int i = nextSlot; x[i] = px; y[i] = py; vx[i] = pvx; vy[i] = pvy; scale[i] = pscale; alpha[i] = 1.0f; rotation[i] = (float)(Math.random() * 360); rotationSpeed[i] = protSpeed; age[i] = 0; lifetime[i] = type.randomLifetime(); alive[i] = true; aliveCount++; nextSlot = (nextSlot + 1) % capacity; } /** * Update all particles. */ public void update(float deltaTime) { for (int i = 0; i < capacity; i++) { if (!alive[i]) continue; // Age particle age[i] += deltaTime; if (age[i] >= lifetime[i]) { // Particle died alive[i] = false; aliveCount--; continue; } // Calculate life progress (0 to 1) float progress = age[i] / lifetime[i]; // Apply alpha fade (fade out in last 30% of life) if (progress > 0.7f) { alpha[i] = 1.0f - (progress - 0.7f) / 0.3f; } // Apply gravity from flyweight Vector2 gravity = type.applyGravity(new Vector2(vx[i], vy[i]), deltaTime); vx[i] = gravity.x; vy[i] = gravity.y; // Update position x[i] += vx[i] * deltaTime; y[i] += vy[i] * deltaTime; // Update rotation rotation[i] += rotationSpeed[i] * deltaTime; } } /** * Render all particles using the shared flyweight. */ public void render(SpriteBatch batch) { for (int i = 0; i < capacity; i++) { if (!alive[i]) continue; // Flyweight (type) provides rendering with intrinsic state // We pass extrinsic state from our arrays type.render(batch, x[i], y[i], scale[i], alpha[i], rotation[i]); } } public int getAliveCount() { return aliveCount; } public int getCapacity() { return capacity; }} // =============================================// CLIENT CODE — Particle Emitter// =============================================public class ParticleEmitter { private final ParticlePool pool; private final float emitX, emitY; private final float emitRate; // Particles per second private float accumulator = 0; public ParticleEmitter(String particleType, int maxParticles, float x, float y, float particlesPerSecond) { ParticleTemplate type = ParticleTypeFactory.getInstance().get(particleType); this.pool = new ParticlePool(type, maxParticles); this.emitX = x; this.emitY = y; this.emitRate = particlesPerSecond; } public void update(float deltaTime) { // Emit new particles accumulator += deltaTime * emitRate; while (accumulator >= 1.0f) { float vx = (float)(Math.random() - 0.5) * 100; float vy = (float)(Math.random()) * 50; float scale = 0.5f + (float)Math.random() * 0.5f; float rotSpeed = (float)(Math.random() - 0.5) * 180; pool.spawn(emitX, emitY, vx, vy, scale, rotSpeed); accumulator -= 1.0f; } // Update existing particles pool.update(deltaTime); } public void render(SpriteBatch batch) { pool.render(batch); } public void printStats() { System.out.printf("Emitter: %d/%d particles, using 1 flyweight%n", pool.getAliveCount(), pool.getCapacity()); }}Notice the ParticlePool uses parallel arrays rather than an array of particle objects. This 'Struct-of-Arrays' layout keeps data compact (no object headers per particle) and improves cache coherence when iterating. It's a common optimization paired with Flyweight in performance-critical game code.
Icon libraries in web and desktop applications often display the same icons many times across the UI. A file browser showing 1,000 files might render 1,000 folder icons—but the folder icon SVG/image data is identical each time.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
/** * Icon Library Flyweight Implementation * * Demonstrates sharing icon definitions across multiple UI usages. */ // =============================================// FLYWEIGHT INTERFACE// =============================================interface Icon { /** Render the icon at given position with size and color (extrinsic) */ render(context: CanvasRenderingContext2D, x: number, y: number, size: number, color: string): void; /** Get SVG path data for the icon */ getPathData(): string; /** Get semantic name for accessibility */ getName(): string;} // =============================================// CONCRETE FLYWEIGHT// =============================================class SVGIcon implements Icon { // Intrinsic state: the icon's definition private readonly name: string; private readonly pathData: string; private readonly viewBox: { width: number; height: number }; // Cached path for efficient repeated rendering private readonly path2D: Path2D; constructor(name: string, pathData: string, viewBoxWidth: number = 24, viewBoxHeight: number = 24) { this.name = name; this.pathData = pathData; this.viewBox = { width: viewBoxWidth, height: viewBoxHeight }; // Pre-parse the path for efficient rendering this.path2D = new Path2D(pathData); } render(context: CanvasRenderingContext2D, x: number, y: number, size: number, color: string): void { context.save(); // Move to position and scale context.translate(x, y); const scale = size / this.viewBox.width; context.scale(scale, scale); // Set color and render context.fillStyle = color; context.fill(this.path2D); context.restore(); } getPathData(): string { return this.pathData; } getName(): string { return this.name; }} // =============================================// FLYWEIGHT FACTORY// =============================================class IconLibrary { private static instance: IconLibrary; private readonly icons: Map<string, SVGIcon> = new Map(); private constructor() { this.registerBuiltInIcons(); } static getInstance(): IconLibrary { if (!this.instance) { this.instance = new IconLibrary(); } return this.instance; } private registerBuiltInIcons(): void { // Register common icons with their SVG path data this.register(new SVGIcon("folder", "M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 " + "2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" )); this.register(new SVGIcon("file", "M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 " + "2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z" )); this.register(new SVGIcon("search", "M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 " + "0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 " + "5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" )); this.register(new SVGIcon("delete", "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 " + "1H5v2h14V4z" )); // Many more icons... } register(icon: SVGIcon): void { this.icons.set(icon.getName(), icon); } get(name: string): SVGIcon { const icon = this.icons.get(name); if (!icon) { throw new Error(`Unknown icon: ${name}`); } return icon; } getIconCount(): number { return this.icons.size; }} // =============================================// CLIENT CODE — File Browser// =============================================interface FileEntry { name: string; type: "file" | "folder"; x: number; y: number;} class FileBrowser { private readonly iconLibrary: IconLibrary; private readonly entries: FileEntry[] = []; private readonly iconSize = 24; private readonly itemHeight = 32; constructor() { this.iconLibrary = IconLibrary.getInstance(); } addEntry(name: string, type: "file" | "folder"): void { const y = this.entries.length * this.itemHeight; this.entries.push({ name, type, x: 8, y }); } render(context: CanvasRenderingContext2D): void { // Get flyweights once const folderIcon = this.iconLibrary.get("folder"); const fileIcon = this.iconLibrary.get("file"); for (const entry of this.entries) { // Select icon flyweight based on type const icon = entry.type === "folder" ? folderIcon : fileIcon; const color = entry.type === "folder" ? "#FFC107" : "#757575"; // Render using flyweight with extrinsic state (position, color) icon.render(context, entry.x, entry.y, this.iconSize, color); // Render filename context.fillStyle = "#212121"; context.fillText(entry.name, entry.x + this.iconSize + 8, entry.y + 16); } } printStats(): void { console.log(`Showing ${this.entries.length} items using ` + `${this.iconLibrary.getIconCount()} flyweight icons`); }} // =============================================// USAGE// =============================================const browser = new FileBrowser(); // Simulate loading 1,000 filesfor (let i = 0; i < 1000; i++) { if (i % 10 === 0) { browser.addEntry(`Folder ${i}`, "folder"); } else { browser.addEntry(`Document ${i}.pdf`, "file"); }} browser.printStats();// Output: Showing 1000 items using 2 flyweight icons // Memory impact:// Without Flyweight: 1000 × ~2KB (path data + parsed path) = ~2 MB// With Flyweight: 2 × ~2KB = ~4 KB (plus small entry objects)The Flyweight Pattern isn't just an academic exercise—it's embedded in foundational systems you use every day.
Java's Integer.valueOf() implements Flyweight for commonly used integer values:\n\njava\n// Java's Integer class (simplified)\npublic final class Integer {\n // Flyweight cache for values -128 to 127\n private static class IntegerCache {\n static final Integer[] cache = new Integer[256];\n \n static {\n for (int i = 0; i < cache.length; i++) {\n cache[i] = new Integer(i - 128);\n }\n }\n }\n \n public static Integer valueOf(int i) {\n if (i >= -128 && i <= 127) {\n return IntegerCache.cache[i + 128]; // Return flyweight\n }\n return new Integer(i); // Create new for non-cached\n }\n}\n\n\nImpact: Autoboxing of small integers (loop counters, enum ordinals, etc.) reuses shared Integer objects, dramatically reducing object creation in typical Java code.
Like all patterns, Flyweight has appropriate and inappropriate applications. Using it in the wrong context adds complexity without benefit.
| Scenario | Use Flyweight? | Reason |
|---|---|---|
| 10,000+ similar objects | Yes | Clear memory savings |
| 100 business entities | No | Overhead exceeds benefit |
| Objects share >70% state | Yes | High sharing ratio |
| Objects share <20% state | No | Low sharing payoff |
| Embedded/mobile app | Likely yes | Memory is constrained |
| Backend microservice | Unlikely | Usually plenty of RAM |
| Game with particle effects | Yes | Classic Flyweight domain |
| REST API response objects | No | Short-lived, unique data |
Never apply Flyweight based on intuition alone. Profile your application, identify actual memory hotspots, and verify that shared state exists. Premature Flyweight optimization is particularly harmful because it distributes complexity throughout your codebase.
The classic Flyweight Pattern can be adapted and combined with other patterns for specific needs.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
/** * Composite Flyweight: flyweights that reference other flyweights. */public class CompositeTextStyle implements CharacterGlyph { // Reference to another flyweight (font definition) private final FontFlyweight font; // Shared among styles using same font // This style's specific intrinsic state private final int textColor; private final boolean underline; CompositeTextStyle(FontFlyweight font, int textColor, boolean underline) { this.font = font; // Reuse shared font flyweight this.textColor = textColor; this.underline = underline; } @Override public void render(Canvas canvas, char character, int x, int y) { // Delegate to composed flyweight for font rendering canvas.setFont(font.getFamily(), font.getSize(), font.getWeight()); canvas.setColor(textColor); canvas.drawChar(character, x, y); if (underline) { int width = font.getCharWidth(character); canvas.drawLine(x, y + 2, x + width, y + 2, textColor); } } @Override public int getAdvanceWidth(char character) { return font.getCharWidth(character); } @Override public int getLineHeight() { return font.getLineHeight(); }} /** * Font flyweight shared by multiple TextStyle flyweights. */public class FontFlyweight { private final String family; private final int size; private final int weight; private final FontMetrics metrics; // Factory for font flyweights private static final Map<String, FontFlyweight> fontCache = new ConcurrentHashMap<>(); private FontFlyweight(String family, int size, int weight) { this.family = family; this.size = size; this.weight = weight; this.metrics = FontMetrics.load(family, size, weight); } public static FontFlyweight get(String family, int size, int weight) { String key = family + "_" + size + "_" + weight; return fontCache.computeIfAbsent(key, k -> new FontFlyweight(family, size, weight)); } // Accessor methods... public String getFamily() { return family; } public int getSize() { return size; } public int getWeight() { return weight; } public int getCharWidth(char c) { return metrics.getWidth(c); } public int getLineHeight() { return metrics.getLineHeight(); }}When implementing Flyweight in your applications, follow these guidelines for success:
We've journeyed from the problem of object explosion through the elegant Flyweight solution, the critical intrinsic/extrinsic distinction, and comprehensive real-world implementations. Let's consolidate our mastery:
| Aspect | Details |
|---|---|
| Intent | Share fine-grained objects efficiently by separating intrinsic from extrinsic state |
| Key Participants | Flyweight, ConcreteFlyweight, FlyweightFactory, Client |
| Intrinsic State | Stored in flyweight; shared; immutable; context-independent |
| Extrinsic State | Stored/computed by client; passed to flyweight operations; context-dependent |
| When to Use | Many objects; significant state sharing; separable state; memory-constrained |
| When NOT to Use | Few objects; unique state; CPU-bound; short-lived objects |
| Related Patterns | Composite, Factory Method, Singleton (for factory), Object Pool |
You now possess a complete, production-ready understanding of the Flyweight Pattern. You can identify candidates for the pattern, correctly classify intrinsic and extrinsic state, implement thread-safe factories, and apply the pattern across diverse domains. This knowledge will serve you well in building scalable, memory-efficient applications—from document editors to game engines to enterprise systems.