
Post #8 built bubble_sort, merge_sort, and quicksort as separate functions — imagine instead a single Sorter object that could switch between them at runtime. Post #14 mentioned a database’s connection handling without addressing how a system ensures only one shared connection pool exists rather than dozens of competing ones. These are not new problems — they are two of the most well-known, named design patterns in software engineering, and this post covers them directly, using this series’ own already-familiar data structures and algorithms as the concrete examples rather than unfamiliar, generic ones.
What a Design Pattern Actually Is
A design pattern is not code you copy — it is a named, reusable shape of a solution to a recurring software design problem, a shared vocabulary letting developers say “this is a Factory” or “that’s an Observer” and immediately communicate an entire structural idea without re-explaining it from scratch. This post covers several of the most common, genuinely useful ones, connecting each directly to concepts already covered across this series.
Singleton: Exactly One Instance, Guaranteed
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
print("Creating the one and only connection pool")
return cls._instance
conn1 = DatabaseConnection()
conn2 = DatabaseConnection()
print(conn1 is conn2) # True — genuinely the same object
The Singleton pattern ensures a class has exactly one instance, globally accessible — directly relevant to Post #14’s database coverage: a real application typically wants exactly one shared connection pool, not a new, separately-managed pool created every time database access is needed, since managing many competing pools would waste resources and risk inconsistent state.
Factory: Centralizing Object Creation
class HashIndex:
def lookup(self, key): return f"O(1) average lookup for {key}"
class BTreeIndex:
def lookup(self, key): return f"O(log n) lookup for {key}, supports range queries"
def create_index(index_type: str):
"""Centralizes the decision of WHICH index class to instantiate."""
if index_type == "hash":
return HashIndex()
elif index_type == "btree":
return BTreeIndex()
raise ValueError(f"Unknown index type: {index_type}")
index = create_index("btree")
Directly modeling Post #14’s actual index-type decision — the calling code asks for “the appropriate index for this situation” without needing to know or directly reference the specific underlying class (Post #4’s hash table or Post #5’s B-tree), with that decision centralized in one place rather than scattered throughout the codebase.
Observer: One Event, Many Reactions
class DataUpdateEvent:
def __init__(self):
self._subscribers = []
def subscribe(self, callback):
self._subscribers.append(callback)
def notify(self, data):
for callback in self._subscribers:
callback(data)
def log_update(data):
print(f"Logged: {data}")
def invalidate_cache(data):
print(f"Cache invalidated for: {data}")
event = DataUpdateEvent()
event.subscribe(log_update)
event.subscribe(invalidate_cache)
event.notify("user_id=42 updated")
This is precisely the mechanism underlying Post #17’s cache invalidation problem, mentioned directly there as a genuinely hard issue — an Observer pattern is exactly how a real system notifies its cache layer (and logging, and anything else that needs to react) the moment underlying data changes, without the code that made the change needing to know every specific thing that should happen as a result.
Strategy: Swappable Algorithms at Runtime
class Sorter:
def __init__(self, strategy):
self.strategy = strategy # any of Post #8's sorting functions
def sort(self, data):
return self.strategy(data)
# Using Post #8's actual functions directly as interchangeable strategies
small_dataset_sorter = Sorter(strategy=insertion_sort) # good for small/nearly-sorted data
large_dataset_sorter = Sorter(strategy=merge_sort) # guaranteed O(n log n)
print(small_dataset_sorter.sort([5, 2, 8, 1]))
This is exactly the Sorter object imagined in this post’s opening — Post #8’s sorting functions were always, structurally, interchangeable strategies; naming this pattern makes explicit what was already true: any of those functions could be selected and swapped at runtime, based on Post #8’s own guidance (small/nearly-sorted data favors insertion sort; general-purpose reliability favors merge sort), without the calling code needing to change at all.
Decorator: Wrapping to Add Behavior
This is the general software pattern — distinct from, though closely related in spirit to, the language-specific @decorator syntax this blog’s Python series covered directly.
class BasicQuery:
def execute(self):
return "SELECT * FROM tasks"
class LoggedQuery:
def __init__(self, wrapped_query):
self.wrapped_query = wrapped_query
def execute(self):
result = self.wrapped_query.execute()
print(f"Executing: {result}")
return result
query = LoggedQuery(BasicQuery())
query.execute() # logs, then returns the same result the wrapped query would have
The Decorator pattern wraps an object to add behavior around it, without modifying the original object’s own code — genuinely the same underlying idea as Python’s @decorator syntax (a function wrapping another function), generalized here to wrapping any object, not specifically functions.
Adapter: Making Incompatible Interfaces Work Together
class HashTableStorage:
def get_value(self, key):
return f"hash lookup for {key}"
class LegacyArrayStorage:
def fetch(self, index):
return f"array lookup at index {index}"
class ArrayToHashAdapter:
"""Makes LegacyArrayStorage usable anywhere a hash-table-style interface is expected."""
def __init__(self, array_storage):
self.array_storage = array_storage
def get_value(self, key):
return self.array_storage.fetch(int(key)) # translates the interface
adapted = ArrayToHashAdapter(LegacyArrayStorage())
print(adapted.get_value("3"))
The Adapter pattern lets two components with genuinely incompatible interfaces work together, without modifying either one — directly relevant anywhere Post #3’s array-based storage and Post #4’s hash-based storage need to be used interchangeably by code expecting a consistent interface.
When Patterns Become Over-Engineering
A genuinely important, direct caveat: applying a named pattern because it exists, rather than because it solves the actual problem at hand more clearly than simpler code would, is a real and common mistake. A single, unlikely-to-grow object almost certainly does not need a full Factory; a value that will genuinely never have multiple interested listeners does not need a full Observer. Patterns earn their place specifically when the problem they solve is genuinely present — Post #17’s caching-invalidation problem genuinely needing Observer-style notification, Post #8’s genuinely swappable sorting algorithms genuinely needing Strategy — not as a demonstration of familiarity with pattern names.
Real-World Use Cases
Singleton: Shared, expensive-to-create resources — database connection pools (directly connecting to Post #14), application-wide configuration.
Factory: Any situation where object creation involves genuine decision logic — Post #14’s index type selection, or choosing between different service implementations based on configuration.
Observer: Event-driven systems broadly — cache invalidation (Post #17), UI updates in response to data changes, logging and monitoring systems reacting to application events.
Strategy: Any situation with genuinely interchangeable algorithms for the same task — Post #8’s sorting functions, different compression algorithms, different payment processing methods.
Decorator: Adding cross-cutting behavior (logging, caching, access control) around existing functionality without modifying its core logic.
Adapter: Integrating legacy systems or third-party libraries with interfaces that don’t naturally match your application’s expected interface.
Common Mistakes and Gotchas
⚠️ Mistake 1: Applying a pattern because it’s “proper,” not because it solves a genuine problem Covered directly above — the single most common design pattern mistake, adding real structural complexity without a corresponding, genuine benefit.
⚠️ Mistake 2: Using Singleton for something that should genuinely have multiple instances Not every shared resource needs to be a true singleton — overusing this pattern can create hidden, hard-to-test global state where genuinely independent instances would have been simpler and safer.
⚠️ Mistake 3: Building an Observer system with tightly coupled subscribers An observer that reaches back and directly modifies the object that triggered the notification defeats the pattern’s entire decoupling purpose — observers should react to the data they’re given, independently.
⚠️ Mistake 4: Confusing the Decorator pattern with language-specific decorator syntax
Covered directly above — Python’s @decorator syntax is one specific, syntactic implementation of the broader Decorator pattern’s underlying idea; the general pattern, illustrated in this post, applies to any language and any kind of object wrapping, not only functions.
Quick Reference
| Pattern | Solves | Connects to This Series |
|---|---|---|
| Singleton | Ensure exactly one instance | Post #14’s connection pooling |
| Factory | Centralize object creation logic | Post #14’s index type selection |
| Observer | One event, many independent reactions | Post #17’s cache invalidation |
| Strategy | Swap algorithms at runtime | Post #8’s interchangeable sort functions |
| Decorator | Add behavior without modifying original code | General wrapping, related to Python’s @decorator |
| Adapter | Make incompatible interfaces work together | Post #3/#4’s storage interface differences |
Exercises
Exercise 1 — Direct application
Implement a SearchStrategy class following this post’s Strategy pattern, wrapping Post #9’s linear_search and binary_search as interchangeable strategies, selected based on whether the input data is sorted.
Exercise 2 — Slight variation
Extend this post’s DataUpdateEvent Observer example with a third subscriber that only reacts when the update data contains a specific keyword, demonstrating that observers can apply their own independent filtering logic.
Exercise 3 — Real-world combination
Design (in comments, or working code) a Factory that creates the appropriate data structure — a SimpleHashTable (Post #4) or a BSTNode-based structure (Post #5) — based on whether the calling code specifies it needs sorted-order traversal support.
Exercise 4 — Open-ended challenge Identify one place in code you have written earlier in this series (or in any other project) where you were, without naming it, already using one of this post’s patterns — write a short explanation of which pattern it was and why that shape emerged naturally.
FAQ
Q: Do I need to memorize every named design pattern in the broader literature? A: No — most working developers recognize a handful of common patterns (the ones covered in this post are among the most frequently used) and pick up others by name recognition as needed; understanding the underlying problems patterns solve matters considerably more than memorizing an exhaustive catalog.
Q: How do I know if I’m over-engineering by applying a pattern? A: A useful test, covered directly in this post: can you explain, concretely, what specific future flexibility or clarity the pattern buys you for your actual problem, beyond “it’s the named, proper way to do it”? If the honest answer is “not really,” simpler code is very likely the better choice.
Q: Are these patterns specific to object-oriented programming? A: Most classical design patterns, including every one covered in this post, originated in an object-oriented context — though the underlying problems they solve (swappable behavior, centralized creation, decoupled notification) appear across virtually every programming paradigm, sometimes expressed through genuinely different, non-OOP mechanisms.
Q: Is there a “correct” pattern for every software design problem? A: No — many genuine problems are best solved with straightforward code, no named pattern at all; patterns are a vocabulary for recognizing and communicating recurring shapes, not a requirement that every problem must map onto one.
Summary and Next Steps
You now have names and concrete, already-familiar examples for six of the most common design patterns in software engineering — Singleton, Factory, Observer, Strategy, Decorator, and Adapter — each directly connected to a problem this series has already covered concretely (connection pooling, index selection, cache invalidation, swappable sorting algorithms). The genuinely important caveat, worth carrying forward: a pattern earns its place by solving a real problem more clearly than the alternative, not by demonstrating familiarity with its name.
Your next step: Complete Exercise 4 — identifying a pattern you were already using, unnamed, in your own past code — since recognizing a pattern in work you’ve already done is what turns this post’s vocabulary from abstract naming into genuine, applied recognition.
Last updated: August 2026.



