5 Design Patterns Every Developer Should Master
🏗️ 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
Pattern | Category | Problem it Solves | Analogy |
---|---|---|---|
Factory | Creational | Centralize object creation | Coffee order |
Singleton | Creational | Ensure only one instance | Printer spooler |
Observer | Behavioral | One-to-many notifications | Twitter followers |
Strategy | Behavioral | Swap behaviors at runtime | Payment methods |
Decorator | Structural | Add features without subclassing | Pizza 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.