DEV Community

Design Patterns in Programming: Stop Building Spaghetti Code (2026 Edition) | Mahdi Shamlou

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.


Mahdi Shamlou

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Creational — How to create objects
  2. Structural — How to organize relationships between objects
  3. 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!)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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",
)
Enter fullscreen mode Exit fullscreen mode

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()
)
Enter fullscreen mode Exit fullscreen mode

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"
)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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?

Mahdi Shamlou

If you enjoyed this deep dive, check out my other articles:


🔗 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)