Senior 4 min · March 06, 2026

PHP Design Patterns — Singleton Test Suite Failure

Tests pass individually but fail together - root cause: Singleton mutable state.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Design patterns are reusable solutions to common object-oriented problems
  • Creational patterns control object creation (Singleton, Factory, Builder)
  • Structural patterns compose classes and objects (Adapter, Decorator, Facade)
  • Behavioral patterns define communication (Strategy, Observer, Command)
  • Misapplying a pattern adds accidental complexity — solve the real problem first
  • In production, patterns improve maintainability but can hide performance pitfalls like lazy loading stalls
Plain-English First

Imagine you're building IKEA furniture. You don't invent a new way to join wood every time — you follow proven assembly patterns printed in the manual. PHP design patterns are exactly that: battle-tested blueprints for solving recurring software problems. You've probably already solved the same problem five different ways across five projects. Patterns give that solution a name, a shape, and a reputation so your whole team can talk about it in one word.

Every PHP codebase beyond a certain size starts to rot in predictable ways — God classes that do everything, tightly coupled modules that break when you sneeze, duplicated logic scattered across controllers. These aren't signs of bad programmers; they're signs that the code grew without a shared vocabulary for solving recurring structural problems. Design patterns are that vocabulary, and they've been the lingua franca of serious software engineering since the Gang of Four published their seminal work in 1994.

The problem patterns solve isn't complexity for its own sake. It's the cost of change. When your PaymentProcessor is hardcoded to Stripe and the business suddenly needs PayPal too, you pay a tax — refactoring, regression testing, prayer. A well-applied Strategy or Factory pattern means that change costs an afternoon, not a sprint. Patterns encode the insight that software requirements always drift, so your architecture should make drift cheap.

By the end of this article you'll be able to identify which pattern fits which problem in a real PHP codebase, implement Singleton, Factory Method, Decorator, Observer, and Strategy with production-grade PHP 8.x code, spot the performance and testability traps each one hides, and answer the patterns questions that trip up even experienced developers in senior interviews.

What Are Design Patterns?

Design patterns are reusable, documented solutions to recurring software design problems. They're not copy-paste code — they're blueprints that you adapt to your architecture. The original 23 patterns from the Gang of Four fall into three categories: creational, structural, and behavioral.

Patterns solve the problem of change cost. When your code is tightly coupled, every new requirement forces you to rewrite large chunks. A pattern introduces a seam — a place where you can insert new behavior without touching existing code. The Strategy pattern, for example, lets you swap algorithms at runtime. The Observer pattern lets you notify multiple objects without hardcoding dependencies.

But patterns come with baggage. Each one adds indirection, more classes, and more files. If you apply a pattern to a problem that doesn't exist yet, you're paying the complexity tax for no benefit. The senior engineer's skill isn't knowing 23 patterns — it's knowing which one to ignore.

TheCodeForge/Patterns/StrategyExample.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

namespace TheCodeForge\Patterns;

interface PaymentStrategy
{
    public function pay(float $amount): string;
}

class CreditCardPayment implements PaymentStrategy
{
    public function pay(float $amount): string
    {
        return "Paid $amount via Credit Card.";
    }
}

class PayPalPayment implements PaymentStrategy
{
    public function pay(float $amount): string
    {
        return "Paid $amount via PayPal.";
    }
}

class Checkout
{
    public function __construct(private PaymentStrategy $strategy) {}

    public function process(float $amount): string
    {
        return $this->strategy->pay($amount);
    }
}

// Usage
$checkout = new Checkout(new PayPalPayment());
echo $checkout->process(150.00);  // Paid 150 via PayPal.
Output
Paid 150 via PayPal.
Pattern or Anti-Pattern?
The Strategy pattern is a structural way to follow the Open/Closed principle. But don't force it — if you only ever use one payment method, a simple if-else is cleaner. Patterns solve for future change, not present certainty.
Production Insight
Strategy pattern introduces a new class per algorithm.
If you have 50 algorithms, you have 50 small classes.
Each class needs its own test — test suite grows linearly.
Rule: Only extract a pattern when the second algorithm appears.
Key Takeaway
Patterns reduce change cost but add indirection.
Apply only when the cost of change exceeds the cost of the pattern.
Start simple, extract later.
When to Apply Strategy Pattern
IfOnly one implementation exists now and no plans for more
UseSkip pattern. Use a simple method with if-else. You can extract later.
IfTwo or more implementations known, but they differ in algorithm
UseUse Strategy. Define an interface and inject via constructor.
IfAlgorithms are not stable — they change frequently or are swapped at runtime
UseStrategy + Factory combined. Use a factory to create the right strategy based on input.

Creational Patterns — Singleton, Factory & Builder

Creational patterns abstract the instantiation process. They make a system independent of how its objects are created, composed, and represented.

The Singleton ensures a class has only one instance and provides a global access point. Use it carefully — it's a global variable with a marketing name. PHP's request lifecycle means a Singleton lives only for the current HTTP request, which often surprises devs coming from Java.

The Factory Method defines an interface for creating an object, but lets subclasses decide which class to instantiate. It decouples client code from concrete classes.

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. Perfect for objects with many optional parameters — avoids telescoping constructors.

TheCodeForge/Patterns/FactoryMethod.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

namespace TheCodeForge\Patterns;

interface Logger
{
    public function log(string $message): void;
}

class FileLogger implements Logger
{
    public function log(string $message): void
    {
        file_put_contents('/var/log/app.log', $message . PHP_EOL, FILE_APPEND);
    }
}

class DatabaseLogger implements Logger
{
    public function log(string $message): void
    {
        // INSERT INTO logs ...
    }
}

abstract class LoggerFactory
{
    abstract public function createLogger(): Logger;

    public function logMessage(string $message): void
    {
        $logger = $this->createLogger();
        $logger->log($message);
    }
}

class FileLoggerFactory extends LoggerFactory
{
    public function createLogger(): Logger
    {
        return new FileLogger();
    }
}

// Usage
$factory = new FileLoggerFactory();
$factory->logMessage('User login event logged.');
Output
Appends message to /var/log/app.log
Creational Patterns as Object Blueprints
  • Singleton — one house for the whole family (shared resource).
  • Factory Method — a real estate agent who picks the right house for your needs.
  • Builder — an architect who lets you configure rooms, floors, and colors step-by-step.
Production Insight
Singleton in PHP is per-request, not global across users.
If you cache a database connection in a Singleton, each request creates one connection anyway — you're not sharing across requests.
Use a Singleton only for cache that lives inside a request (e.g., config parsed once).
Factory Method with many subclasses can cause class explosion — watch out.
Key Takeaway
Creational patterns decouple your code from concrete classes.
Singleton hides global state — prefer dependency injection.
Factory Method is the most broadly useful — start there.

Structural Patterns — Adapter, Decorator & Facade

Structural patterns compose classes and objects to form larger structures. They're about how to wire things together without making the wiring fragile.

The Adapter converts the interface of a class into another interface that clients expect. It's the pattern equivalent of a travel power plug — it doesn't change the device, it makes the connection work.

The Decorator attaches additional responsibilities to an object dynamically. It provides an alternative to subclassing for extending functionality. In PHP, decorators are often used for logging or caching wrappers around services.

The Facade provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use. Think of it as a remote control for a home theatre system — you press one button instead of turning on each device individually.

TheCodeForge/Patterns/Decorator.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

namespace TheCodeForge\Patterns;

interface Notification
{
    public function send(string $message): string;
}

class EmailNotification implements Notification
{
    public function send(string $message): string
    {
        return "Email: $message";
    }
}

abstract class NotificationDecorator implements Notification
{
    public function __construct(protected Notification $notification) {}
}

class SlackDecorator extends NotificationDecorator
{
    public function send(string $message): string
    {
        $result = $this->notification->send($message);
        return $result . " | Slack: $message";
    }
}

class SMSDecorator extends NotificationDecorator
{
    public function send(string $message): string
    {
        $result = $this->notification->send($message);
        return $result . " | SMS: $message";
    }
}

// Usage
$notification = new SlackDecorator(new SMSDecorator(new EmailNotification()));
echo $notification->send('Server down!');
// Output: Email: Server down! | SMS: Server down! | Slack: Server down!
Output
Email: Server down! | SMS: Server down! | Slack: Server down!
Decorator Stack Order Matters
The order in which you wrap decorators changes the outcome. If SlackDecorator and SMSDecorator run in different sequences, the message content may be double-logged or modified unexpectedly. Always document the expected stack order.
Production Insight
Decorators are great for cross-cutting concerns like logging, caching, or monitoring.
But each decorator adds a layer of indirection — too many and stack traces become unreadable.
Also, if a decorator throws an exception mid-chain, you need to decide whether the chain should break or continue.
In production, we saw a chain of 7 decorators that hid the actual error location for two hours.
Key Takeaway
Structural patterns simplify composition.
Adapter makes incompatible interfaces work together.
Decorator adds features without modifying the original.
Facade hides subsystem complexity behind a single class.

Behavioral Patterns — Strategy, Observer & Command

Behavioral patterns focus on communication between objects — how they assign responsibilities and how they interact. These patterns are the most diverse and often the most applicable in daily PHP development.

The Strategy pattern (shown earlier) lets you define a family of algorithms and make them interchangeable. It's the pattern behind PHP's sort functions that accept a comparison callback.

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. PHP's SplSubject and SplObserver provide a native implementation, but many frameworks use event dispatchers instead.

The Command pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. It's the foundation of undo/redo features and job queues.

TheCodeForge/Patterns/Observer.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php

namespace TheCodeForge\Patterns;

class User implements \SplSubject
{
    private int $id;
    private \SplObjectStorage $observers;

    public function __construct(int $id)
    {
        $this->id = $id;
        $this->observers = new \SplObjectStorage();
    }

    public function attach(\SplObserver $observer): void
    {
        $this->observers->attach($observer);
    }

    public function detach(\SplObserver $observer): void
    {
        $this->observers->detach($observer);
    }

    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function setId(int $id): void
    {
        $this->id = $id;
        $this->notify();
    }
}

class EmailNotifier implements \SplObserver
{
    public function update(\SplSubject $subject): void
    {
        if ($subject instanceof User) {
            echo "Email sent to user ID: " . $subject->getId() . "\n";
        }
    }
}
Output
Email sent to user ID: 123
Observer performance tip
If you attach many observers (100+), the notify() loop can become a bottleneck. Consider using a priority queue or async event dispatcher (e.g., Laravel events with queue:work) to avoid slowing down the main request.
Production Insight
Observer pattern in PHP is synchronous by default.
If an observer throws an exception, the entire notify chain stops — subsequent observers never fire.
Always catch exceptions inside each observer's update() method.
Also, if you attach observers inside a request, make sure they're detached to prevent memory leaks in long-running scripts (e.g., Laravel Octane or ReactPHP).
Key Takeaway
Behavioral patterns manage how objects collaborate.
Strategy is the go-to for algorithm selection.
Observer works well for event-driven architectures — but watch sync bottlenecks.
Command enables undo and queued execution.

When to Apply Patterns — And When to Run Away

The biggest mistake junior engineers make is learning a pattern and then looking for somewhere to use it. That's the wrong direction. You should only introduce a pattern when you can point to a concrete pain — a problem you've already felt in production.

Here's a rule of thumb: if you're adding a pattern to code that works fine today, you're probably over-engineering. Patterns are replacements for pain, not decorations. Wait until you have at least two occurrences of a similar problem before extracting a pattern.

Also, patterns are not silver bullets. The Singleton pattern is often misused for global state and breaks testability. The Observer pattern can introduce performance surprises. The Factory pattern can lead to explosion of classes. Always weigh the benefit against the complexity cost.

In production, the most effective approach is to build simple code first, refactor toward patterns when duplication or coupling becomes painful, and never let a pattern become an unchangeable architecture.

TheCodeForge/Patterns/AntiPatternCheck.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace TheCodeForge\Patterns;

// Bad: Using a pattern before needed
class PaymentProcessor
{
    public function process(string $method, float $amount): string
    {
        // Early Strategy pattern — only one method for now
        if ($method === 'credit_card') {
            return $this->processCreditCard($amount);
        }
        throw new \InvalidArgumentException('Only credit_card supported currently');
    }

    private function processCreditCard(float $amount): string { /* ... */ }
}

// Better: Keep it simple until second method arrives
class PaymentProcessor
{
    public function processCreditCard(float $amount): string { /* ... */ }
}
Output
None — this is a refactoring example.
YAGNI Applied to Patterns
  • If the pattern solves a problem you don't have, you're just adding complexity.
  • Simple code is easier to change and debug — patterns lock you into a structure.
  • Refactoring to a pattern is easier and safer than predicting the future.
  • Reversible decisions beat irreversible pattern choices.
Production Insight
We once had a codebase that used the Abstract Factory pattern for a single product.
Every new feature required updating three factory classes.
When the product was eventually removed, those factories were still being maintained.
Pattern overuse adds maintenance overhead that compounds over time.
Rule: Apply patterns as a refactoring outcome, not a design prediction.
Key Takeaway
Don't force patterns onto clean code.
Let duplication and coupling be your triggers.
Patterns are refactoring tools, not upfront design deliverables.
● Production incidentPOST-MORTEMseverity: high

The Singleton That Brought Down the Test Suite

Symptom
Tests pass when run individually but fail in random order when run together. No obvious connection between failing tests.
Assumption
Singleton is safe because it's a well-known pattern and we only use it for configuration.
Root cause
The Singleton instance held a mutable object (database connection pool) that was modified during one test and not reset. Subsequent tests saw stale state because the Singleton was never cleared.
Fix
Replace Singleton with dependency injection for that service. Use a factory or container to control instance lifecycle — test reset becomes explicit.
Key lesson
  • Singleton is overused; it's really a global variable in disguise.
  • Never use Singleton for mutable state that must be isolated (e.g., in tests or request-scoped data).
  • Prefer dependency injection and let the container manage lifetimes.
Production debug guideSymptom → Action guide for the three most common pattern mistakes3 entries
Symptom · 01
Changes to one part of the code unexpectedly affect another part
Fix
Check for global state: Singleton, static properties, or global variables. Introduce dependency injection to break the coupling.
Symptom · 02
Factory method returns wrong type; new code breaks at runtime
Fix
Add type logging inside the factory – var_dump the input and the returned class name. Verify that the mapping logic (switch/match) covers all cases.
Symptom · 03
Observer/event subscribers fire in wrong order or not at all
Fix
Check the order of registration and whether the event dispatcher is a new instance each time. Use PHP's SplPriorityQueue or a dedicated event bus.
★ Pattern Troubleshooting Cheat SheetQuick fixes for the most common pattern misuses in production PHP
Singleton not returning the same instance across requests
Immediate action
Check if the class is being autoloaded multiple times or if opcache is disabled.
Commands
echo spl_object_hash($instance);
Check if the class file is included more than once using get_included_files().
Fix now
Ensure the class is loaded from a single file and that the Singleton uses a static::$instance property with a private constructor.
Factory method returns null or wrong type+
Immediate action
Log the input parameter and the decision logic.
Commands
error_log(print_r($type, true));
Use get_class() on the returned object to verify type.
Fix now
Add a default fallback or throw a meaningful exception in the factory.
Observer pattern : event listener never fires+
Immediate action
Verify that the observer is attached to the correct dispatcher instance.
Commands
spl_object_id($dispatcher) === spl_object_id($expectedDispatcher) ? 'same' : 'different'
Check if the event name string matches exactly (case-sensitive).
Fix now
Register the observer in a central bootstrap file, not inside a controller.
Design Pattern Categories Comparison
CategoryFocusExamplesWhen to Use
CreationalObject creation mechanismsSingleton, Factory, Builder, PrototypeWhen you need to decouple client code from concrete classes or control object lifecycle.
StructuralClass/object compositionAdapter, Decorator, Facade, ProxyWhen you need to combine or extend existing classes without modifying them.
BehavioralObject interaction & responsibilityStrategy, Observer, Command, IteratorWhen you have complex communication patterns or need to encapsulate requests.

Key takeaways

1
Design patterns are reusable solutions to recurring problems
not goals in themselves.
2
Use Creational patterns to decouple object creation from usage.
3
Use Structural patterns to compose existing classes without inheritance mess.
4
Use Behavioral patterns to manage communication and algorithms between objects.
5
Apply patterns only when you have a concrete pain
don't over-engineer.
6
Patterns improve maintainability but add indirection; always balance benefit vs. complexity.

Common mistakes to avoid

3 patterns
×

Using Singleton for mutable state

Symptom
Tests fail in random order; state leaks across requests in long-running processes.
Fix
Replace Singleton with dependency injection. For request-scoped data, use a request-scoped container or a factory that returns a fresh instance each time.
×

Forcing a pattern on simple code

Symptom
Codebase has many small classes that make it hard to follow the flow; every change touches multiple files.
Fix
Refactor back to simpler code. Only extract patterns when you have at least two distinct variations of the same problem.
×

Ignoring the Observer synchronous bottleneck

Symptom
REST API response time jumps when many event listeners are attached.
Fix
Use an asynchronous queue for observers that don't need immediate feedback (e.g., sending emails). In PHP, offload to a message queue like RabbitMQ or use Laravel's queue.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between Factory Method and Abstract Factory patte...
Q02SENIOR
When would you use the Decorator pattern versus inheritance in PHP?
Q03SENIOR
Why is Singleton considered an anti-pattern in many contexts? When is it...
Q01 of 03SENIOR

Explain the difference between Factory Method and Abstract Factory patterns with PHP examples.

ANSWER
Factory Method uses inheritance: a single method that subclasses override to create objects. Abstract Factory uses composition: an interface with multiple factory methods that create related families of objects. In PHP, Factory Method is simpler — you define an abstract method in a base class. Abstract Factory is useful when you have multiple product types (e.g., UI buttons and checkboxes) that must match across themes.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is a design pattern in simple terms?
02
Are design patterns specific to PHP?
03
How many design patterns should a senior PHP developer know?
04
Can I use too many design patterns?
🔥

That's Advanced PHP. Mark it forged?

4 min read · try the examples if you haven't

Previous
PHP Exception Handling
3 / 13 · Advanced PHP
Next
PHP Security Best Practices