A Reliable Method to Choose the Right Design Pattern

A Reliable Method to Choose the Right Design Pattern

It was two years into my engineering career and I had just started a new job when I first heard the term design patterns. My brain immediately pictured fabric swatches and tailor’s chalk—patterns that help cut clothes into shape. Blame it on my aversion to certain writing styles or my less-than-stellar attention span in CSC 205 lectures back in university. Whatever the reason, I was painfully unaware at the time and had to familiarize myself with the topic fast.

A quick online search yielded some interesting articles which helped a little, but the real progress happened on the job. I had to not only learn about design patterns but also develop the skill of deciding which pattern worked best for which problem.

This article breaks down what design patterns are, the categories, and how I choose the right one to solve real problems. My goal? To give you a practical lens for your next coding challenge—so when it’s time to decide, you won’t just guess. You’ll know.

Let's dive in! What is a design pattern? 

A design pattern is a typical solution to a commonly recurring problem.

In software design, we tend to get the same problems in slightly different forms. A design pattern provides us with a blueprint that can be used to solve a class of problems in a maintainable and consistent manner.

The benefit of design patterns is that they have been tried, tested and documented by other engineers. Which means that you can focus your energy on other aspects of your system without having to figure out from scratch a problem that has been solved before.

Design patterns generally fall into three categories: Creational, Structural and Behavioral.

  • Creational design patterns help to solve problems that involve creating objects (classes, methods, etc) in a flexible manner i.e at runtime.

    Creational Pattern Example - Factory Method

# Selecting a payment processor 
class PaymentProcessor:
    def pay(self, amount):
        raise NotImplementedError()

class PayPalProcessor(PaymentProcessor):
    def pay(self, amount):
        print(f"Paid {amount} with PayPal")

class StripeProcessor(PaymentProcessor):
    def pay(self, amount):
        print(f"Paid {amount} with Stripe")

class PaymentFactory:
    @staticmethod
    def get_processor(method):
        if method == "paypal":
            return PayPalProcessor()
        elif method == "stripe":
            return StripeProcessor()
        else:
            raise ValueError("Unknown payment method")


processor = PaymentFactory.get_processor("paypal")
processor.pay(100)

 

  • Structural design patterns help to solve problems that involve composing multiple objects into larger, more complex structures.
    Structural Pattern Example – Adapter method

# Adapting an old logging library to a new interface
class OldLogger:
    def log_message(self, msg):
        print(f"[OLD LOG] {msg}")

class LoggerAdapter:
    def __init__(self, old_logger):
        self.old_logger = old_logger

    def log(self, message):  # new interface
        self.old_logger.log_message(message)

# Client code
old_logger = OldLogger()
logger = LoggerAdapter(old_logger)
logger.log("Adapter makes old code usable!")

 

  • Behavioural design patterns help to solve problems of communication and responsibility sharing between objects.

    Behavioral Pattern Example – Command Pattern

class Command:
    def execute(self):
        pass

class SayHello(Command):
    def execute(self):
        print("Hello, World!")

class SayGoodbye(Command):
    def execute(self):
        print("Goodbye, World!")

class Greeting:
    def __init__(self):
        self.commands = {}
    def register_command(self, name, command):
        self.commands[name] = command
    def greet(self, name):
        if name in self.commands:
            self.commands[name].execute()
        else:
            print("No such command registered.")

greeting = Greeting()
greeting.register_command("hello", SayHello())
greeting.register_command("goodbye", SayGoodbye())
greeting.greet("hello")

 

The first step in choosing a design pattern is to understand the problem at hand

You have to think, what exactly is the problem I am trying to solve here, in the coding sense of it?

Imagine, for instance, that you are building an app to remotely control your home automation system. The remote control has several buttons that each perform different tasks such as: turning lights on, opening the garage door, playing music, etc. What kind of problem is that? Is it about deciding what object to use to perform a task? Is it about creating one giant object made up of many smaller objects? Or is it about how different parts of the system communicate with each other?

Let’s dig deeper. What kind of design problem is this? Let’s test it against the three categories of patterns.

First, is this a creational problem? Creational design patterns solve the problem of flexible creation of objects. The problem in this case is not how to create the devices, we are struggling with triggering actions on them. So, this is not a creational problem.

Next, we need to ask if this problem can be solved by having one giant complex object that has different smaller objects. If our remote control needs to assemble several devices into one object or if the different buttons need to share internal connections, this would be a good fit but each button is independent in this case, there is no composition of objects hence, this is not a problem for the structural design pattern.

Finally, could this be a problem of communication between objects and assigning responsibilities? The remote buttons need a way to request that a certain home device perform an action, without knowing the details of how that action is performed. We are looking for a pattern that separates the action of the button “invoking a request” from the action of “executing the request” so that they can be generally independent. This seems to be a problem of communication between objects which is definitely a behavioural pattern problem.

Now, let us look for a specific behavioural pattern that best solves this problem

There are lots of behavioural patterns which lead us to the next challenge. How to know which behavioural design pattern is best suited for this problem?

Let’s restate our remote control problem. We want our remote control button to send a request to a device which would execute the request. For the device to successfully execute the request, the request has to contain all necessary information.

Of all the different behavioural patterns, the command pattern stands out for being the closest in definition to this problem.

The command pattern allows us to turn a request into a stand-alone object that contains all the information needed to perform an action. This object can be passed along between two different objects or queued for later execution, or used to trigger an event. This standalone object is called a command.

And there we have it, the right pattern for solving the problem! Of course, not every design problem is this easy to classify, but thinking through the categories and matching the problem to each pattern’s definition is a reliable way to make the right choice.

Now that we’ve found the right pattern, let’s have a look at a simple implementation of the command pattern for our home remote control app.

from abc import ABC, abstractmethod

# Command Interface – Defines the execute() method that all commands must implement.
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass


# Receiver – The actual device that performs the work (Light, Blinds, etc.).
class Light:
    def on(self):
        print("Light is ON")

    def off(self):
        print("Light is OFF")


class Blinds:
    def close(self):
        print("Blinds are CLOSED")

    def open(self):
        print("Blinds are OPEN")


# Concrete Commands – Implement specific actions (turn lights on, close blinds).
class LightOnCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        self.light.on()


class BlindsCloseCommand(Command):
    def __init__(self, blinds):
        self.blinds = blinds

    def execute(self):
        self.blinds.close()


# Invoker – The remote control that triggers the commands.
class RemoteControl:
    def __init__(self):
        self.buttons = {}

    def set_command(self, button_name, command):
        self.buttons[button_name] = command

    def press(self, button_name):
        if button_name in self.buttons:
            self.buttons[button_name].execute()
        else:
            print("No command assigned to this button.")


# Client – The setup code that binds buttons to commands.
if __name__ == "__main__":
    # Receivers
    living_room_light = Light()
    blinds = Blinds()

    # Commands
    light_on = LightOnCommand(living_room_light)
    blinds_close = BlindsCloseCommand(blinds)

    # Remote setup
    remote = RemoteControl()
    remote.set_command("A", light_on)
    remote.set_command("B", blinds_close)

    # Press buttons
    remote.press("A")  # Output: Light is ON
    remote.press("B")  # Output: Blinds are CLOSED

Choosing the right behavioral pattern isn’t about memorizing definitions—it’s about understanding what each one unlocks. Once you recognize the shape of the problem, the right solution starts to reveal itself. The more you lean into that mindset, the more intuitive these decisions become.

Want to learn more?

If you’d like to explore design patterns in more detail, here are some excellent resources: