top of page

Design Patterns Explained Without the Jargon

  • ShiftQuality Contributor
  • May 17, 2025
  • 5 min read

Design patterns have an image problem. The Gang of Four book from 1994 described 23 patterns using Java-era enterprise language that made simple ideas sound complicated. Then the software industry spent thirty years over-applying them — cramming patterns into code that didn't need them because "we should use the Strategy Pattern here" sounded more sophisticated than "we should use a function."

Strip away the jargon and most patterns are just names for things you already do. Naming them is useful — it gives teams a shared vocabulary for common solutions. Over-engineering them is wasteful. Let's walk through the patterns that actually come up in everyday development, explained as simply as possible.

Patterns You Already Use

Strategy: Swappable Behavior

The jargon: "Define a family of algorithms, encapsulate each one, and make them interchangeable."

The simple version: Instead of hardcoding behavior, pass the behavior in as a parameter.

# Without the pattern — hardcoded behavior
def sort_users(users, sort_by):
    if sort_by == "name":
        return sorted(users, key=lambda u: u.name)
    elif sort_by == "date":
        return sorted(users, key=lambda u: u.created_at)
    elif sort_by == "activity":
        return sorted(users, key=lambda u: u.last_active)

# With the pattern — the "strategy" is just a function
def sort_users(users, key_func):
    return sorted(users, key=key_func)

# Usage
sort_users(users, lambda u: u.name)
sort_users(users, lambda u: u.created_at)

You've been using Strategy every time you pass a callback function, a comparator, or any function as an argument. The pattern just names it.

Observer: "Let Me Know When Something Happens"

The jargon: "Define a one-to-many dependency between objects so that when one changes state, all dependents are notified."

The simple version: Events. When something happens, tell everyone who cares.

// You already use this
button.addEventListener('click', handleClick);
window.addEventListener('resize', handleResize);

// It's the same pattern in your backend
eventBus.on('order.created', sendConfirmationEmail);
eventBus.on('order.created', updateInventory);
eventBus.on('order.created', notifyWarehouse);

Every event listener, every pub/sub system, every webhook is the Observer pattern. One thing happens, multiple other things react to it.

Factory: "Make Me One, I Don't Care How"

The jargon: "Define an interface for creating objects, but let subclasses decide which class to instantiate."

The simple version: A function that creates the right thing based on what you ask for.

def create_notification(type, message):
    if type == "email":
        return EmailNotification(message)
    elif type == "sms":
        return SmsNotification(message)
    elif type == "push":
        return PushNotification(message)

# Usage — the caller doesn't know or care about the specific class
notification = create_notification("email", "Your order shipped")
notification.send()

Factories are useful when the creation logic is complex or when the caller shouldn't need to know which specific type it's getting. If creation is simple, just use the constructor directly — don't add a factory for the sake of having a factory.

Decorator: "Same Thing, But Also..."

The jargon: "Attach additional responsibilities to an object dynamically."

The simple version: Wrap a thing to add behavior without changing the original.

# Python decorators are literally this pattern
@require_auth
@log_request
@rate_limit
def get_user(user_id):
    return db.find_user(user_id)

Each decorator wraps the function to add behavior (authentication check, logging, rate limiting) without modifying the original function. Express middleware, .NET middleware, and Python decorators are all the same pattern.

Singleton: "There Can Be Only One"

The jargon: "Ensure a class has only one instance and provide a global point of access."

The simple version: A thing that exists once and everything shares it.

Database connection pools, configuration objects, logging instances — things where having multiple copies would be wasteful or cause conflicts.

The warning: Singleton is the most over-applied pattern. It introduces global state, makes testing harder, and creates hidden dependencies. In most modern frameworks, dependency injection handles the "only one instance" requirement more cleanly than explicit singletons.

Patterns Worth Knowing

Repository: Data Access in One Place

All database operations for a given entity live behind one interface. The rest of the code asks the repository for data; it doesn't know whether the data comes from a database, an API, or a file.

public interface IUserRepository
{
    Task<User?> GetById(int id);
    Task<List<User>> GetActive();
    Task Save(User user);
}

The benefit: if you change your database, you change the repository implementation. The rest of the application doesn't know or care.

Middleware/Pipeline: Process in Steps

A request passes through a series of steps, each adding behavior. Authentication, logging, error handling, compression — each is a step in the pipeline.

Request → Auth → Logging → Rate Limit → Your Handler → Response

ASP.NET Core middleware, Express middleware, and Django middleware all implement this pattern. Each step can modify the request, modify the response, or short-circuit the pipeline.

Adapter: Make Incompatible Things Work Together

You need to use a library that has a different interface than your code expects. Instead of changing your code or the library, you write a thin wrapper that translates between them.

# Your code expects this interface
class PaymentProcessor:
    def charge(self, amount, currency):
        pass

# The Stripe SDK has a different interface
# Adapter bridges the gap
class StripeAdapter(PaymentProcessor):
    def __init__(self):
        self.stripe = stripe.Client()

    def charge(self, amount, currency):
        return self.stripe.charges.create(
            amount=int(amount * 100),
            currency=currency
        )

If you've ever written a wrapper around a third-party library, you've written an adapter.

When NOT to Use Patterns

When the code is simple. A function that does one thing doesn't need a Strategy Pattern. A class with one instance doesn't need to be a formal Singleton. Don't add architecture to code that isn't complex enough to need it.

When you're anticipating future requirements. "We might need different notification types someday" isn't a reason to add a Factory today. Build for what you need now. Add the pattern when the complexity arrives.

When the pattern makes the code harder to read. If adding a pattern requires three new files, two interfaces, and a factory method for something that was previously a straightforward function call — the pattern is making things worse, not better.

The test: Would a new developer reading this code understand it faster with the pattern or without it? If without, the pattern isn't earning its place.

The Real Value of Patterns

Patterns aren't recipes. They're vocabulary. When a senior developer says "this is an Observer" or "we could use a Strategy here," they're communicating a structural idea in two words instead of a five-minute explanation.

You don't need to memorize all 23 Gang of Four patterns. You need to recognize the 6-8 patterns that appear regularly in modern code, understand when they help and when they don't, and use the vocabulary to communicate with your team.

Key Takeaway

Most design patterns are names for things you already do: Strategy is passing a function, Observer is event listeners, Factory is a creation function, Decorator is middleware, Singleton is a shared instance. Use patterns when they reduce complexity and improve communication. Don't use them when they add ceremony to simple code or when you're anticipating requirements that don't exist yet. Patterns are vocabulary, not architecture.

Comments


bottom of page