Prototype & Object Pool Pattern in Magento 2
AI generated
mironsoft.deBlogDesign Patterns
Magento 2 · Design Patterns
Prototype & Object Pool
Pattern in Magento 2

The Prototype Pattern clones existing objects instead of re-instantiating them. The Object Pool Pattern manages a stock of reusable, expensive objects. Both patterns serve performance, and both are deeply rooted in the Magento 2 framework.

12 min read PHP 8.4 Magento 2.4.8

The PHP clone() mechanism in detail

In PHP, the Prototype Pattern is built on a language mechanism baked right into the runtime: the clone keyword. When you call clone $object, PHP internally creates a new instance of the same class and copies all properties from the original. No constructor is invoked in the process, which is the key difference compared to new. Afterward, PHP calls the magic method __clone() on the freshly created clone, provided it is defined.

The Prototype Pattern is one of the classic creational patterns from the GoF (Gang of Four) book. It is well suited whenever fully initializing an object is expensive (for example because it requires database queries, network communication, or complex calculations) and when many similar objects that differ only in a few details are needed. Instead of repeating every step of initialization, you take an already fully configured object as a template and create a copy of it.

In PHP 8.4 the clone mechanism itself is unchanged, but it is complemented by new language features. Since PHP 8.3, readonly properties can be written inside __clone(), which previously triggered an error. This enables genuinely immutable value objects that can still be cloned and slightly modified. PHP 8.4 additionally introduces the clone with syntax, which allows properties to be overwritten directly during cloning without having to write a __clone() method.

In Magento 2.4.8, the PHP clone mechanism is deliberately used in several places, sometimes obviously, sometimes hidden deep inside the framework. To understand why, you first need to understand the difference between shallow copy and deep copy.

<?php
declare(strict_types=1);

// PHP 8.4: clone with, direct property override while cloning
class ProductPrice
{
    public function __construct(
        public readonly string $sku,
        public readonly float $price,
        public readonly string $currency = 'EUR'
    ) {}
}

$basePrice = new ProductPrice('SKU-001', 29.99, 'EUR');

// PHP 8.4: clone with overrides properties without __clone()
$priceUsd = clone $basePrice with { currency: 'USD', price: 32.50 };

// Classic approach: __clone() for more complex objects
class QuoteItemPrototype
{
    public function __construct(
        private string $sku,
        private float $qty,
        private ?AddressData $shippingAddress = null
    ) {}

    // Deep clone: explicitly clone nested objects
    public function __clone(): void
    {
        if ($this->shippingAddress !== null) {
            $this->shippingAddress = clone $this->shippingAddress;
        }
    }
}

Shallow copy vs. deep copy: the crucial difference

PHP's clone produces a shallow copy by default. That means all properties with scalar types (int, float, string, bool, null) are copied in full and exist independently in the clone. Arrays are copied too, and their direct values are independent. However, if an array contains objects, those objects are not cloned, they are carried over by reference.

Object properties are where things get critical: PHP does not copy the object itself, only the pointer to it. After a simple clone, the original and the clone point to the very same instance of the nested object. A mutation on the inner object, whether performed through the original or through the clone, affects both at the same time. In most cases that is not the desired behavior.

A deep copy recursively clones all contained objects as well. In PHP you implement this by overriding the __clone() method. Inside it, every object property is explicitly given a clone treatment. If the inner object contains objects of its own, that object also needs a __clone() method for the deep-copy chain to be complete.

In Magento 2, most model objects (Product, Quote, Order) are instances of Magento\Framework\DataObject, which internally uses an array to hold all data. Since arrays are copied on a shallow copy, the simple data values are independent after a clone. Extension attributes, on the other hand, are objects, and here extra caution is warranted.

The Prototype Pattern in Magento 2: cloning quote items

The best-known example of the Prototype Pattern in Magento 2 lives in the cart system. When a configurable product is added to the cart, Magento internally creates two quote items: one for the configurable product itself (the parent item) and one for the concrete simple product variant (the child item). The child item is not fully initialized from scratch, it is created as a clone of the parent item.

This approach is a deliberate choice. The parent item already has all relevant properties set: quantity, quote reference, store scope, custom price and other fields. The child item is meant to inherit these values. Instead of manually transferring every value, Magento creates a clone and adjusts only the differing properties, namely the concrete product reference and the parent-child relationship.

The product type pool in Magento 2 also relies on the prototype concept. The class Magento\Catalog\Model\Product\Type holds prototype instances of the individual TypeModel classes (Simple, Configurable, Bundle, Virtual, Downloadable). On first access to a product type, the instance is created and cached. Every subsequent request receives the very same instance back. Since TypeModels are stateless, no cloning is necessary here, the prototype instance can be reused directly.

Another use case is batch import: when importing thousands of similar products, a fully configured base product object can serve as a prototype. For every row in the import, it is cloned, and only the SKU, price, and variant-specific attributes are overwritten. This saves the full object initialization, including all its default values, for every single record.

<?php
declare(strict_types=1);

namespace Mironsoft\Import\Model;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\Data\ProductInterfaceFactory;

/**
 * Prototype-based product import processor.
 * Clones a pre-configured base product for each import row.
 */
class ProductImportProcessor
{
    private ?ProductInterface $prototype = null;

    public function __construct(
        private readonly ProductInterfaceFactory $productFactory,
        private readonly array $defaultAttributes = []
    ) {}

    /**
     * Get or build the prototype product with shared default configuration.
     */
    private function getPrototype(): ProductInterface
    {
        if ($this->prototype === null) {
            $this->prototype = $this->productFactory->create();
            $this->prototype->setTypeId('simple');
            $this->prototype->setAttributeSetId(4);
            $this->prototype->setStatus(1);
            $this->prototype->setVisibility(4);

            foreach ($this->defaultAttributes as $code => $value) {
                $this->prototype->setData($code, $value);
            }
        }

        // Prototype Pattern: clone instead of new for every import item
        return clone $this->prototype;
    }

    /**
     * Process a single import row using the cloned prototype.
     */
    public function processRow(array $row): ProductInterface
    {
        $product = $this->getPrototype();
        $product->setSku($row['sku']);
        $product->setName($row['name']);
        $product->setPrice((float) $row['price']);

        return $product;
    }
}

Why clone() instead of new: the performance perspective

Choosing clone over new is not a matter of elegance, it is a matter of performance. If an object's constructor runs database queries, calls external services, or performs expensive calculations, every new call repeats those costs. clone, on the other hand, is a pure memory operation: PHP copies the object's property table without running the constructor at all.

For simple PHP objects without expensive initialization, the difference is minimal. For Magento objects that are built via the DI container and, in the process, invoke factory classes, configuration readers and other dependent services, the difference can be measurable, especially in loops over thousands of records. Import jobs, reindex processes and batch CLI commands benefit from this the most.

There is another aspect worth noting: with clone you capture the current state of the prototype at a specific point in time. All default values, configuration parameters and shared properties are transferred in a single step. This reduces the risk of errors when setting values manually: whatever is forgotten is automatically present anyway, because it was inherited from the prototype.

Magento also applies this principle together with factories: a factory always creates a new instance with create(). When the factory internally works with a prototype and clones it instead of fully instantiating it, you combine a clean factory API with the efficiency of the Prototype Pattern. This pattern is used implicitly in several Magento core factories.

When clone() is dangerous: reference traps in Magento objects

The Prototype Pattern carries real risks if you ignore the shallow-copy nature of PHP's clone. Magento objects are especially prone to this because they frequently contain nested objects that get passed around as references. Extension attributes are a classic example: a ProductInterface can return an extension attribute object via getExtensionAttributes(). If the product is cloned without __clone(), the original and the clone share the very same extension attribute object.

It becomes particularly dangerous when the clone is passed to another part of the code and that code modifies the extension attribute object there. The change then silently affects the original as well. In a Magento system that processes thousands of requests, where observers, plugins and repositories access objects jointly, these kinds of reference bugs can cause problems that are extremely hard to debug.

Quote address objects, order items with product references, and custom data objects can be affected as well. Whenever you use clone on a Magento object, you should implement the __clone() method and explicitly clone every object property. It is more work, but considerably safer.

There is another risk with objects that carry a database identity: if you clone a persisted product and then save the clone, you either end up with a new product that has the same ID, or you have to explicitly reset the ID. The Prototype Pattern is therefore better suited to transient objects (quote items, temporary calculations) than to entities with a database identity.

<?php
declare(strict_types=1);

namespace Mironsoft\Catalog\Model;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\Data\ProductExtensionInterface;

/**
 * Safe product cloning with explicit deep copy for nested objects.
 */
class SafeProductCloner
{
    /**
     * Create a deep clone of a product, including extension attributes.
     */
    public function deepClone(ProductInterface $product): ProductInterface
    {
        $clone = clone $product;

        // Shallow copy DANGER: extension attributes are shared!
        // We must explicitly clone the extension attributes object.
        $extAttributes = $product->getExtensionAttributes();
        if ($extAttributes !== null) {
            $clone->setExtensionAttributes(clone $extAttributes);
        }

        // Reset database identity, this is a new product, not an update
        $clone->setId(null);
        $clone->unsEntityId();

        return $clone;
    }
}

The Object Pool Pattern: concept and use cases

The Object Pool Pattern solves a different performance problem than the Prototype Pattern. It does not target expensive object initialization via a constructor, it targets the repeated creation of resource-intensive connections or handles. The core idea: instead of creating a new object every time you need one, you keep a stock (pool) of already-created objects on hand and take one from it whenever needed.

The classic example is database connections. Opening a new TCP connection to the database, performing the handshake, authenticating, and initializing the MySQL session can, depending on system load, take anywhere from 5 to 50 milliseconds. If every SQL query opened a new connection and closed it again afterward, a Magento request with hundreds of database operations would be massively slowed down. The connection pool fixes this: a connection is opened once and reused for all subsequent queries within the same request.

Other typical use cases for object pools include HTTP client instances for API calls to external services (ERP, PIM, payment providers), SFTP connections for file transfers, Redis client instances for cache operations, and Elasticsearch client connections. What all of these objects have in common is that building them is expensive, and they can be reused for repeated operations.

Unlike the Prototype Pattern, pool objects do not need to be returned after use, in a PHP request model (FPM) the pool ends with the request anyway. In PHP, object pools never survive across request boundaries, since every PHP-FPM worker has its own process and therefore its own memory. Object pools in PHP are therefore always request-scoped, which makes the implementation considerably simpler than in languages like Java or C# with shared-memory pools.

Object pools in Magento 2: shared instances and connection pooling

Magento 2 implements the Object Pool Pattern at two levels. The first and most comprehensive implementation is the DI container itself: injectable objects are managed as shared instances by default. The ObjectManager internally holds a $sharedInstances array that acts as an implicit object pool. The first call to get() for a given class type creates the instance and stores it in the pool. Every subsequent call returns that same instance.

This implicit object pool implementation is exactly why constructor injection in Magento 2 is so efficient. Services, repositories, loggers, session objects and every other injectable class are instantiated only once per request, no matter how many classes depend on them. The result is fewer objects in memory, fewer constructor calls and faster request processing.

The second, more explicit object pool implementation can be found in Magento\Framework\App\ResourceConnection. This class implements a genuine connection pool for database connections. It holds an internal $connections array that indexes connection instances by their $resourceName. By default, Magento has two connections: the main write connection (default) and the read connection (indexer). In larger setups with read replicas, additional connections are added.

The Elasticsearch and OpenSearch client in Magento 2 also relies on the pooling principle: a single client object is reused for all search requests and index operations within a request. The same applies to the Redis client in Magento\Framework\Cache\Backend\Redis, the connection is established once and used for all cache read and write operations.

// Magento\Framework\App\ResourceConnection, a simplified look at the connection pool
class ResourceConnection
{
    /** @var \Magento\Framework\DB\Adapter\AdapterInterface[] the pool */
    private array $connections = [];

    /**
     * Get or create a database connection for the given resource name.
     * This is the Object Pool pattern: reuse instead of recreate.
     */
    public function getConnection(string $resourceName = self::DEFAULT_CONNECTION): AdapterInterface
    {
        $connectionName = $this->config->getConnectionName($resourceName);

        if (!isset($this->connections[$connectionName])) {
            // First access: create and add to pool
            $this->connections[$connectionName] = $this->connectionFactory->create(
                $this->config->getConnectionConfig($connectionName)
            );
        }

        // Subsequent access: return from pool, no new connection overhead
        return $this->connections[$connectionName];
    }

    /**
     * Close all pooled connections and clear the pool.
     * Called at the end of the request lifecycle.
     */
    public function closeConnection(string $resourceName = self::DEFAULT_CONNECTION): void
    {
        $connectionName = $this->config->getConnectionName($resourceName);
        if (isset($this->connections[$connectionName])) {
            $this->connections[$connectionName]->closeConnection();
            unset($this->connections[$connectionName]);
        }
    }
}

Implementing your own object pool with PHP 8.4

For custom integrations, such as API connections to ERP systems, payment providers or external product databases, it is worth implementing your own object pool. The pattern is easy to implement in Magento 2: you create a class with an internal array as the pool, wired up via constructor injection with the necessary factories. Since the class itself is managed as a shared instance in the DI container, the pool survives the entire request.

A real-world use case: a Magento shop integrates several external supplier APIs. Every supplier has its own base URL and its own authentication. The HTTP client pool caches one client instance per supplier. The first request to a given supplier creates the client (connection setup, certificate handling, timeout configuration). Every further request reuses the same instance and saves the connection setup overhead.

The pool implementation should offer a release() method that lets individual pool entries be explicitly freed, for example when a connection is broken and needs to be rebuilt. A clear() method empties the entire pool, which is useful in tests and after error scenarios.

In PHP 8.4, an object pool can be written very concisely using readonly properties for the injected factories and typed generics comments (for IDEs and static analysis). Constructor property promotion significantly reduces boilerplate, and using declare(strict_types=1) prevents implicit type coercions in pool keys that can otherwise lead to hard-to-debug errors.

<?php
declare(strict_types=1);

namespace Mironsoft\Integration\Model\Pool;

use Magento\Framework\HTTP\ClientFactory;
use Magento\Framework\HTTP\ClientInterface;
use Psr\Log\LoggerInterface;

/**
 * HTTP client pool, reuses one client instance per supplier base URL.
 * Registered as shared="true" in di.xml (default for injectable classes).
 */
class HttpClientPool
{
    /** @var array<string, ClientInterface> Pool indexed by base URL */
    private array $pool = [];

    public function __construct(
        private readonly ClientFactory $clientFactory,
        private readonly LoggerInterface $logger,
        private readonly int $timeout = 30
    ) {}

    /**
     * Get or create an HTTP client for the given base URL.
     * First call per base URL creates the client; subsequent calls reuse it.
     */
    public function getClient(string $baseUrl): ClientInterface
    {
        if (!isset($this->pool[$baseUrl])) {
            $this->logger->debug('HttpClientPool: creating new client', ['baseUrl' => $baseUrl]);

            $client = $this->clientFactory->create();
            $client->setTimeout($this->timeout);
            $client->setHeaders([
                'Accept'       => 'application/json',
                'Content-Type' => 'application/json',
            ]);

            $this->pool[$baseUrl] = $client;
        }

        return $this->pool[$baseUrl];
    }

    /**
     * Remove a specific client from the pool (e.g., after a connection error).
     */
    public function release(string $baseUrl): void
    {
        unset($this->pool[$baseUrl]);
        $this->logger->debug('HttpClientPool: released client', ['baseUrl' => $baseUrl]);
    }

    /**
     * Clear all pooled clients, useful in test teardown or error recovery.
     */
    public function clear(): void
    {
        $this->pool = [];
    }

    /**
     * Get current pool size for monitoring/diagnostics.
     */
    public function size(): int
    {
        return count($this->pool);
    }
}

Prototype & Object Pool: the essentials at a glance

PHP clone() = shallow copy

Primitives and arrays are copied. Object properties are carried over by reference. For a deep copy: implement __clone() and explicitly clone every nested object.

Quote items: Prototype in action

Child items for configurable products are created as clone $parentItem. Only the differing properties (product, parent reference) are adjusted.

Shared instances = implicit pool

The DI container manages injectable objects as shared instances, which is effectively an object pool. The first instantiation places it in the pool, and every following injection reuses it.

Connection pool = explicit pool

ResourceConnection caches database connections by resourceName. Opened once, reused for every DB operation of the request.

Mironsoft

Magento 2 performance & architecture

Performance optimization through pattern architecture?

We analyze Magento 2 projects for performance bottlenecks and implement Prototype and Object Pool patterns exactly where they make the biggest difference: for faster API integration, efficient batch processes, and scalable architecture.

API integration

HTTP client pools for efficient API calls without connection setup overhead

Batch processing

Prototype Pattern for fast variant creation in import/export processes

Performance audit

Identify object creation bottlenecks and resolve them through pooling

Summary

The Prototype Pattern and the Object Pool Pattern pursue the same goal through different means: minimizing the cost of object creation. The Prototype Pattern achieves this by using clone instead of new, thereby avoiding expensive constructor calls. The Object Pool Pattern achieves it by keeping already created objects around and reusing them instead of discarding and recreating them.

In Magento 2.4.8, both patterns are deeply rooted in the framework. The Prototype Pattern shows up when cloning quote items and in import processes. The Object Pool Pattern is everywhere: the DI container itself is an object pool for all injectable services, and ResourceConnection is an explicit connection pool for database connections.

For your own Magento modules, the rule of thumb is: if you need expensive objects (HTTP clients, external connections) repeatedly, implement your own pool as a shared instance. If you create similar objects inside loops, consider the Prototype Pattern with careful deep-copy management. Both patterns pay into the same account: less overhead, faster requests and a more robust system.

FAQ: Prototype & Object Pool in Magento 2

1 What is the Prototype Pattern in software development?
The Prototype Pattern is one of the classic GoF creational patterns. It creates new objects not through a constructor, but by cloning an existing prototype object, implemented in PHP via clone $obj. Useful when initializing an object is expensive, or when many similar objects with slightly different properties are needed.
2 What is the difference between shallow copy and deep copy in PHP?
Shallow copy: primitive values are copied, object properties are carried over by reference. Changes to nested objects affect both instances. Deep copy: all contained objects are cloned as well. In PHP this is implemented via __clone(), where every object property is explicitly given a clone treatment.
3 How does Magento 2 use the Prototype Pattern internally?
When adding configurable products to the cart: child items are created as clone $parentItem. Only the differing properties (product, parent reference) are adjusted. The product type pool also keeps prototype instances of the TypeModel classes on hand and reuses them directly.
4 When is clone() dangerous in Magento 2?
When the cloned object contains nested objects that are not explicitly cloned as well. In that case the original and the clone share the same reference. Especially dangerous with Magento objects that carry extension attributes, address objects, or complex data objects. Always implement __clone().
5 What is the Object Pool Pattern and what is it used for?
The object pool manages a stock of pre-instantiated, reusable objects. Instead of creating a new object, one is taken from the pool. Used for expensive objects: database connections, HTTP clients, SFTP connections, Redis client instances, essentially anything where establishing a new connection costs significant time.
6 How does Magento 2 implement the Object Pool Pattern?
Implicitly: the DI container manages shared instances in the $sharedInstances array, effectively an object pool. Explicitly: ResourceConnection caches database connections by resourceName and returns the same reused connection on every call.
7 How do you implement your own object pool in Magento 2?
A shared instance class with an internal array as the pool, wired up via constructor injection of a factory. getClient(string $key) checks whether the instance exists, creates it otherwise, and returns it. Since the pool class itself is a shared instance, the pool survives the entire request.
8 What is the difference between Object Pool and Singleton?
Singleton: exactly one instance globally. Object pool: multiple instances of the same type, distinguished by keys. In PHP-FPM every request is its own process, so object pools are request-scoped and automatically thread-safe without any synchronization.
9 How do you test classes that use clone?
clone is plain PHP and requires no mocks. Unit test: modify the original, clone it, then verify that both are independent. For __clone() tests: check nested objects for reference equality or inequality after cloning (the === operator).
10 Can the Prototype Pattern be used for product variants in imports?
Yes. A base product object is configured once, then cloned for every variant, and only the differing properties (SKU, price, EAV attribute values) are set afterward. This saves the complete object initialization with default values, extension attributes and standard configuration for every single record.