Mahdi Shamlou here.
If you've read my OWASP Top 10 or durable workflow engines articles, you know I care about writing code that actually works in production. If you've seen my message broker comparison, you know I think deeply about system architecture.
Today, we're tackling something that separates junior developers from senior ones:
Which design patterns should you actually use in 2026?
I've seen countless codebases where developers either:
- Don't use any patterns (chaos)
- Overuse patterns everywhere (over-engineered nightmare)
- Use patterns they don't understand (copy-paste disaster)
- Pick the wrong pattern for the job (wrong tool, right hand)
Most tutorials teach patterns in a vacuum. They don't tell you when, where, and why to use them.
So I decided to write a practical guide that shows you the design patterns that matter, with real Python code you can use today.
Let's dive in.
What Are Design Patterns?
A design pattern is a reusable solution to a common programming problem.
Instead of reinventing the wheel every time, you use a proven blueprint that other developers have tested in real systems.
Think of them like recipes:
Instead of figuring out how to make pasta:
You follow a pattern (boil water → add salt → cook pasta → drain)
Design patterns help you:
- Write cleaner code
- Make systems easier to maintain
- Avoid common mistakes
- Communicate with other developers
- Build scalable systems
The most practical patterns fall into three groups:
- Creational — How to create objects
- Structural — How to organize relationships between objects
- Behavioral — How objects interact and communicate
Creational Patterns (That Actually Matter)
1. Singleton Pattern
The Singleton pattern ensures only one instance of a class exists.
When to use it:
- Database connections
- Configuration managers
- Logging systems
- Cache managers
Bad code (without pattern):
# Every time you call this, you get a new instance
class DatabaseConnection:
def __init__(self):
self.connection = connect_to_db()
print("New connection created")
db1 = DatabaseConnection() # "New connection created"
db2 = DatabaseConnection() # "New connection created" again!
# Now you have 2 connections (bad!)
Good code (with Singleton):
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnection(metaclass=Singleton):
def __init__(self):
self.connection = connect_to_db()
print("Connection created once")
db1 = DatabaseConnection() # "Connection created once"
db2 = DatabaseConnection() # Reuses same instance, nothing printed
print(db1 is db2) # True - same object
Pros:
- Only one instance in memory
- Thread-safe when done right
- Good for shared resources
Cons:
- Can make testing harder
- Global state (use carefully)
- Can hide dependencies
2. Factory Pattern
The Factory pattern creates objects without telling the client what class to instantiate.
When to use it:
- Creating different types of objects based on conditions
- Database drivers (MySQL, PostgreSQL, MongoDB)
- Payment processors (Stripe, PayPal, Square)
- Log handlers (File, Email, Slack)
Bad code (without pattern):
# You need to know about every payment type
if payment_type == "stripe":
processor = StripeProcessor()
elif payment_type == "paypal":
processor = PayPalProcessor()
elif payment_type == "square":
processor = SquareProcessor()
else:
raise ValueError("Unknown payment type")
processor.process_payment(amount)
Good code (with Factory):
class PaymentProcessor:
def process_payment(self, amount):
raise NotImplementedError
class StripeProcessor(PaymentProcessor):
def process_payment(self, amount):
return f"Processing ${amount} with Stripe"
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount):
return f"Processing ${amount} with PayPal"
class SquareProcessor(PaymentProcessor):
def process_payment(self, amount):
return f"Processing ${amount} with Square"
class PaymentFactory:
_processors = {
"stripe": StripeProcessor,
"paypal": PayPalProcessor,
"square": SquareProcessor,
}
@staticmethod
def create(payment_type):
processor_class = PaymentFactory._processors.get(payment_type)
if not processor_class:
raise ValueError(f"Unknown payment type: {payment_type}")
return processor_class()
# Now you just ask the factory
processor = PaymentFactory.create("stripe")
processor.process_payment(100) # "Processing $100 with Stripe"
Pros:
- Easy to add new types
- Decouples client from concrete classes
- Follows Open/Closed principle
Cons:
- More classes to manage
- Can be overkill for simple cases
3. Abstract Factory Pattern
The Abstract Factory pattern creates families of related objects without specifying their concrete classes.
If Factory Method answers:
"Which object should I create?"
Abstract Factory answers:
"Which group of related objects should I create together?"
Think about a real application that supports multiple databases.
When you choose PostgreSQL, you don't just need a PostgreSQL connection.
You also need:
- PostgreSQL repositories
- PostgreSQL query builders
- PostgreSQL transaction managers
When you switch to MongoDB, you need the MongoDB versions of all those components.
Without Abstract Factory, your code becomes full of conditionals.
When to use it:
- Supporting multiple databases
- Multi-cloud systems (AWS, Azure, GCP)
- Cross-platform applications
- UI frameworks with multiple themes
- Plugin architectures
Bad code (without pattern)
database_type = "postgres"
if database_type == "postgres":
connection = PostgreSQLConnection()
repository = PostgreSQLUserRepository()
transaction = PostgreSQLTransactionManager()
elif database_type == "mongo":
connection = MongoConnection()
repository = MongoUserRepository()
transaction = MongoTransactionManager()
else:
raise ValueError("Unsupported database")
The problem:
- Every new database requires more conditionals
- Related objects can accidentally be mixed
- Business logic becomes tightly coupled to implementations
Imagine accidentally creating:
connection = PostgreSQLConnection()
repository = MongoUserRepository()
Now your application is broken.
Good code (with Abstract Factory)
from abc import ABC, abstractmethod
# Products
class Connection(ABC):
pass
class UserRepository(ABC):
pass
class TransactionManager(ABC):
pass
# PostgreSQL Family
class PostgreSQLConnection(Connection):
pass
class PostgreSQLUserRepository(UserRepository):
pass
class PostgreSQLTransactionManager(TransactionManager):
pass
# MongoDB Family
class MongoConnection(Connection):
pass
class MongoUserRepository(UserRepository):
pass
class MongoTransactionManager(TransactionManager):
pass
# Abstract Factory
class DatabaseFactory(ABC):
@abstractmethod
def create_connection(self):
pass
@abstractmethod
def create_repository(self):
pass
@abstractmethod
def create_transaction_manager(self):
pass
# Concrete Factories
class PostgreSQLFactory(DatabaseFactory):
def create_connection(self):
return PostgreSQLConnection()
def create_repository(self):
return PostgreSQLUserRepository()
def create_transaction_manager(self):
return PostgreSQLTransactionManager()
class MongoFactory(DatabaseFactory):
def create_connection(self):
return MongoConnection()
def create_repository(self):
return MongoUserRepository()
def create_transaction_manager(self):
return MongoTransactionManager()
# Usage
factory = PostgreSQLFactory()
connection = factory.create_connection()
repository = factory.create_repository()
transaction = factory.create_transaction_manager()
Now all objects belong to the same family and remain compatible.
Pros:
- Guarantees compatibility between related objects
- Makes switching implementations easy
- Reduces conditional logic
- Follows the Open/Closed Principle
Cons:
- Adds extra abstraction
- More classes to maintain
- Can be overkill for small projects
My Take:
Most developers don't need Abstract Factory every day.
But when you're building systems that support multiple providers, databases, clouds, or platforms, Abstract Factory can save you from hundreds of conditionals and a lot of architectural pain.
4. Builder Pattern
The Builder pattern constructs complex objects step by step instead of creating them with a massive constructor.
As applications grow, objects often require many optional parameters.
Soon you end up with constructors that are difficult to read, understand, and maintain.
The Builder pattern solves this problem by separating the construction process from the final object.
Think of ordering a custom computer.
You choose:
- CPU
- RAM
- Storage
- GPU
- Operating System
The computer is built step by step before the final product is delivered.
That's exactly what the Builder pattern does.
When to use it
- Complex configuration objects
- API request builders
- Query builders
- Domain models with many optional fields
- Object creation involving multiple steps
Bad code (without pattern)
class User:
def __init__(
self,
name,
email,
phone=None,
address=None,
role=None,
avatar=None,
timezone=None,
language=None,
):
self.name = name
self.email = email
self.phone = phone
self.address = address
self.role = role
self.avatar = avatar
self.timezone = timezone
self.language = language
user = User(
"Mahdi",
"mahdi@example.com",
None,
None,
"admin",
None,
"UTC",
"en",
)
Problems:
- Hard to read
- Easy to pass arguments incorrectly
- Constructor grows forever
Good code (with Builder)
class UserBuilder:
def __init__(self):
self.user = {}
def name(self, value):
self.user["name"] = value
return self
def email(self, value):
self.user["email"] = value
return self
def role(self, value):
self.user["role"] = value
return self
def timezone(self, value):
self.user["timezone"] = value
return self
def language(self, value):
self.user["language"] = value
return self
def build(self):
return self.user
user = (
UserBuilder()
.name("Mahdi")
.email("mahdi@example.com")
.role("admin")
.timezone("UTC")
.language("en")
.build()
)
The result is far more readable.
Pros
- Improves readability
- Handles optional parameters elegantly
- Makes object creation more explicit
- Avoids giant constructors
Cons:
- Additional class to maintain
- Slightly more code
My Take:
Builder is one of the most practical patterns in modern software development.
Whenever you see constructors with ten or more parameters, Builder should immediately come to mind.
5. Prototype Pattern
The Prototype pattern creates new objects by cloning existing ones instead of constructing them from scratch.
This pattern becomes useful when object creation is expensive, slow, or complicated.
Rather than rebuilding everything every time, you create a copy of an existing object and modify only what changes.
Think about document templates.
Instead of creating a new invoice from scratch every time, you start with an invoice template and clone it.
When to use it
- Expensive object creation
- Template systems
- Game development
- Document generation
- Large configuration objects
Bad code (without pattern)
class Invoice:
def __init__(
self,
company_name,
company_address,
tax_rate,
footer,
):
self.company_name = company_name
self.company_address = company_address
self.tax_rate = tax_rate
self.footer = footer
invoice1 = Invoice(
"My Company",
"New York",
0.15,
"Thank you for your business"
)
invoice2 = Invoice(
"My Company",
"New York",
0.15,
"Thank you for your business"
)
invoice3 = Invoice(
"My Company",
"New York",
0.15,
"Thank you for your business"
)
The same data is repeated over and over.
Good code (with Prototype)
from copy import deepcopy
class Invoice:
def __init__(
self,
company_name,
company_address,
tax_rate,
footer,
):
self.company_name = company_name
self.company_address = company_address
self.tax_rate = tax_rate
self.footer = footer
def clone(self):
return deepcopy(self)
template = Invoice(
"My Company",
"New York",
0.15,
"Thank you for your business"
)
invoice = template.clone()
invoice.customer = "John Doe"
invoice2 = template.clone()
invoice2.customer = "Jane Doe"
Now the common configuration is defined only once.
This is faster and easier to maintain.
Pros:
- Faster object creation
- Reduces duplication
- Useful for template systems
- Simplifies complex initialization
Cons:
- Deep copying can become complicated
- Circular references require care
My Take:
Prototype is less common than Factory or Builder, but when object creation becomes expensive, it's incredibly useful.
Many developers never explicitly implement Prototype, but they use the idea constantly through templates, cloning, and copying existing configurations.
Quick Comparison
| Pattern | Purpose | Complexity | Common Use Cases |
|---|---|---|---|
| Singleton | Ensure one instance exists | Low | Logging, Configuration |
| Factory Method | Create objects conditionally | Low | Payment Providers, Storage Drivers |
| Abstract Factory | Create related families of objects | Medium | Databases, Cloud Providers |
| Builder | Construct complex objects step by step | Medium | Configurations, API Requests |
| Prototype | Clone existing objects | Medium | Templates, Games, Documents |
Which Creational Patterns Should You Actually Use?
If you're building modern backend systems, these are the patterns you'll encounter most often:
- Factory Method : Probably the most common pattern in production systems. Use it whenever object creation depends on runtime conditions.
- Builder : Extremely useful for complex objects and configurations. Many modern frameworks use Builder-style APIs.
- Singleton (Carefully) : Useful for shared resources, but dependency injection is often a better choice.
- Abstract Factory : Valuable when supporting multiple providers, databases, platforms, or cloud vendors. Most small projects won't need it.
- Prototype : Less common, but powerful when object creation becomes expensive or repetitive.
Final Thoughts
Design patterns are not rules. They're tools.
Use them when they solve real problems:
- Factory when you need to create different types
- Strategy when you have multiple ways to do something
- Repository when you need to abstract data access
- Observer when you have real events
- Decorator when you need to add features without modifying code
- Singleton carefully, and usually prefer dependency injection
The best code is code that's easy to read, test, and change. Patterns help with that. But bad code with patterns is still bad code.
Start with simple code. Add patterns when you need them. Don't add them "just in case."
What's Next?
This article focused entirely on Creational Design Patterns.
In the next article, we'll explore Structural Design Patterns
After that, we'll dive into Behavioral Design Patterns
Each article will include practical Python examples, production use cases, common mistakes, and guidance on when a pattern is actually worth using.
Stay tuned.
Want More?
If you enjoyed this deep dive, check out my other articles:
- Message Brokers in 2026: Kafka, RabbitMQ, NATS — Which One Should You Really Use — Architecture decisions
- OWASP Top 10 for Developers (2026 Edition) — How to Actually Fix the Most Dangerous Web Vulnerabilities — Security patterns
- Injection Attacks Are Not Dead: SQL, NoSQL, ORM, and Command Injection — How to Actually Fix Them — Attack patterns
- Durable Workflow Engines: Temporal vs dbt OS vs Transact vs Prefect — System patterns
🔗 LinkedIn: https://www.linkedin.com/in/mahdi-shamlou-3b52b8278
📱 Telegram: https://telegram.me/mahdi0shamlou
📸 Instagram: https://www.instagram.com/mahdi0shamlou/
Author: Mahdi Shamlou | مهدی شاملو


Top comments (0)