An honest patterns course has to end here: on its dark side. If a pattern is a proven solution to a recurring problem, an anti-pattern is the opposite and something more poisonous — an attractive, recurring solution that makes the problem worse, and that often arrives dressed as best practice. The catalog's patterns can turn into anti-patterns when applied without a symptom, and this entire module has been building the defenses: the method from 05-01, the rejections from 05-02, the discipline from 05-04. This lesson completes them with the gallery of the classics, the concrete misuses of what you learned in this course, the warning signs for catching them in code review, and the inverse operation: undoing a badly applied pattern.
Contents
- What an anti-pattern is
- The classic gallery: symptom, cause, remedy
- Misuses of the course's patterns
- Warning signs in code review
- De-refactoring: undoing a badly applied pattern
- Exercises and conclusion
- Module wrap-up
What an anti-pattern is
The term was popularized by the book AntiPatterns (Brown, Malveau, McCormick, Mowbray, 1998), and its definition has two mandatory parts:
- A recurring solution that looks good and produces negative consequences — "bad code" is not enough: the anti-pattern seduces, which is why it keeps recurring.
- A documented remedy — just as a pattern has a problem→solution shape, the anti-pattern has a trap→way-out shape.
The second part is what makes it useful in a team: naming the anti-pattern ("this is a Golden Hammer") is not an insult, it is a diagnosis with the treatment attached — the same shared vocabulary from 01-01, applied to the bad side of the map.
The classic gallery
| Anti-pattern | Symptom | Typical cause | Remedy |
|---|---|---|---|
| God Object | One enormous class that knows and does everything; every change goes through it; impossible to test in isolation | Adding "just one more method" for years; fear of creating classes | Extract Class by responsibility; leave at most a Facade that orchestrates (the sequence from 05-04) |
| Spaghetti Code | Flow impossible to follow: jumps, flags, mile-long methods, everything depends on everything | Growing without design or refactoring; chronic haste | Characterize with tests, extract methods/classes, introduce seams; patterns only afterwards, on top of code that is already readable |
| Golden Hammer | The same solution for every problem ("everything is a microservice", "everything gets an Observer") | Mastering a single tool; past success extrapolated | Diagnosis before solution (05-01); learn the neighboring tool; cross review |
| Lava Flow | Dead or incomprehensible code nobody deletes "just in case"; fossil layers of old attempts | Fear of deleting without tests; loss of historical context | Coverage + brave deletion (the VCS remembers); remove dead flags and branches on every pass through the area |
| Poltergeist | Ephemeral classes that only create or invoke others and vanish; indirection with no contribution | "More classes = more OO"; ritual management layers | Inline Class: merge the ghost into whoever does the real work |
| Copy-Paste Programming | The same block, with subtle variations, in N places; bugs get fixed in N−1 of them | Copying is faster than abstracting... for the first week | Rule of three → extract method/class; if the copies vary along one axis, that axis calls for its pattern (Strategy, Template Method) |
| Magic Numbers / Strings | Nameless literals governing the logic (if (status == 3)) |
Haste; "I'll document it later" | Named constants, enums; if the literal selects behavior, maybe State/Strategy |
| Reinventing the Wheel | A homegrown logging/events/injection framework living alongside the standard one | Ignorance of the ecosystem; "we're special here" syndrome | Use the standard piece (05-03 taught you to recognize them); homegrown code only for your own domain |
Note the asymmetry with the GoF catalog: patterns are chosen; anti-patterns are contracted, like habits. That is why the remedy almost always includes a process change (tests, review, rule of three) and not just a code change.
Misuses of the course's patterns
The part that hits close to home: each family of the catalog has its characteristic way of being misapplied. The five cases you will see most:
Singletonitis: global state in disguise
Singleton is the easiest pattern to write and the most expensive to maintain, and we warned about it back in 02-02. The disease is not having one Singleton: it is getInstance() becoming the normal way to obtain dependencies. Symptoms: tests that contaminate each other (shared state that survives), the impossibility of holding two configurations (remember PideYaConfig when we wanted to test the Mexico market?), invisible dependencies — the method's signature lies about what it uses. Remedy: dependency injection — uniqueness is managed by whoever composes (the container, the main), not by the class; exactly what Spring does with its singleton-per-container (05-03).
Patternitis: over-engineering with a pedigree
The course's founding case: the welcome-email "cathedral" from 01-06 — AbstractEmailSenderFactory, a greeting strategy, a template of templates... for an email that hadn't changed in two years. Patternitis is the Golden Hammer of someone who has studied the catalog: every problem gets a pattern, even without a symptom. You recognize it because the abstractions have a single imaginary reason to exist ("in case some day...") and because the number of files you must open to understand a function grows while what the function does doesn't. Remedy: the five filters of the scale, and the humility of "wait until it hurts". Patterns are earned, not planted — the third time we've written it in this course, because it is the lesson most easily forgotten.
The single-implementation Factory
// The full ceremonial trio, seen in so many real projects:
public interface OrderService { ... }
public class OrderServiceImpl implements OrderService { ... } // the ONLY implementation
public class OrderServiceFactory {
public static OrderService create() {
return new OrderServiceImpl(); // ...and always this one
}
}Three files where one would do. The mirror interface with its Impl and its factory only pay off when there is (or there is imminent evidence of) a second implementation, a proxy to interpose, or a real module boundary to protect — otherwise it is ritual indirection: every read hops through three files and gains nothing. Remedy: Inline — a concrete class and new (or direct injection), and the interface gets extracted in one minute with the IDE the day the second implementation arrives (05-04). That the future extraction is cheap is precisely what makes the speculative extraction unnecessary.
Premature Visitor hierarchies
Visitor was the pattern with the longest fine print in module 4: double dispatch, one method per node type in every visitor, and the node hierarchy frozen (adding a node forces touching every visitor). Setting it up "because the menu will surely have many operations" when there is a single operation — and the node hierarchy is still growing — is buying the exact cost at the worst moment: you pay the rigidity on the axis that is still moving. Remedy: while operations number one or two and the nodes keep changing, methods on the nodes or instanceof with pattern matching (Java 21 made it respectable); Visitor when the axis truly inverts: stable nodes, operations sprouting.
The god mediator
The degeneration foretold in 04-06: DispatchCenter is born to decouple colleagues and, protocol by protocol, absorbs the business logic of all of them — 900 lines where kitchen, couriers and orders are puppets with no behavior of their own. It is the God Object holding a pattern's ID card, and that makes it more dangerous: it survives reviews ("it's a Mediator, it's in the book"). The cutting line: the mediator must coordinate conversations, not execute jobs — if it holds rules that conceptually belong to a colleague, hand them back; if its criteria vary, extract them into injected strategies (like the AssignmentStrategy).
Warning signs in code review
The reviewer's checklist — each sign is a question to ask, not an automatic verdict:
- The pattern's name without its intent: a
*Factorywith only one thing to make, a*Manager/*Helperthat can't say what it manages, an Observer with exactly one observer planned forever. - Abstractions with a population of one: an interface with one implementation, a hierarchy with one leaf, a strategy parameter that always receives the same one. Question: "what is the second variant, and when does it arrive?"
- Indirection with no checkpoint: if following a call takes you through three classes that only delegate without adding a decision, a validation or a translation — it smells of Poltergeist with GoF vocabulary.
- The disproportionate diff: the feature was "add a field to the ticket" and the PR brings two interfaces, a factory and a builder. Question: "what present symptom pays for this structure?"
- Tests fighting the design: if testing requires resetting singletons, mocks of mocks, or booting half the system — the design is screaming; tests are the code's first client and the best anti-pattern detector.
- Justification in the future tense: "this will allow us to...", "when we scale...". Code is justified by present symptoms; the future gets written down in an intent note, not in classes.
- Localized fear: "better not touch that one", pointing at a class. Where there is fear there is a God Object, Lava Flow or missing tests — and probably all three.
De-refactoring: undoing a badly applied pattern
The inverse operation to 05-04, with the exact same discipline — tests first, steps that compile, small commits — and the moves mirrored: where we used to extract, now we inline.
| Surplus pattern | Inverse operation |
|---|---|
| Single-implementation Factory | Inline the factory at the call sites (new or direct injection); delete the interface if nobody else uses it |
| Strategy with a single strategy | Inline the strategy's method into the context; delete interface and field |
| Singleton | Introduce the object as an injected dependency from the top down; getInstance() remains as a deprecated adapter until the last usage dies (the gradual migration from 05-04, mirrored) |
| Premature Visitor | Move each visitX as a method onto the corresponding node (or into a switch with pattern matching); delete accept |
| God mediator | Return each rule to its owning colleague (Move Method); the mediator keeps the pure routing — which was its job |
| Single ceremonial Decorator/layer | Inline the layer into the component; keep it only if it genuinely controls, translates or adds |
A minimal example, undoing the ceremonial factory in three commits:
// Commit 1: the factory delegates... to nothing that deserves to exist. Mark it:
@Deprecated // use new OrderService() or injection — see PR-1841
public class OrderServiceFactory { ... }
// Commit 2..n: each call site, migrated and compiling:
OrderService service = new OrderService(repository, events); // before: Factory.create()
// Final commit: delete OrderServiceFactory and merge interface + Impl
// (Rename OrderServiceImpl → OrderService with the IDE: zero risk).The psychological rule matters as much as the technical one: undoing a pattern is not admitting a shameful mistake — it is the same engineering that put it there, applied to new information (the imagined axis of variation never arrived). A team that knows how to de-refactor applies patterns with less fear, because no decision is a life sentence.
Common Mistakes and Tips
- Using "anti-pattern" as a projectile in review. The diagnosis includes remedy and context or it isn't a diagnosis; "this is Singletonitis, I propose injecting X and Y" builds — a bare "this is an anti-pattern" doesn't.
- Overcorrecting: going from patternitis to "no patterns allowed here" is the same Golden Hammer swung the other way. The criterion is still the symptom, in both directions.
- Confusing an anti-pattern with a different context: a Singleton in a one-afternoon migration script is not Singletonitis; a stable three-case
switchdoesn't call for Strategy. Remedies apply where there is a symptom, not where there is a resemblance. - De-refactoring without a net: removing structure breaks things exactly as much as adding it. Characterization tests here too.
- Tip: in PRs that introduce a pattern, ask for one line of justification with the symptom ("second contract type, third duplicated
if"). It costs ten seconds and filters out 90% of patternitis before it is born. - Tip: keep a small "local gallery" of your own project's anti-patterns (with links to the PRs that undid them): it teaches more than any book, because the examples are from home.
Exercises
Exercise 1: diagnosis in the gallery
Classify each case with its anti-pattern (a classic or one of the course's five) and sketch the remedy in one sentence:
OrderUtilshas 74 static methods; it appears imported in 180 files and there are three versions ofcalculateTotalwith slightly different results.- To read one configuration property:
ConfigReaderFactory.getInstance().createReader().getConfig().getValue("timeout")— four classes, none with logic. - The team solved notifications well with Bridge in module 3; since then, the last three features have come in as "a new Bridge", including one where there was only one dimension.
- In
DiscountManagerthere is a 200-line commented-out block with the note "// old coupon system — DO NOT DELETE (still used in Mexico?)", dated 2024.
Exercise 2: the PR review
A PR arrives titled "Prepare report exporting for the future": it adds ReportExporter (interface), CsvReportExporterImpl (the only implementation), ExporterFactory (always returns the former) and ExportManager (calls the factory and delegates). The sprint's only requirement was "export the closing report to CSV". Write (a) the concrete warning signs from the checklist that apply, (b) the review comment you would leave — constructive, with a remedy and with the criterion for when that structure WOULD be worth it.
Exercise 3: de-refactoring plan
PideYaConfig is still a classic Singleton (getInstance()) used from 23 classes, and the integration tests keep trampling each other's configuration. Design the gradual de-refactoring plan: the order of the steps, how getInstance() and injection coexist during the migration, which safety net you use, and what the final deletion condition is.
Solutions
Solution 1: (1) God Object (in its "utility class" variant) with Copy-Paste inside (three calculateTotals): extract by responsibility toward the domain objects that own each calculation, unify the clones with tests that pin down which of the three behaviors is the correct one. (2) A Poltergeist chain: inline the ghost layers down to config.timeout() — one piece with real logic (read and cache) and none that is ceremonial. (3) Post-success Golden Hammer: back to diagnosis by forces (05-01) — Bridge pays off with two independently varying dimensions; with one, it is a plain interface. (4) Lava Flow: resolve the unknown (does Mexico use it? — the logs or the Mexico team can answer within the hour), and delete; the VCS is the net, the mummified comment is not.
Solution 2: (a) Abstractions with a population of one (an interface, a one-product factory); justification in the future tense ("for the future" in the very title); a disproportionate diff (four classes for a one-class requirement); a probable Poltergeist in ExportManager (it only calls and delegates). (b) A comment along these lines: "The requirement is covered by one CsvReportExporter class and its test. The interface and the factory have no second variant today to justify them — I propose leaving them out and noting the intent: when the second format arrives (is it on the roadmap?), extracting ReportExporter with the IDE takes a minute, and then the factory — or the closing report's Template Method, which already exists — will be the right structure with two real cases in front of it. That way the next person reads one class, not four." — it names the missing symptom, gives the remedy, sets the reopening criterion and humiliates nobody.
Solution 3: Net: characterization tests for the most delicate configuration consumers + the existing integration tests (which are also the motivation: they will stop trampling each other). Steps: 1. make the class injectable (a public or factory constructor; the instance stops managing itself) while keeping getInstance() as a bridge returning a default instance — green, commit; 2. migrate the 23 classes in batches: each one switches to receiving PideYaConfig through its constructor (the batches are dictated by the dependency graph: leaves first, then whoever creates them) — commit per batch; 3. the integration tests now build their own configuration per scenario — the contamination disappears and proves it; 4. @Deprecated on getInstance() once only internal usages remain; 5. deletion condition: zero calls to getInstance() outside the composition point (the main/container, from now on the only place that decides uniqueness). It is the gradual migration from 05-04, mirrored: build the new track, migrate in green batches, demolish the old one at the end.
Conclusion
You now have the map of the dark side: the honest definition of an anti-pattern (seductive solution + consequences + remedy), the classic gallery from God Object to Lava Flow, and the five misuses of the catalog that this very course could provoke — Singletonitis, patternitis, ceremonial factories, premature Visitors and god mediators — with the warning signs for hunting them in review and the mirrored discipline for undoing them without fear. The module's final symmetry is this: applying a pattern and removing it are the same engineering, guided by the same judge — the present symptom, never fashion and never fear.
Module wrap-up
With this, the module on the craft is complete: you know how to choose a pattern with a method and explicit rejections, you have seen the catalog collaborating at real scale in PideYa, you recognize it in the JDK, Spring and other people's code, you know how to refactor toward a pattern with a safety net — and now also away from one when it is surplus. The GoF catalog is no longer a list of 23 index cards: it is a toolbox with usage criteria, which was the course's promise.
But the GoF was written for objects inside a single process, and the software of 2026 lives spread out: services calling each other over the network, threads competing for data, architectures imposing patterns of their own — hexagonal, CQRS, sagas, circuit breakers, producers and consumers. Many will feel familiar, because they are the catalog's same intents stretched over the network and concurrency; others are new fauna with new problems. That is the territory of the module that begins now: see you in Design Patterns in Modern Architectures.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
