PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Flyweight pattern – object sharing, instance cache, Magento 2

by Henryk Tews / Tuesday, 26 May 2026 / Published in Wzorce projektowe

Flyweight is a structural pattern that minimises memory usage by sharing as much data as possible between similar objects. Instead of creating thousands of objects with repeated data – you create one object and references to it. In Magento 2 this pattern appears in many places: translations, store configuration, EAV objects. I show an implementation from scratch and how to recognise it in existing code.

The problem – thousands of identical objects

<?php

// Without Flyweight - each product creates its own config objects
class ProductWithoutFlyweight
{
    private array $taxConfig;
    private array $currencyConfig;
    private array $storeConfig;

    public function __construct(private string $sku, private float $price)
    {
        // Each of 10,000 products loads the same data
        $this->taxConfig      = ['rate' => 0.23, 'type' => 'vat', 'display' => 'gross'];
        $this->currencyConfig = ['code' => 'PLN', 'symbol' => 'zl', 'precision' => 2];
        $this->storeConfig    = ['id' => 1, 'locale' => 'pl_PL', 'timezone' => 'Europe/Warsaw'];
        // ~200 bytes * 10,000 products = ~2MB just for repeated data
    }
}

// With Flyweight - shared data exists only once
class ProductFlyweight
{
    public function __construct(
        private string $sku,
        private float $price,
        private SharedProductConfig $sharedConfig // same instance for all
    ) {}
}

Flyweight implementation

<?php

declare(strict_types=1);

// Flyweight - only immutable, shared data
final class ProductTypeConfig
{
    public function __construct(
        public readonly string $type,
        public readonly float  $taxRate,
        public readonly bool   $isShippable,
        public readonly bool   $isDownloadable,
        public readonly array  $allowedAttributes,
    ) {}
}

// Flyweight Factory - guarantees instance uniqueness
class ProductTypeConfigFactory
{
    /** @var ProductTypeConfig[] */
    private array $pool = [];

    public function get(string $type): ProductTypeConfig
    {
        if (!isset($this->pool[$type])) {
            $this->pool[$type] = $this->create($type);
        }
        return $this->pool[$type]; // always the same instance for a given type
    }

    private function create(string $type): ProductTypeConfig
    {
        return match($type) {
            'simple'   => new ProductTypeConfig(
                type: 'simple',
                taxRate: 0.23,
                isShippable: true,
                isDownloadable: false,
                allowedAttributes: ['weight', 'dimensions', 'color', 'size'],
            ),
            'virtual'  => new ProductTypeConfig(
                type: 'virtual',
                taxRate: 0.23,
                isShippable: false,
                isDownloadable: false,
                allowedAttributes: ['duration', 'access_period'],
            ),
            'downloadable' => new ProductTypeConfig(
                type: 'downloadable',
                taxRate: 0.23,
                isShippable: false,
                isDownloadable: true,
                allowedAttributes: ['file_size', 'file_type', 'max_downloads'],
            ),
            default => throw new \InvalidArgumentException("Unknown product type: {$type}"),
        };
    }

    public function poolSize(): int { return count($this->pool); }
}

// Context - data unique to each object
class Product
{
    public function __construct(
        private readonly int    $id,
        private readonly string $sku,
        private float           $price,
        private readonly ProductTypeConfig $typeConfig, // flyweight - shared
    ) {}

    public function getTaxAmount(): float
    {
        return $this->price * $this->typeConfig->taxRate;
    }

    public function canShip(): bool
    {
        return $this->typeConfig->isShippable;
    }

    public function getGrossPrice(): float
    {
        return $this->price * (1 + $this->typeConfig->taxRate);
    }
}

// Usage
$factory = new ProductTypeConfigFactory();

$products = [];
for ($i = 1; $i <= 10000; $i++) {
    $type       = ['simple', 'virtual', 'downloadable'][random_int(0, 2)];
    $products[] = new Product(
        id:         $i,
        sku:        "SKU-{$i}",
        price:      mt_rand(999, 99999) / 100,
        typeConfig: $factory->get($type), // shared instance
    );
}

echo "Products: "      . count($products) . "\n";
echo "Config objects in pool: " . $factory->poolSize() . "\n"; // always 3, not 10,000

Flyweight with extrinsic state – translations

<?php

declare(strict_types=1);

class TranslationDictionary
{
    private array $translations = [];

    public function load(string $locale): void
    {
        $file = __DIR__ . "/i18n/{$locale}.csv";
        if (!file_exists($file)) return;

        foreach (file($file, FILE_IGNORE_NEW_LINES) as $line) {
            [$key, $value] = str_getcsv($line);
            $this->translations[$locale][$key] = $value;
        }
    }

    public function translate(string $key, string $locale): string
    {
        return $this->translations[$locale][$key] ?? $key;
    }
}

class TranslationDictionaryFactory
{
    private array $dictionaries = [];

    public function __construct(private TranslationDictionary $dictionary) {}

    public function getForLocale(string $locale): TranslationProxy
    {
        if (!isset($this->dictionaries[$locale])) {
            $this->dictionary->load($locale);
            $this->dictionaries[$locale] = new TranslationProxy($this->dictionary, $locale);
        }
        return $this->dictionaries[$locale];
    }
}

class TranslationProxy
{
    public function __construct(
        private TranslationDictionary $dictionary, // flyweight
        private string $locale,                    // extrinsic state
    ) {}

    public function __($key): string
    {
        return $this->dictionary->translate($key, $this->locale);
    }
}

Flyweight in Magento 2 – where to find it

<?php

// 1. Magento\Eav\Model\Config - EAV attribute cache
//    Instead of loading the same attributes for each product - cache in Config object

/** @var \Magento\Eav\Model\Config $eavConfig */
$attribute = $eavConfig->getAttribute('catalog_product', 'color');
// Second call - returns the same instance from cache, does not load from DB

// 2. Magento\Store\Model\StoreManagerInterface
//    One Store object shared throughout the request

// 3. Flyweight implementation in custom module - on-demand cache
class AttributeOptionCache
{
    private array $cache = [];

    public function __construct(
        private \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory $factory
    ) {}

    public function getOptions(int $attributeId): array
    {
        if (!isset($this->cache[$attributeId])) {
            $collection = $this->factory->create();
            $collection->setAttributeFilter($attributeId);
            $collection->setStoreFilter(0);

            $this->cache[$attributeId] = $collection->toOptionArray();
        }

        return $this->cache[$attributeId]; // same array for every call
    }
}

When to use Flyweight

Criterion Flyweight fits Flyweight unnecessary
Number of objects Thousands of similar objects A few dozen
Repeated data Large portion of state is shared Every object is unique
Memory RAM is a problem Memory is not an issue
Immutable data Shared data is readonly State changes frequently

Summary

Flyweight solves a specific problem: too many objects with repeated data consuming too much memory. The key is splitting into intrinsic state (shared, immutable – the Flyweight) and extrinsic state (unique to each context). The Factory guarantees that each unique intrinsic state exists exactly once. In PHP this pattern is particularly valuable in CLI scripts processing large collections – product imports, report generation, bulk order operations.

About Henryk Tews

What you can read next

CQRS – Command Bus, Query Bus, read models, Magento 2 integration
GoF patterns in Magento 2 – where to find them and how they work
Template Method – algorithm skeleton, hooks, abstract vs hook, comparison with Strategy

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}