Creational Design Patterns

Understand the five classic creational patterns — Factory Method, Abstract Factory, Builder, Singleton, and Prototype — well enough to recognize the object-construction problem each one solves, implement each in modern PHP 8, and judge honestly when a pattern is earning its complexity versus when a plain constructor call would have been better.

Creational patterns are all about one question: how does an object come into existence? That sounds trivial — write new ClassName(...) — and for most code, it is. But construction gets genuinely hard when the concrete class to build depends on runtime configuration, when building one object means building several related ones consistently, when an object takes a dozen optional parameters, when you need exactly one shared instance of something, or when the cheapest way to get a new object is to copy an existing one rather than rebuild it from scratch. Each pattern in this lesson is a named, well-understood answer to one of those specific problems — and each one also has a well-known failure mode where developers reach for it out of habit rather than need, adding indirection that buys nothing.

Factory Method — centralizing 'which class do I build?'

A Factory Method replaces scattered new ConcreteClass(...) calls with a single method that decides which concrete class to instantiate and returns it through a common interface or abstract type. The caller depends only on that interface, never on the concrete classes, so new variants can be added without touching calling code.

It earns its place when the decision of which class to build is itself real logic — driven by configuration, input data, or environment — and when that decision would otherwise be duplicated at every call site. If there is exactly one concrete class and no foreseeable second one, a factory method is a detour: it hides a call to new behind a method that will only ever do one thing.

Abstract Factory — building families of related objects

An Abstract Factory is a factory of factories: it defines an interface for creating a *set* of related objects, and each concrete implementation of that interface produces a matching, internally-consistent family. The canonical case is swappable infrastructure — a payment provider, a database driver, a UI toolkit — where using an object from one family together with an object from another would be a bug.

The value is consistency: the abstract factory makes it structurally impossible to accidentally pair a Stripe charge object with a PayPal refund object, because both come from the same concrete factory instance. It becomes overengineering the moment there is really only one family in practice, or when the 'related objects' don't actually need to travel together — at that point a couple of independent factory methods, or even just a service container binding, is simpler and equally safe.

Builder — assembling complex objects step by step

A Builder separates the *construction* of a complex object from its *representation*, exposing a fluent, step-by-step API (often with method chaining) so callers can assemble an object piece by piece and only pay attention to the parts they care about. It shines when an object has many optional parts, when construction order matters, or when the same step sequence can produce meaningfully different final objects.

PHP's named arguments and constructor property promotion (both PHP 8+) genuinely reduced the need for builders on simpler objects — a constructor with ten named, optional parameters is now readable without a builder at all. Reach for a Builder when assembly involves real steps (validation between steps, incremental collection into internal collections, a final build() that assembles everything at once), not just as a workaround for 'this constructor has a lot of arguments'.

Singleton — one shared instance, and its real costs

A Singleton guarantees a class has exactly one instance, reachable globally through a static accessor (getInstance()), and lazily creates that instance on first use. It looks attractive for things that are conceptually singular in an application — a configuration store, a logger, a single database connection pool.

The honest downside is significant and well documented: a Singleton is global mutable state wearing an object-oriented costume. Any class that calls Config::getInstance() internally has an invisible, hardcoded dependency that doesn't show up in its constructor signature, which makes the class harder to test in isolation (you cannot substitute a fake config without reaching into static state), harder to reason about (any code, anywhere, can mutate the shared instance), and harder to run in parallel or reset between test runs (PHP's static state persists for the life of the process/worker).

The modern, strongly preferred alternative for the same goal — 'only one instance should exist' — is to register the object as a shared ('singleton-scoped') service in a dependency injection container, and inject it explicitly into whatever needs it. The container still creates exactly one instance, but every consumer declares the dependency openly in its constructor, so it's visible, swappable, and mockable in tests. Treat the classic getInstance() Singleton as something to recognize in legacy code and in interview questions, but rarely to write yourself in new PHP.

Prototype — cloning instead of rebuilding

A Prototype creates new objects by copying an existing, fully-configured instance rather than constructing one from raw inputs. PHP has first-class support for this via the clone keyword, which performs a shallow copy, and the __clone() magic method, which lets an object customize what happens during a clone — most importantly, deep-copying any object-typed properties that shouldn't be shared by reference with the original.

This pattern is useful when building an object from scratch is expensive (an object that required an expensive computation, a database round trip, or complex initialization to reach its current state) or when you need many near-identical variants of a fully-configured object and only want to change a few fields on each copy. It is overkill when construction is already cheap and simple — at that point clone just adds an extra concept for no savings, and a plain new is clearer.

Choosing a creational pattern — and knowing when not to

All five patterns solve the same underlying problem — controlling how and when objects come into existence — but they solve different shapes of that problem, and none of them is a default. The single most common overengineering mistake with creational patterns is applying one where a plain new ClassName(...) was already perfectly clear: a Factory Method wrapping one concrete class, a Builder for a five-argument constructor that named arguments already made readable, or an Abstract Factory for a 'family' that in practice never has more than one member.

A useful discipline: only introduce a creational pattern once you can point to the concrete problem it removes — a real branch point on which class to build (Factory Method), a real risk of mismatched related objects (Abstract Factory), real multi-step assembly logic (Builder), a real, deliberate architectural decision to share one instance via DI (not getInstance()), or a real, measured cost to rebuilding from scratch (Prototype). If you can't name the concrete problem, you're adding a pattern instead of solving one.

• Factory Method: use when the concrete class to build is decided by logic/config; skip for a single hardcoded class. • Abstract Factory: use when several related objects must stay consistent with each other; skip when there's really only one family. • Builder: use for genuine multi-step, validated assembly; skip when named constructor arguments already read cleanly. • Singleton: prefer a DI container's shared/singleton binding over a static getInstance() accessor in almost all new code. • Prototype: use when copying a configured object is meaningfully cheaper or more convenient than rebuilding it; skip when construction is already trivial.

Creational pattern skeletons

self::$instance ??= new self() is the idiomatic PHP 8 lazy-init one-liner: it assigns only if the property is currently null. Without a custom __clone(), PHP's default clone is shallow — object-typed properties still point at the *same* nested objects as the original, which is rarely what you want for a true prototype copy.

// Factory Method
interface Logger { public function log(string $message): void; }
abstract class LoggerFactory {
    abstract public function create(): Logger;
}

// Abstract Factory
interface PaymentFactory {
    public function createCharge(): ChargeGateway;
    public function createRefund(): RefundGateway;
}

// Builder
class QueryBuilder {
    public function select(array $cols): static { /* ... */ return $this; }
    public function where(string $cond): static { /* ... */ return $this; }
    public function build(): string { /* assemble final SQL */ return ''; }
}

// Singleton (classic form -- generally avoid in new code)
final class Registry {
    private static ?self $instance = null;
    private function __construct() {}
    public static function getInstance(): self {
        return self::$instance ??= new self();
    }
}

// Prototype
class Document {
    public function __construct(public Metadata $meta) {}
    public function __clone(): void {
        $this->meta = clone $this->meta; // deep-copy the nested object
    }
}

Code Examples

The calling code never mentions EmailNotifier or SmsNotifier directly — it asks the factory for 'whatever notifier handles this channel' and programs against the Notifier interface. Adding a PushNotifier later means adding one match arm, not hunting down every place a notifier is constructed. Expected result: Texting +15551234567: Your order has shipped.

return match ($channel) { 'email' => new EmailNotifier(), 'sms' => new SmsNotifier(), ... };

Because createCharge() and createRefund() both live on one StripeFactory, it's structurally impossible for checkout() to accidentally combine a Stripe charge with a PayPal refund object -- the factory is the single source for a whole consistent family of related objects. Expected result: Charged: stripe_ch_2500

$charge = $gateway->createCharge(); $refund = $gateway->createRefund();

Each fluent call returns $this, so the calling code reads like a small sentence describing the query, and the builder accumulates internal state (columns, table, conditions) across several steps before assembling everything into one final SQL string in build(). Expected result: SELECT id, name, email FROM users WHERE active = 1 AND role = 'admin' public function select(string...$columns): static { $this->columns = $columns; return $this; }

WelcomeBanner works but its dependency on AppConfig is invisible from its constructor -- you cannot substitute a test double without touching global static state. WelcomeBannerDI gets the exact same single shared instance, but declares it openly, so a unit test can simply pass in a fake AppConfig instead.

public function __construct(private readonly AppConfig $config) {}

Common mistake: Reaching for AppConfig::getInstance() (or any static getInstance()) inside business-logic classes instead of accepting the dependency through the constructor -- it works today and becomes an untestable knot later.

Without __clone(), $copy->meta and $original->meta would be the exact same Metadata object, so pushing 'final' onto $copy's tags would corrupt $original too. The custom __clone() clones the nested Metadata object as well, giving true independent copies -- this is the Prototype pattern's whole value. Expected result: finance,draft finance,draft,final public function __clone(): void { $this->meta = clone $this->meta; }

interface Notifier
{
    public function send(string $to, string $message): void;
}

final class EmailNotifier implements Notifier
{
    public function send(string $to, string $message): void
    {
        echo "Emailing {$to}: {$message}\n";
    }
}

final class SmsNotifier implements Notifier
{
    public function send(string $to, string $message): void
    {
        echo "Texting {$to}: {$message}\n";
    }
}

final class NotifierFactory
{
    public static function create(string $channel): Notifier
    {
        return match ($channel) {
            'email' => new EmailNotifier(),
            'sms'   => new SmsNotifier(),
            default => throw new InvalidArgumentException("Unknown channel: {$channel}"),
        };
    }
}

$notifier = NotifierFactory::create('sms');
$notifier->send('+15551234567', 'Your order has shipped.');
interface ChargeGateway { public function charge(int $cents): string; }
interface RefundGateway { public function refund(string $chargeId): void; }

interface PaymentFactory
{
    public function createCharge(): ChargeGateway;
    public function createRefund(): RefundGateway;
}

final class StripeCharge implements ChargeGateway
{
    public function charge(int $cents): string { return "stripe_ch_{$cents}"; }
}
final class StripeRefund implements RefundGateway
{
    public function refund(string $chargeId): void { echo "Stripe refunding {$chargeId}\n"; }
}
final class StripeFactory implements PaymentFactory
{
    public function createCharge(): ChargeGateway { return new StripeCharge(); }
    public function createRefund(): RefundGateway { return new StripeRefund(); }
}

function checkout(PaymentFactory $gateway, int $cents): void
{
    $charge = $gateway->createCharge();
    $refund = $gateway->createRefund();
    $id = $charge->charge($cents);
    echo "Charged: {$id}\n";
    // $charge and $refund are guaranteed to be from the SAME provider
}

checkout(new StripeFactory(), 2500);
final class SqlSelectBuilder
{
    private array $columns = ['*'];
    private string $table = '';
    private array $conditions = [];

    public function select(string ...$columns): static
    {
        $this->columns = $columns;
        return $this;
    }

    public function from(string $table): static
    {
        $this->table = $table;
        return $this;
    }

    public function where(string $condition): static
    {
        $this->conditions[] = $condition;
        return $this;
    }

    public function build(): string
    {
        $sql = 'SELECT ' . implode(', ', $this->columns) . ' FROM ' . $this->table;
        if ($this->conditions !== []) {
            $sql .= ' WHERE ' . implode(' AND ', $this->conditions);
        }
        return $sql;
    }
}

$sql = (new SqlSelectBuilder())
    ->select('id', 'name', 'email')
    ->from('users')
    ->where('active = 1')
    ->where("role = 'admin'")
    ->build();

echo $sql;
// The classic Singleton -- works, but hides a global dependency.
final class AppConfig
{
    private static ?self $instance = null;
    private array $values;

    private function __construct()
    {
        $this->values = ['app_name' => 'SampleApp'];
    }

    public static function getInstance(): self
    {
        return self::$instance ??= new self();
    }

    public function get(string $key): mixed
    {
        return $this->values[$key] ?? null;
    }
}

// Any class can silently reach into global state:
final class WelcomeBanner
{
    public function render(): string
    {
        return 'Welcome to ' . AppConfig::getInstance()->get('app_name');
    }
}

// Preferred: inject the shared instance explicitly via a container binding.
final class WelcomeBannerDI
{
    public function __construct(private readonly AppConfig $config) {}

    public function render(): string
    {
        return 'Welcome to ' . $this->config->get('app_name');
    }
}
// $container->bind(AppConfig::class, fn () => AppConfig::getInstance(), shared: true);
// $banner = $container->make(WelcomeBannerDI::class); // config is visible & mockable
final class Metadata
{
    public function __construct(public array $tags) {}
}

final class Document
{
    public function __construct(
        public string $title,
        public Metadata $meta,
    ) {}

    public function __clone(): void
    {
        // Deep-copy the nested object so the clone doesn't share it with the original.
        $this->meta = clone $this->meta;
    }
}

$original = new Document('Q3 Report', new Metadata(['finance', 'draft']));

$copy = clone $original;
$copy->title = 'Q3 Report (Final)';
$copy->meta->tags[] = 'final';

echo implode(',', $original->meta->tags) . "\n"; // unaffected by the copy's change
echo implode(',', $copy->meta->tags) . "\n";

Where each creational pattern intervenes

Every creational pattern answers 'how do I get an object?' differently -- by class choice, by family, by steps, by sharing, or by copying.

caller
    |
    v
  [ Factory Method ]  -->  picks ONE concrete class, returns common interface
    |
  [ Abstract Factory ] -->  picks a whole FAMILY of related concrete classes
    |
  [ Builder ]          -->  assembles ONE object across several steps, then build()
    |
  [ Singleton ]        -->  returns the SAME shared instance every time
    |
  [ Prototype ]        -->  clone()s an existing configured instance instead of new

Important Rules

Reach for a creational pattern only when you can name the concrete construction problem it removes -- not by default.

Factory Method centralizes the decision of which single concrete class to build; Abstract Factory does the same for a whole family that must stay internally consistent.

PHP 8's named arguments and constructor promotion cover many cases that used to need a Builder -- reserve Builder for real multi-step, stateful assembly.

Prefer a DI container's shared/singleton binding over a static getInstance() Singleton; the object count is the same, but the dependency stays visible and testable.

Prototype's __clone() must manually deep-copy any object-typed properties -- PHP's default clone is shallow.

Where these show up in real PHP frameworks

Laravel's service container and Symfony's DI container are, functionally, industrial-strength replacements for hand-rolled Singletons -- singleton() bindings and Symfony's default shared: true services give you exactly-one-instance semantics without static accessors. Doctrine's connection/driver setup is an Abstract Factory in practice: pick a driver family (pdo_mysql, pdo_pgsql) and get back a consistent set of platform, schema-manager, and query-builder objects. Symfony's Request object construction and query builders across nearly every ORM (Doctrine's QueryBuilder, Eloquent's query builder) are textbook fluent Builders. Prototype shows up whenever a framework clones a configured value object -- Doctrine entities are sometimes cloned deliberately to detach a copy from the identity map.

Wrapping a single concrete class in a Factory Method that will realistically never have a second implementation.

Call new ConcreteClass(...) directly until a second, genuinely different implementation actually shows up.

A factory method with one branch adds a layer of indirection with zero behavioral benefit -- it's speculative generality, not flexibility.

Using AppConfig::getInstance() (or any static Singleton accessor) inside a service class's methods.

Inject the shared instance through the constructor, with the container configured to hand out the same instance to every consumer.

Hidden static dependencies make unit testing require touching global state, and make the class's real dependencies invisible from its signature.

Adding a Builder for a constructor that already takes three or four well-named, optional parameters.

Use PHP 8 named arguments: new Widget(color: 'red', size: 'lg') and skip the builder entirely.

Named arguments solve the readability problem builders were originally invented for, without an extra class.

What is the main difference between Factory Method and Abstract Factory? A. Factory Method builds one object of a chosen concrete class; Abstract Factory builds a whole family of related objects that must stay consistent with each other. B. Abstract Factory is only usable with abstract classes, never interfaces. C. Factory Method is always static; Abstract Factory is never static. D. They are the same pattern with different names.

A. Factory Method builds one object of a chosen concrete class; Abstract Factory builds a whole family of related objects that must stay consistent with each other. — Factory Method picks one concrete class to instantiate; Abstract Factory groups several factory methods so the objects they return (e.g. a charge gateway and a refund gateway) are guaranteed to come from the same compatible family.

Why is a classic getInstance() Singleton considered risky in application code, even though it guarantees a single instance? A. It makes the class's dependency on that instance invisible from its constructor, hurting testability and making the state effectively global and mutable. B. PHP does not support static properties, so Singletons cannot actually be implemented. C. Singletons always cause memory leaks in PHP-FPM. D. It is impossible to lazily initialize a Singleton in PHP.

A. It makes the class's dependency on that instance invisible from its constructor, hurting testability and making the state effectively global and mutable. — The 'one instance' guarantee itself is fine -- the problem is *how* it's usually accessed: a hardcoded static call hides the dependency, making the class harder to test and its collaborators harder to see.

What does PHP's default (built-in) clone operator do to object-typed properties? A. Shallow-copies them -- the clone's property still references the exact same nested object as the original. B. Deep-copies them automatically, recursively cloning every nested object. C. Throws a fatal error unless __clone() is defined. D. Converts them to arrays.

A. Shallow-copies them -- the clone's property still references the exact same nested object as the original. — Without a custom __clone() method, PHP's clone is shallow: scalar properties are copied by value, but object-typed properties still point at the same shared object, which is why Prototype implementations must deep-copy manually inside __clone().

A team introduces a Builder for a constructor that already reads cleanly as new Invoice(customer: $c, dueDate: $date, taxRate: 0.2) using PHP 8 named arguments. Why is this most likely overengineering? A. Named arguments already solved the readability problem the Builder was meant to fix, so the Builder adds a class with no real benefit. B. Named arguments are deprecated in PHP 8 and should not be relied on. C. Builders can only be used with abstract classes. D. Constructors cannot take more than three parameters in PHP.

A. Named arguments already solved the readability problem the Builder was meant to fix, so the Builder adds a class with no real benefit. — Builders earn their complexity from real multi-step, stateful assembly logic -- not merely from 'a constructor has several parameters', a problem PHP 8's named arguments already address directly.

Which scenario is the best fit for the Prototype pattern? A. You need many variants of an object that took real work (computation, I/O, deep configuration) to reach its current state, and only a few fields differ per variant. B. You need exactly one shared instance of a logger across the whole application. C. You need to decide, at runtime, which of two unrelated classes to construct based on a config flag. D. You need a fluent API to assemble a complex value step by step.

A. You need many variants of an object that took real work (computation, I/O, deep configuration) to reach its current state, and only a few fields differ per variant. — Prototype's value is avoiding the cost of rebuilding an expensively-configured object from scratch by cloning an existing one and tweaking a few fields -- the other scenarios describe Singleton, Factory Method, and Builder respectively.

Design a ReportFactory that returns a ReportGenerator (interface with generate(): string) for 'csv', 'json', or 'pdf' report types, throwing on an unknown type. Then write a ReportBuilder that lets a caller chain ->setTitle(), ->addSection(string $name, string $body), and ->build(): Report, assembling a Report value object only once build() is called.

final class ReportFactory { public static function create(string $type): ReportGenerator { return match($type) { 'csv' => new CsvReportGenerator(), 'json' => new JsonReportGenerator(), 'pdf' => new PdfReportGenerator(), default => throw new InvalidArgumentException($type) }; } } final class ReportBuilder { private string $title = ''; private array $sections = []; public function setTitle(string $t): static { $this->title = $t; return $this; } public function addSection(string $name, string $body): static { $this->sections[$name] = $body; return $this; } public function build(): Report { return new Report($this->title, $this->sections); } }

The factory's match expression should map each string type to new CsvReportGenerator(), new JsonReportGenerator(), etc. The builder should accumulate title/sections into private properties and only construct the final immutable Report inside build().