You're three weeks into a codebase, and you find it. A `StrategyFactory` that builds `StrategyProvider` objects, each wrapping a single `if` statement. Somewhere upstream, an `AbstractSingletonProxyFactoryBean`-style contraption decides which strategy to inject, through a registry, through a config file, for a business rule that changes twice a year. You didn't need any of this. You needed an `if`.
That's the moment every senior engineer has with design patterns. Not the moment you learn them. The moment you realize half the codebase applied them like a checklist instead of a diagnosis.
Design patterns aren't the problem. Cargo-culting them is. There's a real, narrow set of patterns senior engineers reach for because the shape of the problem demands it, and there's a much longer list that shows up because someone read the Gang of Four book on a plane and wanted to use it. This article is about telling those two piles apart, out loud, with names attached.
Strategy Is the Design Pattern That Earns Its Keep When Behavior Actually Varies
Strategy is probably the most legitimately useful pattern on this list, and also the most abused. The real test is simple: does the behavior change based on something you only know at runtime, and will more variants show up later?
Payment processing is the textbook case for a reason. Credit card, wallet, bank transfer, buy now, pay later. Each has its own validation, its own retry logic, its own failure modes. You genuinely don't know which one you're dealing with until a request comes in, and you genuinely expect a fifth payment method next quarter. Strategy gives you a clean seam to add it without touching the other four.
Now compare that to the version you see in "clean code" tutorials: a `DiscountStrategy` interface with exactly two implementations, `PercentageDiscount` and `FixedDiscount`, that will never be extended to a third. That's not variation. That's two `if` branches wearing a costume. If you can enumerate every case in a single method and nobody on the roadmap is asking for a third, you don't need the interface, the factory to build it, and the registry to look it up. You need a function.
The tell is stability. Strategy is for behavior that's stable in shape but unstable in content. The moment your "strategies" are actually steps that need to happen in a specific order, you don't want Strategy. You want a plain function that calls other plain functions, in order, and you want to be able to read it top to bottom without opening four files.
Observer Is for Real Events, Not for Decoupling You Don't Need
Observer has a strange reputation because everyone's used it without naming it. Every event listener, every pub/sub queue, every webhook system is an Observer in production clothes. That's not an accident. It's a genuinely good fit for a genuinely common problem: something happens, and an unknown or growing number of things need to react to it.
A user signs up. Marketing wants to send a welcome email. Analytics wants to log the event. Billing wants to start a trial clock. None of those three care about each other, and you don't want the signup code to know all three exist. That's Observer solving a real coupling problem, and it's why every serious event system in production uses some version of it, formalized or not.
Where it goes sideways is when someone builds an internal observer system, with subscribe and notify and a list of listeners, for two things that always happen together and always will. If `saveUser()` always needs to call `sendConfirmationEmail()`, immediately, synchronously, with no other listener ever joining, you've built an event bus to call one function. That's not decoupling. That's obfuscation with extra ceremony, and the person debugging it in six months won’t thank you.
The honest question before reaching for Observer: is there more than one interested party, and will there realistically be more? If the answer is "just one, forever," write the direct call and move on with your day.
Factory Design Patterns Only Make Sense When Object Creation Actually Needs to Vary
Factory Method and Abstract Factory exist because sometimes constructing an object is genuinely complicated, or the exact type you need isn't known until runtime, or you want to swap implementations for testing without rewriting the caller. All real problems. None of them show up as often as the pattern shows up in the wild.
A logging system that instantiates `FileLogger`, `ConsoleLogger`, or `CloudLogger` based on the environment configuration is a legitimate Factory use case. The caller doesn't need to know which one it gets; the decision genuinely depends on runtime configuration, and new logger types will plausibly appear. That's the pattern doing its job.
What you'll actually find in a lot of "enterprise" code is a factory for a class with one constructor and one implementation, built because "you should always use factories for object creation," a rule someone absorbed without the second half of the sentence. If `new Invoice()` works fine and will keep working fine, wrapping it in `InvoiceFactory.createInvoice()` adds a file, a layer of indirection, and zero flexibility you'll ever use. You've made the code harder to trace for a benefit that only exists in a hypothetical future.
Senior engineers reach for a constructor first. They reach for Factory when the construction logic is actually branching, actually complex, or actually needs to be swappable. Everyone else reaches for Factory because it looks like architecture on a resume.
Singleton Is the Pattern Most Likely to Be Solving a Problem You Created
If your instinct for using Singleton is "it's annoying to pass this everywhere," that's dependency injection avoiding you, not a real constraint. Fix the plumbing. Don't reach for a pattern that turns a local annoyance into a global one.
The Real Skill Is Naming the Problem Before Naming the Pattern
Here's what separates the senior engineer from the resume-driven pattern user: they name the problem first, then check whether a pattern fits, rather than picking a pattern and then hunting for a problem it can solve. "Behavior varies based on runtime input and will keep growing" comes before Strategy. "Multiple independent parties need to react to one event" comes before Observer. Not the other way around.
This is also why so many patterns end up over-engineered in interview prep and portfolio projects specifically. There's real pressure to demonstrate you know the Gang of Four catalog, so patterns get inserted where the problem doesn't call for them, because the pattern is the point of the exercise instead of a byproduct of solving something real. A hiring manager who's actually senior can tell the difference in about ninety seconds, and it doesn't read as sophistication. It reads as if someone hasn’t shipped enough real systems to know when a shortcut is better than the ceremony.
Composition, plain functions, and a well-placed `if` statement are no less impressive than a pattern. They're what patterns are supposed to simplify into once you strip away the parts that don't apply to your problem. A Strategy pattern with two stable branches isn't halfway to good design. It's a function that took the long way there.
Where This Actually Lands
Patterns are compressed experience, not requirements. They exist because smart people kept solving the same five problems the same way and gave the shape a name. That's useful. What's not useful is treating the catalog as a checklist to satisfy, rather than a set of tools you reach for when the problem's shape matches.
The rule that actually holds up under real production pressure: if you can describe the problem without naming the pattern, and the pattern still turns out to be the right shape, use it with confidence. If you can only justify the pattern by naming the pattern, that's the sound of a book talking, not a problem. Delete the abstraction and write the twelve lines of code that do the thing. The codebase and whoever inherits it after you will be better for it.

