Design Patterns, Explained Simply · Modern Patterns and Choosing Well
Anti-Patterns and Over-Engineering
The most common ways patterns go wrong: god objects, singletons as global variables, service locators, premature abstraction and using a favourite pattern for everything.
The problem
A team discovers design patterns and starts using them everywhere. A greeting feature ends up with GreetingStrategy, GreetingStrategyFactory, AbstractGreetingProvider and a GreetingService registered in a container: five files to print "Hello, Ada". Meanwhile the real problem in the codebase, a 3,000-line OrderManager that does everything, is untouched.
Patterns are tools. Used without the problem they solve, they make code harder to read.
Real-world analogy
Building a garden shed with the blueprint for a skyscraper: steel frame, elevator shaft, fire stairs. Every part is "best practice" for skyscrapers. For a shed it is wasted effort and a worse result. Good architects pick the structure that fits the building.
The idea
An anti-pattern is a common response to a recurring problem that looks reasonable but usually makes things worse. The ones you will meet most:
| Anti-pattern | What it looks like | Better |
|---|---|---|
| God object | One class (OrderManager, Utils) that knows and does everything |
Split by responsibility (SRP); Facade for a simple entry point |
| Singleton as global variable | Config.instance() called from everywhere |
One instance created at startup and injected |
| Service locator | container.get("mailer") inside business classes |
Constructor injection of the specific dependencies |
| Golden hammer | Every problem solved with the team's favourite pattern | Start from the problem, not the pattern |
| Speculative generality | Interfaces, factories and hooks "in case we need them" | YAGNI: add the abstraction when the second case arrives |
| Anemic domain model | Objects with only getters and setters; all logic in "services" | Put behaviour with the data it uses, where it helps |
| Copy-paste programming | The same logic in five places with small differences | Extract a function; Template Method or Strategy if variants differ |
Two rules of thumb help:
- YAGNI ("you aren't gonna need it"): don't build for imagined requirements.
- Rule of three: duplicate once if you must; on the third occurrence, extract the abstraction. By then you know what actually varies.
The code
The same output, written twice: an over-engineered version full of patterns, and the plain version that should have been written. Patterns are justified only when the problem they solve exists.
# Over-engineered: strategy + factory + abstract provider + service, for one greeting
class GreetingStrategy:
def greet(self, name):
raise NotImplementedError
class FriendlyGreeting(GreetingStrategy):
def greet(self, name):
return f"Hello, {name}!"
class GreetingStrategyFactory:
def create(self, kind):
if kind == "friendly":
return FriendlyGreeting()
raise ValueError(kind)
class GreetingService:
def __init__(self, factory):
self.factory = factory
def greet(self, name, kind="friendly"):
return self.factory.create(kind).greet(name)
print(GreetingService(GreetingStrategyFactory()).greet("Ada"))
# Plain: what the feature actually needs today
def greet(name):
return f"Hello, {name}!"
print(greet("Ada"))
print("classes in version 1: 4 | in version 2: 0 | same behaviour:",
GreetingService(GreetingStrategyFactory()).greet("Bo") == greet("Bo"))
Output
Hello, Ada! Hello, Ada! classes in version 1: 4 | in version 2: 0 | same behaviour: True
// Over-engineered: strategy + factory + abstract provider + service, for one greeting
interface GreetingStrategy { greet(name: string): string; }
class FriendlyGreeting implements GreetingStrategy {
greet(name: string) { return `Hello, ${name}!`; }
}
class GreetingStrategyFactory {
create(kind: string): GreetingStrategy {
if (kind === "friendly") return new FriendlyGreeting();
throw new Error(kind);
}
}
class GreetingService {
constructor(private factory: GreetingStrategyFactory) {}
greet(name: string, kind = "friendly") { return this.factory.create(kind).greet(name); }
}
console.log(new GreetingService(new GreetingStrategyFactory()).greet("Ada"));
// Plain: what the feature actually needs today
const greet = (name: string) => `Hello, ${name}!`;
console.log(greet("Ada"));
const same = new GreetingService(new GreetingStrategyFactory()).greet("Bo") === greet("Bo");
console.log("classes in version 1: 4 | in version 2: 0 | same behaviour:", same ? "True" : "False");
Output
Hello, Ada! Hello, Ada! classes in version 1: 4 | in version 2: 0 | same behaviour: True
public class Main {
// Plain: what the feature actually needs today
static String greet(String name) { return "Hello, " + name + "!"; }
public static void main(String[] args) {
System.out.println(new GreetingService(new GreetingStrategyFactory()).greet("Ada", "friendly"));
System.out.println(greet("Ada"));
boolean same = new GreetingService(new GreetingStrategyFactory()).greet("Bo", "friendly").equals(greet("Bo"));
System.out.println("classes in version 1: 4 | in version 2: 0 | same behaviour: " + (same ? "True" : "False"));
}
}
// Over-engineered: strategy + factory + abstract provider + service, for one greeting
interface GreetingStrategy { String greet(String name); }
class FriendlyGreeting implements GreetingStrategy {
public String greet(String name) { return "Hello, " + name + "!"; }
}
class GreetingStrategyFactory {
GreetingStrategy create(String kind) {
if (kind.equals("friendly")) return new FriendlyGreeting();
throw new IllegalArgumentException(kind);
}
}
class GreetingService {
private final GreetingStrategyFactory factory;
GreetingService(GreetingStrategyFactory factory) { this.factory = factory; }
String greet(String name, String kind) { return factory.create(kind).greet(name); }
}
Output
Hello, Ada! Hello, Ada! classes in version 1: 4 | in version 2: 0 | same behaviour: True
Run this on your computer — see “Get Started” for setup.
<?php
// Over-engineered: strategy + factory + abstract provider + service, for one greeting
interface GreetingStrategy { public function greet(string $name): string; }
class FriendlyGreeting implements GreetingStrategy {
public function greet(string $name): string { return "Hello, $name!"; }
}
class GreetingStrategyFactory {
public function create(string $kind): GreetingStrategy {
if ($kind === "friendly") return new FriendlyGreeting();
throw new InvalidArgumentException($kind);
}
}
class GreetingService {
public function __construct(private GreetingStrategyFactory $factory) {}
public function greet(string $name, string $kind = "friendly"): string { return $this->factory->create($kind)->greet($name); }
}
echo (new GreetingService(new GreetingStrategyFactory()))->greet("Ada"), "\n";
// Plain: what the feature actually needs today
function greet(string $name): string { return "Hello, $name!"; }
echo greet("Ada"), "\n";
$same = (new GreetingService(new GreetingStrategyFactory()))->greet("Bo") === greet("Bo");
echo "classes in version 1: 4 | in version 2: 0 | same behaviour: ", $same ? "True" : "False", "\n";
Output
Hello, Ada! Hello, Ada! classes in version 1: 4 | in version 2: 0 | same behaviour: True
If greetings later need five styles chosen per customer, refactor the function into a Strategy then. The pattern will fit the real variation instead of a guessed one.
Common mistakes
- Treating patterns as a checklist ("our codebase should use all 23").
- Naming without the pattern: a class called
UserFactorythat doesn't create anything, or anAdapterthat is really a Facade. Wrong names mislead readers. - Refactoring toward patterns without tests. Changing structure safely needs tests that pin the behaviour first.
- Ignoring the language. Many patterns shrink to one line in languages with first-class functions (Strategy, Command, Observer as callbacks), and some disappear (Iterator via generators, Singleton via modules).
Key points
- An anti-pattern is a common solution that looks right but causes more problems than it solves.
- The usual ones: god object, singleton-as-global, service locator, golden hammer, speculative generality.
- Apply YAGNI and the rule of three: abstract when the variation is real.
- The goal is simple, changeable code. Patterns are a means, not the goal.
- Further reading: God object and Martin Fowler on service locators.
Practice questions
What is an anti-pattern? Give three examples.
Answer
A common response to a recurring problem that seems reasonable but usually makes things worse. Examples: the god object (one class doing everything), singleton as a global variable, service locator hiding dependencies, the golden hammer (one favourite solution for everything), and speculative generality.
What do YAGNI and the rule of three mean for applying design patterns?
Answer
YAGNI (you aren't gonna need it): don't add abstractions for imagined future needs. Rule of three: tolerate a little duplication, and when the third similar case appears, extract the abstraction, because by then you know what actually varies.
How would you refactor a god object?
Answer
Add tests that pin its current behaviour, then identify responsibilities that change for different reasons and extract them into focused classes (SRP), injecting them back. A facade can keep a simple entry point for existing callers while the internals are split.
Open in the app to mark lessons complete, take notes and track your progress.