Design patterns are tools. Used well, they capture a recurring solution in a name people recognize. Used badly, they become a vocabulary problem — the code is "Factory" or "Singleton" because someone read a book, not because the pattern actually solves anything.
This post is the patterns most commonly misapplied, what the anti-pattern looks like, and what to do instead.
Singleton Where a Service Container Would Do
The Singleton pattern enforces exactly one instance of a class. Used to solve "I need shared access to this object from anywhere."
class Database {
private static ?Database $instance = null;
public static function getInstance(): self {
return self::$instance ??= new self();
}
}
// Usage
Database::getInstance()->query(...);
The problems:
- Hidden dependencies. Code that calls
Database::getInstance()does not declare it as a dependency. Tests cannot substitute it. - Global mutable state. One singleton, one shared state, all the concurrency and ordering issues that come with it.
- Hard to swap implementations. Production wants Postgres, tests want SQLite. Singleton hard-codes one of them.
Modern alternative: dependency injection through a service container. Bind the interface once; inject it where needed.
class UserRepository {
public function __construct(private Database $db) {}
}
The container creates one instance and shares it — the same outcome as a singleton, without the global access. Tests can bind a fake. Production can swap implementations.
If you find yourself reaching for Singleton, you are usually reaching for "shared instance" — which is what containers give you cleanly.
Factory of Factories
The Factory pattern abstracts object creation. Useful when the creation logic is non-trivial. The anti-pattern: applying factory layering speculatively.
// Real problem
$user = (new UserFactory())->createFromRequest($request);
// Anti-pattern
$userFactoryFactory = new UserFactoryFactory();
$userFactory = $userFactoryFactory->createFactoryFor('default');
$user = $userFactory->createFromRequest($request);
There is only one factory and there will only ever be one. The factory of factories adds a layer for no reason.
The right time for a factory is when:
- Construction takes more than a constructor's worth of code
- The same construction is repeated in multiple call sites
- Different inputs produce different concrete types
If none of those apply, just call new.
Inheritance Where Composition Would Do
abstract class Vehicle {
abstract public function go(): void;
}
class Car extends Vehicle { /* ... */ }
class Truck extends Vehicle { /* ... */ }
class FlyingCar extends Car { /* uh oh */ }
Inheritance models "is-a" relationships. The classic failure mode: forcing a class hierarchy that does not actually exist. A FlyingCar is not really a Car-with-flying — it has features from cars and features from aircraft.
The composition alternative: build behavior out of small components.
class FlyingCar {
public function __construct(
private DriveBehavior $drive,
private FlyBehavior $fly,
) {}
}
Each component can be swapped or replaced. New combinations are new compositions, not new subclasses.
The "favor composition over inheritance" rule has been around for decades. Apply it especially when the hierarchy is starting to get awkward.
Observer Misapplied as Event Bus
The Observer pattern fits when one object directly notifies a known set of dependents about its state changes. Languages and frameworks have made events first-class, and "event bus" has become the default mental model for any pub/sub.
The anti-pattern: using events to coordinate the wrong things.
// Suspicious — events as control flow
event(new OrderShouldBeValidated($order));
// ... and another listener
event(new OrderShouldBeCharged($order));
event(new OrderShouldBeShipped($order));
If the same handler always responds to event A by triggering event B, you have a workflow expressed as events. The control flow is implicit and hard to follow. Just call the next step directly, or use an orchestrator pattern.
Events are right when:
- Multiple unrelated systems need to react to the same fact
- The producer should not know about the consumers
- Each consumer's failure should not affect others
Events are wrong when they hide a workflow that would be clearer as code.
Service Class That Should Be a Function
Some "service classes" are functions wearing a costume.
final class OrderTotalCalculator
{
public function calculate(Order $order): int
{
return array_sum(array_map(fn ($i) => $i->price * $i->quantity, $order->items));
}
}
// Usage
$total = (new OrderTotalCalculator())->calculate($order);
The class has one method, no state, no dependencies. It is a function with extra steps.
Better:
namespace App\Domain\Orders;
function calculateOrderTotal(Order $order): int
{
return array_sum(array_map(fn ($i) => $i->price * $i->quantity, $order->items));
}
Or a static method on the entity itself if it is closely related.
Classes are for things with state and behavior. A class with no state and one method is a function. PHP supports namespaced functions; use them.
Premature Strategy Pattern
The Strategy pattern lets you swap algorithms behind a common interface. When the algorithms genuinely vary, it is the right tool. When there is one algorithm and there will only be one, it is overhead.
// Premature
interface PricingStrategy {
public function calculate(Cart $cart): int;
}
class DefaultPricingStrategy implements PricingStrategy { /* the only one */ }
// In the service
public function __construct(private PricingStrategy $strategy) {}
If there is one pricing strategy, do not abstract it behind an interface. Add the interface when a second strategy actually exists — not before.
This applies to most "pluggable" abstractions. Future flexibility has a cost paid every day; future plugability that never materializes is pure cost.
Adapter That Adapts to Itself
The Adapter pattern bridges incompatible interfaces. When you wrap a class in another class that has the same methods and behavior, you have created confusion, not adaptation.
// Pointless
class UserServiceAdapter
{
public function __construct(private UserService $userService) {}
public function findUser(int $id): User
{
return $this->userService->findUser($id);
}
}
If the wrapper does no translation, no extra logic, no error handling — delete it.
The real adapter pattern translates: it converts a Stripe Charge object into your domain's Payment, or maps a legacy SOAP service into a modern REST-style interface. If your "adapter" is a pass-through, it is not adapting anything.
Decorator That Should Be a Method
The Decorator pattern adds behavior to an object dynamically. Useful for cross-cutting concerns — adding logging, caching, or rate limiting to an existing service.
// Real decorator
class CachedUserRepository implements UserRepository
{
public function __construct(
private UserRepository $inner,
private Cache $cache,
) {}
public function find(int $id): ?User
{
return $this->cache->remember("user:{$id}", 60, fn () => $this->inner->find($id));
}
}
The anti-pattern: using decorators to add normal behavior that belongs in the class itself.
// Should not be a decorator
class UserRepositoryWithEmailLookup
{
// Just add findByEmail to the original class
}
Decorators are for adding orthogonal concerns to existing types — not for adding methods that belong in the type.
When the Pattern Name Is Confusing
If you find yourself explaining what your XxxFactoryProviderManager does, the name is not pulling its weight. Patterns are useful when the name communicates the role. When the role does not match the pattern, you have a non-standard class with a confusing name.
The cure is to look at what the class actually does and rename it. A class that fetches data is UserFinder, not UserDataProviderFactoryManager. A class that constructs objects is OrderBuilder, not OrderInstantiationOrchestrator.
The Rule
Design patterns are tools that solve specific problems. Reaching for them prophylactically — because they might be useful someday — produces code that has all the costs of indirection with none of the benefits.
The honest question to ask before applying a pattern: "what specific problem in this code does this pattern solve?" If the answer is concrete (multiple algorithms, real swapping, real cross-cutting concerns), apply it. If the answer is "future flexibility," wait until the flexibility is needed.
Pattern names are useful vocabulary. They are not a substitute for actually understanding the problem.
Reviewing a codebase that has acquired more design patterns than the team actually needed? We help teams refactor toward clarity — keeping the patterns that earn their cost and removing the ones that do not. scopeforged.com