Back to Blog

5 Design Patterns Every Developer Should Master

January 18, 2025Bobby Jose5 min read
Design Patterns
Software Architecture
C#
Python
Best Practices

🏗️ Design patterns can sound academic or intimidating, but at their core, they're just battle-tested solutions to common coding problems.

Here are the 5 most important patterns you'll see everywhere — from frameworks you use daily to the apps you write yourself.

1. Factory Method ☕

What it solves: You need to create objects without hardcoding their classes.

Analogy: Ordering coffee — you just ask for a Latte or Cappuccino, and the barista (factory) makes it.

C# Example

public interface ICoffee
{
    void Serve();
}

public class Latte : ICoffee
{
    public void Serve() => Console.WriteLine("Serving Latte");
}

public class Cappuccino : ICoffee
{
    public void Serve() => Console.WriteLine("Serving Cappuccino");
}

public class CoffeeFactory
{
    public static ICoffee Create(string type)
    {
        return type switch
        {
            "latte" => new Latte(),
            "cappuccino" => new Cappuccino(),
            _ => throw new ArgumentException("Unknown type")
        };
    }
}

// Usage
var coffee = CoffeeFactory.Create("latte");
coffee.Serve();

Python Example

class Coffee:
    def serve(self): raise NotImplementedError

class Latte(Coffee):
    def serve(self): print("Serving Latte")

class Cappuccino(Coffee):
    def serve(self): print("Serving Cappuccino")

class CoffeeFactory:
    @staticmethod
    def create(type: str) -> Coffee:
        if type == "latte": return Latte()
        elif type == "cappuccino": return Cappuccino()
        else: raise ValueError("Unknown type")

coffee = CoffeeFactory.create("latte")
coffee.serve()

2. Singleton 🔑

What it solves: You want one and only one instance of a class across your app.

Analogy: A printer spooler — no matter how many apps send jobs, there's only one spooler coordinating everything.

C# Example

public class Logger
{
    private static Logger? _instance;
    private static readonly object _lock = new();

    private Logger() { }

    public static Logger Instance
    {
        get
        {
            lock (_lock)
            {
                return _instance ??= new Logger();
            }
        }
    }

    public void Log(string msg) => Console.WriteLine(msg);
}

// Usage
Logger.Instance.Log("App started");

Python Example

class Logger:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def log(self, msg):
        print(msg)

# Usage
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2)  # True
logger1.log("App started")

3. Observer 🔔

What it solves: One object changes → many others need to be updated automatically.

Analogy: Twitter — when someone tweets, all followers get notified.

C# Example

public interface IObserver
{
    void Update(string msg);
}

public class Follower : IObserver
{
    private string _name;
    public Follower(string name) => _name = name;
    public void Update(string msg) => Console.WriteLine($"{_name} got: {msg}");
}

public class TwitterAccount
{
    private List<IObserver> _followers = new();

    public void Subscribe(IObserver follower) => _followers.Add(follower);
    public void Tweet(string msg)
    {
        foreach (var f in _followers) f.Update(msg);
    }
}

// Usage
var account = new TwitterAccount();
account.Subscribe(new Follower("Alice"));
account.Subscribe(new Follower("Bob"));

account.Tweet("Hello world!");

Python Example

class Follower:
    def __init__(self, name): self.name = name
    def update(self, msg): print(f"{self.name} got: {msg}")

class TwitterAccount:
    def __init__(self): self._followers = []
    def subscribe(self, f): self._followers.append(f)
    def tweet(self, msg):
        for f in self._followers: f.update(msg)

account = TwitterAccount()
account.subscribe(Follower("Alice"))
account.subscribe(Follower("Bob"))

account.tweet("Hello world!")

4. Strategy 🔄

What it solves: You want to swap algorithms or behaviors at runtime without rewriting code.

Analogy: Paying at checkout — cash, card, or Apple Pay. The checkout process is the same, but the strategy differs.

C# Example

public interface IPaymentStrategy
{
    void Pay(decimal amount);
}

public class CreditCard : IPaymentStrategy
{
    public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} with Credit Card");
}

public class PayPal : IPaymentStrategy
{
    public void Pay(decimal amount) => Console.WriteLine($"Paid {amount} with PayPal");
}

public class Checkout
{
    private IPaymentStrategy _strategy;
    public Checkout(IPaymentStrategy strategy) => _strategy = strategy;
    public void SetStrategy(IPaymentStrategy strategy) => _strategy = strategy;
    public void Process(decimal amount) => _strategy.Pay(amount);
}

// Usage
var checkout = new Checkout(new CreditCard());
checkout.Process(50);
checkout.SetStrategy(new PayPal());
checkout.Process(75);

Python Example

class PaymentStrategy:
    def pay(self, amount): raise NotImplementedError

class CreditCard(PaymentStrategy):
    def pay(self, amount): print(f"Paid {amount} with Credit Card")

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

class Checkout:
    def __init__(self, strategy: PaymentStrategy):
        self._strategy = strategy
    def set_strategy(self, strategy: PaymentStrategy):
        self._strategy = strategy
    def process(self, amount):
        self._strategy.pay(amount)

checkout = Checkout(CreditCard())
checkout.process(50)
checkout.set_strategy(PayPal())
checkout.process(75)

5. Decorator 🍕

What it solves: Add new functionality to objects without modifying their class.

Analogy: Pizza — start with plain, then add cheese, mushrooms, or pepperoni.

C# Example

public interface IPizza
{
    string GetDescription();
    decimal GetCost();
}

public class PlainPizza : IPizza
{
    public string GetDescription() => "Plain Pizza";
    public decimal GetCost() => 5;
}

public class CheeseDecorator : IPizza
{
    private readonly IPizza _pizza;
    public CheeseDecorator(IPizza pizza) => _pizza = pizza;
    public string GetDescription() => _pizza.GetDescription() + ", Cheese";
    public decimal GetCost() => _pizza.GetCost() + 2;
}

// Usage
IPizza pizza = new PlainPizza();
pizza = new CheeseDecorator(pizza);
Console.WriteLine($"{pizza.GetDescription()} costs {pizza.GetCost()}");

Python Example

class Pizza:
    def get_description(self): raise NotImplementedError
    def get_cost(self): raise NotImplementedError

class PlainPizza(Pizza):
    def get_description(self): return "Plain Pizza"
    def get_cost(self): return 5

class CheeseDecorator(Pizza):
    def __init__(self, pizza): self._pizza = pizza
    def get_description(self): return self._pizza.get_description() + ", Cheese"
    def get_cost(self): return self._pizza.get_cost() + 2

pizza = PlainPizza()
pizza = CheeseDecorator(pizza)
print(f"{pizza.get_description()} costs {pizza.get_cost()}")

📝 Recap

PatternCategoryProblem it SolvesAnalogy
FactoryCreationalCentralize object creationCoffee order
SingletonCreationalEnsure only one instancePrinter spooler
ObserverBehavioralOne-to-many notificationsTwitter followers
StrategyBehavioralSwap behaviors at runtimePayment methods
DecoratorStructuralAdd features without subclassingPizza toppings

👉 If we master these 5, we'll recognize 70% of what's happening inside modern frameworks (like .NET Core, Django, React, Angular).

Think of them as the Swiss Army Knife of design patterns — small, practical, and ridiculously useful.