Everyone who learns Design Patterns probably goes through the same phase. You read about a bunch of patterns and suddenly every one of them looks useful. Then you go back to your project and start looking for places to use them.
I did the same thing.
A few months later, I looked at my own code and wondered why I had created three layers of abstraction just to read a config file.
The problem is that most tutorials explain what a pattern is , but spend much less time explaining when to use it and when not to use it .
Knowing the Pattern Isn’t Enough
A pattern is not just a solution to a problem. It also comes with a cost. This is the part that often gets ignored when learning Design Patterns.
See a lot of if/elif statements? Think Strategy. See too many parameters? Think Builder. If every problem immediately makes you think of a pattern, your code can easily become more complicated than it needs to be.
Design Patterns are not a checklist for your project. The goal is not to use as many patterns as possible. The goal is to make your code easier to understand and easier to change .
So why learn Design Patterns?
First, they give teams a shared vocabulary. When someone says “use Strategy here,” the team can quickly understand the general design without having to explain everything from scratch.
Second, patterns help you recognize familiar problems. When you face a new problem that looks similar to something you’ve solved before, you can start from a known approach instead of figuring everything out from scratch.
But remember one thing: knowing a pattern doesn’t mean you have to use it .
Every Pattern Is a Trade-off
Most patterns do something similar: they reduce dependencies between parts of the code, but in return, they introduce another layer of indirection.
For example, instead of calling Stripe directly:
def create_order(cart):
stripe.charge(cart.total)
We can separate the dependency:
def create_order(cart, gateway: PaymentGateway):
gateway.charge(cart.total)
The second version means create_order no longer depends directly on Stripe. If you need to switch to VNPay or another payment gateway later, it becomes easier. Testing is also easier because you can pass in a fake gateway.
But there is a cost. The code is no longer as straightforward to read. If you want to know what gateway actually is, you have to trace it back to where it was created.
So before using a pattern, ask one simple question: does this part of the code change often?
If nobody is going to change the payment gateway for the next few years, adding another abstraction might not be worth it. But if the system needs to switch gateways regularly, that abstraction can be very useful.
In other words, don’t ask, “Can I use this pattern here?”
Ask “Where does this pattern make my code easier to change?”
If you don’t have a good answer, you probably don’t need the pattern.
SOLID Through Real Code
SOLID is often taught as five definitions that are easy to forget. A simpler way to understand it is to look for the signs directly in your code.
Single Responsibility means a class shouldn’t be responsible for too many unrelated things. If UserManager validates users, saves them to the database, and sends welcome emails, it may be time to split it up.
Open/Closed means that adding a new feature shouldn’t always require modifying existing code. However, you don’t need to prepare an abstraction for every possible future requirement. You can wait until there is a real need to extend the system.
Liskov Substitution is about inheritance. A subclass should be usable wherever its parent class is expected without breaking the program. If a subclass has to violate what the parent class promises, the inheritance relationship may not be appropriate.
Interface Segregation is simple: don’t force a class to implement things it doesn’t need. Smaller interfaces are usually easier to use than one large interface with too many methods.
Dependency Inversion means separating business logic from low-level technical details. For example, order processing shouldn’t need to know whether it is working directly with PostgreSQL or some other specific database.
The important thing is that SOLID isn’t a set of rules you have to follow mechanically. These principles are simply tools for recognizing when code is becoming difficult to understand or change.
DRY, KISS, and YAGNI
These three principles are quite simple.
DRY means avoiding duplicated information. But that doesn’t mean every two pieces of similar code should immediately be merged.
For example, customer address validation and warehouse address validation might look exactly the same today. But if they are likely to change in different ways, putting them into the same function can make the code harder to maintain later.
Sometimes, a little duplication is better than the wrong abstraction.
KISS means keeping things simple. If two solutions work equally well, choose the one that you can still understand at 2 AM when something breaks.
YAGNI stands for “You Aren’t Gonna Need It.” Don’t write code for requirements that don’t exist yet. An abstraction created for a future that may never happen often just makes today’s code more complicated.
Anti-pattern
Not every design approach that is commonly used is necessarily a good one. Some approaches appear frequently in codebases but make the code harder to understand and change. These are often referred to as Anti-patterns .
A common example is overusing patterns .
When learning a new pattern, it is easy to want to apply it everywhere, even when the problem does not actually require it. As a result, a simple piece of code can turn into multiple classes, interfaces, and abstractions just to solve a very small problem.
The problem is not the pattern itself. The problem is making the code more complex without having a real need for that complexity in return.
A simple way to avoid this is to ask where a pattern will make the code easier to change or understand before introducing it. If you cannot give a clear answer, you probably do not need the pattern yet.
What’s Next
The upcoming posts will follow the same approach. Each pattern will start with a real problem, and I’ll explain the idea in the simplest way possible.
More importantly, every post will clearly explain when to use the pattern and when not to use it .
If Python already has a feature that solves the problem without needing a pattern, I’ll say that too. The goal isn’t to use more Design Patterns. The goal is to write code that is simple, easy to understand, and easy to change .