Why Design Patterns Matter
Every time you reuse the same solution to fix a recurring problem, you are using a pattern. Design patterns give a shared vocabulary to teams, so saying "just slap a Factory here" tells everyone exactly how objects get created without drawing UML diagrams. Rather than reinventing code, you borrow battle-tested approaches that companies like Netflix, Shopify, and GitHub already run in production.
How We Will Learn the Patterns
Instead of abstract theory, we will see one runnable example per pattern in three mainstream languages. You can paste each snippet into Node, Python, or a Java IDE and hit Run. The goal is to recognize the pattern next time you need it, tweak the skeleton, and ship.
Creational Patterns: Flexible Object Creation
Factory Pattern: Creating Objects Without New Operators
The Factory pattern detaches object instantiation from the caller: clients supply hints like a string or enum; the factory returns the correct concrete class. Think of it as a vending machine for objects.
- One change in the factory file adds a new subclass, avoiding shotgun surgery in dozens of callers.
- Perfect when the exact class depends on runtime data such as user locale, payment gateway, or device type.
- Teams use it alongside dependency injection so tests can swap in a FakeFactory.
JavaScript Example – Payment SDK Factory
class StripeApi { pay(amount) { console.log(`Charged $${amount} via Stripe`);} } class PayPalApi { pay(amount) { console.log(`Approved $${amount} on PayPal`);} } class PaymentFactory { static create(type) { switch (type) { case 'stripe': return new StripeApi(); case 'paypal': return new PayPalApi(); default: throw new Error('Unknown payment provider'); } } } const sdk = PaymentFactory.create('paypal'); sdk.pay(99); // Uses the correct API
Python Example
from abc import ABC, abstractmethod class Transport(ABC): @abstractmethod def deliver(self): pass class Truck(Transport): def deliver(self): return 'Deliver by land in a box' class Ship(Transport): def deliver(self): return 'Deliver by sea in a container' class TransportFactory: @staticmethod def create(logistics_type): if logistics_type == 'road': return Truck() elif logistics_type == 'sea': return Ship() raise ValueError('Unknown transport type') service = TransportFactory.create('sea') print(service.deliver())
Structural Patterns: Bending Code Into Shape
Adapter Pattern: Making Incompatible Classes Play Nice
When third-party libraries update their interfaces overnight, the Adapter pattern acts as a translator, isolating legacy clients from churn. GitHub Actions uses adapters so the same workflow can target both Docker and Kubernetes runners.
- Wrap the external library once and your existing codebase never touches vendor APIs directly.
- Unit tests remain stable; you can mock the adapter instead of a complex SDK.
Java Example
// Existing target interface expected by the app interface MediaPlayer { void play(String filename); } // Netflix SDK you must integrate class NetflixSDK { void stream(String show) { System.out.println("Streaming " + show + " on Netflix"); } } // Adapter translates app calls to Netflix calls class NetflixAdapter implements MediaPlayer { private final NetflixSDK netflix = new NetflixSDK(); @Override public void play(String filename) { netflix.stream(filename); } } class MediaPlayerClient { public static void main(String[] args) { MediaPlayer player = new NetflixAdapter(); player.play("Stranger Things"); } }
Decorator Pattern: Composable Behavior Buckets
Ruby on Rails gems like Pundit use decorators to wrap ActiveRecord objects with authorization logic without intrusive inheritance. Decorators let you bolt features on the fly, closing the class for modification but open for extension—classic open/closed principle.
JavaScript Example—Logging Coffee Prices in Node
class Coffee { cost() { return 2; } } class MilkDecorator { constructor(coffee) { this.coffee = coffee; } cost() { return this.coffee.cost() + 0.5; } } class SugarDecorator { constructor(coffee) { this.coffee = coffee; } cost() { return this.coffee.cost() + 0.2; } } let drink = new SugarDecorator(new MilkDecorator(new Coffee())); console.log(`Total Cost: $${drink.cost()}`); // 2.7
Behavioral Patterns: Choreographing Interactions
Observer Pattern: Reactive UIs Without Callback Hell
React's state management under the hood leans on Observer. When state mutates, all subscribed components rerender. Native DOM events are another flavor of the same pattern. One subject broadcasts updates; any number of listeners react.
Python Example: Simple Event Bus
class EventBus: def __init__(self): self.subscribers = {} def subscribe(self, event, callback): self.subscribers.setdefault(event, []).append(callback) def emit(self, event, data): for callback in self.subscribers.get(event, []): callback(data) bus = EventBus() # UI watches for new comments bus.subscribe('new_comment', lambda c: print('UI refreshes:', c)) # Backend persists comment then broadcasts bus.emit('new_comment', 'Hello world!')
Strategy Pattern: Swappable Algorithms on the Fly
The Strategy pattern teaches us to compose a class with interchangeable algorithms instead of encoding them directly. Node.js's Passport.js uses strategies to allow Google, Apple, or password auth with a single interface.
- Add OAuth2Strategy next week without touching login routes.
- Dynamic A/B testing: vary the strategy for 5% of users.
Java Example – Payment Strategy
interface PaymentStrategy { void pay(int amount); } class CreditCardStrategy implements PaymentStrategy { public void pay(int amount) { System.out.println("Paid " + amount + " via Credit Card"); } } class CryptoStrategy implements PaymentStrategy { public void pay(int amount) { System.out.println("Paid " + amount + " via Bitcoin"); } } class CheckoutCart { private PaymentStrategy strategy; public void setPaymentStrategy(PaymentStrategy strategy) { this.strategy = strategy; } public void checkout(int amount) { if (strategy == null) throw new IllegalStateException("Pick a payment method"); strategy.pay(amount); } } public class Main { public static void main(String[] args) { CheckoutCart cart = new CheckoutCart(); cart.setPaymentStrategy(new CreditCardStrategy()); cart.checkout(100); } }
Iterator Pattern: Safe Traversal Under the Hood
JavaScript's `for...of` loops, Python's generators, and Java's enhanced for loops all rely on Iterators. A custom iterator keeps the traversal logic outside your business classes, upholds Single Responsibility, and hides risky internal indices.
Python Snippet – Skip-None Iterator
class SkipNoneList: def __init__(self, data): self.data = data def __iter__(self): index = 0 while index < len(self.data): item = self.data[index] if item is not None: yield item index += 1 nums = SkipNoneList([1, None, 3, None, 5]) print(list(nums)) # [1, 3, 5]
Facade Pattern: Simple APIs Over Messy Subsystems
AWS CDK wraps hundreds of CloudFormation APIs behind one-line commands like `new s3.Bucket(...)`. That is the Facade pattern. You keep the power of the underlying AWS SDK while shielding teams from 47 parameters on every call.
Template Method Pattern: Skeleton Reuse With Custom Hooks
Spring Framework's `JdbcTemplate` uses this pattern. The superclass handles connection, transaction, and exception mapping; subclasses fill in the variable SQL mapping. One invariant structure, many custom shapes.
Anti-Pattern Alert: Singleton as Global Holder
The Gang-of-Four Singleton keeps exactly one running instance. Unfortunately, many legacy codebases turn singletons into global variables, making tests brittle and parallelism hard. Today prefer dependency injection containers when you need one instance.
Tips to Recognize a Pattern in Existing Code
- Names ending in **-Adapter, -Factory, -Strategy, -Iterator** are usually self-documenting.
- If you see switch statements on type or enum, suspect a missing Strategy or Factory.
- A public getter that returns only read-only collections hints at Iterator or internal defensive copying.
Practice Project: Refactor a Monolith With Patterns
- Pick a legacy function that handles file uploads, image resizing, and database inserts inside one 300-line block.
- Extract upload logic into a Strategy interface and produce LocalFileSystemStrategy, S3Strategy, GcpBucketStrategy.
- Create an Adapter around image resizing libraries so you can swap ImageMagick for Sharp.js inside Node without touching callers.
- Finally wrap the new UploadContext class with a Factory so the CLI can choose storage via a command-line flag.
Wrap-Up
Design patterns are not magic superpowers; they are time-tested shipping shortcuts. Learn the 10 in this article—Factory, Adapter, Decorator, Observer, Strategy, Iterator, Facade, Singleton, Template Method, and Delegate—with runnable examples you can paste right now. The next time a teammate says "We need a Strategy here," you will nod instead of panic.
Disclaimer: This article was generated by an AI assistant and has been reviewed by a human editor. The code samples are minimal illustrations; adapt to your production standards. No statistics or percentage claims were made without public sources.