As a developer with over a decade of experience, I’ve seen design patterns misused more often than applied correctly. They’re not magical incantations that automatically make code better. Instead, they’re well-documented solutions to problems that emerge as systems grow in complexity. The key is recognizing when you actually have one of those problems.
I remember early in my career when I thought using patterns everywhere made me a better programmer. I’d force a Factory pattern even when simple constructor calls would suffice. The result was over-engineered code that was harder to maintain than what I started with. Patterns should serve your code, not the other way around.
The Factory Method pattern solves a specific problem: when object creation logic becomes too complex or when you need to defer instantiation decisions. I’ve found it particularly valuable in plugin systems where I don’t know the exact types I’ll be working with until runtime.
Here’s a more practical example from a recent project where we needed to handle different database types:
class DatabaseConnectionFactory:
def create_connection(self, config):
db_type = config.get('type')
if db_type == 'postgresql':
return PostgresConnection(config)
elif db_type == 'mysql':
return MySQLConnection(config)
elif db_type == 'sqlite':
return SQLiteConnection(config)
else:
raise ValueError(f"Unsupported database type: {db_type}")
# Usage becomes clean and centralized
factory = DatabaseConnectionFactory()
connection = factory.create_connection({'type': 'postgresql', 'host': 'localhost'})
This approach meant that when we added MongoDB support six months later, we only had to modify the factory instead of searching through hundreds of files for database instantiation logic.
The Observer pattern has saved me countless hours when building reactive systems. I once worked on a trading application where price changes needed to update multiple UI components, trigger alerts, and log activity simultaneously. Trying to manage these dependencies with direct method calls would have created a maintenance nightmare.
Here’s a TypeScript implementation from that project:
interface PriceObserver {
onPriceChange(symbol: string, price: number): void;
}
class PriceSubject {
private observers: PriceObserver[] = [];
private currentPrices: Map<string, number> = new Map();
addObserver(observer: PriceObserver): void {
this.observers.push(observer);
}
setPrice(symbol: string, price: number): void {
this.currentPrices.set(symbol, price);
this.notifyObservers(symbol, price);
}
private notifyObservers(symbol: string, price: number): void {
this.observers.forEach(observer => {
observer.onPriceChange(symbol, price);
});
}
}
class PortfolioUI implements PriceObserver {
onPriceChange(symbol: string, price: number): void {
this.updatePortfolioValue(symbol, price);
}
private updatePortfolioValue(symbol: string, price: number): void {
// Update UI logic
console.log(`UI updated for ${symbol}: $${price}`);
}
}
class RiskManager implements PriceObserver {
onPriceChange(symbol: string, price: number): void {
this.checkRiskThresholds(symbol, price);
}
private checkRiskThresholds(symbol: string, price: number): void {
// Risk calculation logic
console.log(`Risk checked for ${symbol} at $${price}`);
}
}
What made this powerful was the loose coupling. The PriceSubject didn’t need to know anything about its observers. When we needed to add a new notification system six months into the project, we simply created a new observer class and registered it.
The Strategy pattern has become one of my most frequently used patterns, especially when dealing with business rules that vary by context. I recently built a shipping cost calculator that needed different pricing strategies based on customer type, location, and service level.
Here’s a Java example from that system:
public interface ShippingStrategy {
double calculateCost(Order order);
}
public class StandardShipping implements ShippingStrategy {
@Override
public double calculateCost(Order order) {
return order.getWeight() * 0.5 + 5.0;
}
}
public class ExpressShipping implements ShippingStrategy {
@Override
public double calculateCost(Order order) {
return order.getWeight() * 1.2 + 15.0;
}
}
public class InternationalShipping implements ShippingStrategy {
@Override
public double calculateCost(Order order) {
double baseCost = order.getWeight() * 2.5 + 25.0;
return baseCost + calculateCustomsFee(order);
}
private double calculateCustomsFee(Order order) {
// Complex international fee calculation
return order.getValue() * 0.1;
}
}
public class ShippingCalculator {
private ShippingStrategy strategy;
public void setStrategy(ShippingStrategy strategy) {
this.strategy = strategy;
}
public double calculateShipping(Order order) {
if (strategy == null) {
throw new IllegalStateException("Shipping strategy not set");
}
return strategy.calculateCost(order);
}
}
The beauty of this approach revealed itself when we needed to add expedited international shipping. I created a new strategy class that combined elements of both express and international shipping, without touching any of the existing logic.
One of the most important lessons I’ve learned is that patterns have costs. Each abstraction layer adds complexity and can impact performance. In performance-critical sections, I often measure the overhead of pattern implementations.
I once worked on a high-frequency trading system where we initially used a Strategy pattern for price calculations. Performance profiling showed the virtual method calls were adding measurable latency. We ended up using a simpler function-based approach with compile-time strategy selection in that specific component.
Language capabilities significantly influence how I implement patterns. In Python, I might use first-class functions instead of a formal Strategy pattern. In JavaScript, I often use objects and closures rather than classical class hierarchies.
Here’s how the Strategy pattern might look in modern JavaScript:
const compressionStrategies = {
zip: (data) => `zip:${data}`,
gzip: (data) => `gzip:${data}`,
brotli: (data) => `brotli:${data}`
};
class FileProcessor {
constructor() {
this.strategy = compressionStrategies.zip;
}
setStrategy(strategyName) {
if (!compressionStrategies[strategyName]) {
throw new Error(`Unknown strategy: ${strategyName}`);
}
this.strategy = compressionStrategies[strategyName];
}
processFile(data) {
return this.strategy(data);
}
}
This approach feels more natural in JavaScript and leverages the language’s strengths with functions and objects.
The real power emerges when patterns work together. I recently architected a system where Factories created Observers that used Strategies. This sounds complex, but each pattern solved a distinct problem in a cohesive way.
Consider a notification system where different events require different delivery strategies:
class NotificationFactory:
def create_notification(self, event_type, user):
if event_type == "welcome":
return WelcomeNotification(user)
elif event_type == "payment":
return PaymentNotification(user)
# ... other notification types
class NotificationStrategy:
def send(self, notification):
pass
class EmailStrategy(NotificationStrategy):
def send(self, notification):
# Email sending logic
pass
class SMSStrategy(NotificationStrategy):
def send(self, notification):
# SMS sending logic
pass
class PushStrategy(NotificationStrategy):
def send(self, notification):
# Push notification logic
pass
class NotificationManager:
def __init__(self):
self.strategies = []
def add_strategy(self, strategy):
self.strategies.append(strategy)
def notify(self, event_type, user, message):
factory = NotificationFactory()
notification = factory.create_notification(event_type, user)
for strategy in self.strategies:
strategy.send(notification)
This architecture allowed us to easily add new notification types and delivery methods without disrupting existing functionality.
Documentation is crucial when using patterns. I make sure to include comments explaining why a particular pattern was chosen and what problem it solves. This helps new team members understand the design intent rather than just seeing the implementation.
I’ve found that the best time to introduce patterns is during refactoring, when the need becomes obvious. If I notice multiple conditional statements checking the same conditions, or if I find myself constantly modifying the same class for new features, those are signals that a pattern might help.
Testing patterns requires careful consideration. The Dependency Inversion Principle inherent in many patterns makes testing easier through dependency injection and mocking. However, I’ve seen teams struggle with testing overly abstracted code where the relationships between components become unclear.
Here’s how I might test the Strategy pattern example:
@Test
public void testShippingStrategies() {
Order testOrder = new Order(2.0, 50.0); // 2kg, $50 value
ShippingCalculator calculator = new ShippingCalculator();
calculator.setStrategy(new StandardShipping());
assertEquals(6.0, calculator.calculateShipping(testOrder), 0.01);
calculator.setStrategy(new ExpressShipping());
assertEquals(17.4, calculator.calculateShipping(testOrder), 0.01);
}
The table below summarizes when I typically reach for certain patterns:
Pattern | When I Use It | What Problem It Solves |
---|---|---|
Factory Method | Object creation logic becomes complex | Centralizes creation logic, supports dependency injection |
Observer | Multiple components need to react to state changes | Decouples event producers from consumers |
Strategy | Algorithms need to vary at runtime | Isolates algorithm implementation from usage |
Decorator | Need to add responsibilities dynamically | Provides flexible alternative to subclassing |
Singleton | Exactly one instance needed globally | Controls access to shared resources |
As programming paradigms evolve, so do patterns. Functional programming has introduced new ways of thinking about old problems. I’ve started using more function composition and fewer classical patterns in my JavaScript and Python work.
The most valuable aspect of patterns isn’t the code itself—it’s the shared vocabulary they provide. When I say “let’s use a Repository pattern here,” my team immediately understands the structure and benefits without lengthy explanations.
I’ve learned to resist the temptation to show off pattern knowledge. The simplest solution that works is usually the best. If a pattern doesn’t clearly solve a current or immediately foreseeable problem, I avoid it. The goal is maintainable, understandable code—not pattern collection.
The true test of a pattern’s value comes months or years later, when requirements change. Good patterns make changes easier; bad patterns make them harder. I judge my pattern choices by how they stand up to real-world evolution rather than how elegant they look initially.
Patterns are tools in my toolbox, not rules to be followed blindly. Each project has unique constraints and requirements. The art lies in selecting the right tool for the job and knowing when no special tool is needed at all.