
Design Patterns: Proven Solutions
Singleton, Factory, Observer... Don't reinvent the wheel. Use the proven blueprints.

Singleton, Factory, Observer... Don't reinvent the wheel. Use the proven blueprints.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Establishing TCP connection is expensive. Reuse it for multiple requests.

Three years into my career, I joined a new project. Opening the codebase, I felt this strange déjà vu.
"Wait... this structure looks familiar?"The way they managed database connections, created API objects, structured event listeners—completely different company, different team, different project, yet the code patterns were eerily similar.
What's more bizarre: the C# code from a C# tutorial I studied earlier and this JavaScript code looked structurally identical despite different languages.
I asked a senior developer:
"Is this code structure intentional?"
Senior: "Of course. Singleton pattern. Design pattern."
Me: "Design pattern?"
Senior: "Think of it as a cheat sheet. Battle-tested solutions from 30 years of developers solving the same problems. Languages change, but problems stay the same. Check out the GoF book."
That evening, I ordered the Gang of Four's "Design Patterns" book. When it arrived, I saw 700 pages and immediately regretted it.
Initially, I completely missed the point.
"Patterns? Can't I just write code my way? My code is the most intuitive anyway."
But reality hit hard through code reviews.
First review:// My code
let dbConnection1 = createDatabaseConnection();
let dbConnection2 = createDatabaseConnection();
let dbConnection3 = createDatabaseConnection();
Reviewer: "Database connections should be singular. Use Singleton pattern."
Me: "Singleton?" (frantically Googling)
Reviewer: "Use getInstance() method to return a single instance..."
→ 30 seconds of explanation, barely understood
Second review (one month later):Reviewer: "Use Singleton here too."
Me: "Got it!" (understood in 1 second)
That's when it clicked.Design patterns aren't about "how to write code"—they're a shared vocabulary for developers.
You can compress a 30-second explanation into one word: "Singleton." That's the magic.
Opening the GoF book, several things shocked me:
23 patterns to memorize? "Do I need to memorize all of these? This isn't a bootcamp..."
When do I use which pattern? "What's the difference between Singleton and Factory?"
Can't I just code intuitively? "Using patterns actually makes code look more complex..."
Examples are too academic "Shape, Circle, Rectangle... when will I ever draw shapes in real projects?"
The biggest question: "Do people actually use these in production?"
Book examples were shapes, animals, cars—too academic, too detached from reality.
After a month of reading theory, I revisited production code. Shock hit me.
"Wait... these are all patterns?"new Promise() → Factory patternArray.map() → Strategy patternI'd been using design patterns all along. I just didn't know their names.
A senior's analogy finally made sense:
"Design patterns are like cooking recipes.
First time making pasta:
- How much salt? Cooking time?
- Water amount? When to add oil?
Recipe says: '10g salt per 1L water, boil 8 minutes'
Same with design patterns. 'This problem? Singleton recipe. That problem? Factory recipe.'
Don't reinvent the wheel. Follow proven recipes = fewer failures."
That's what it was all about: Design patterns aren't "new techniques"—they're naming what you already do.
Knowing the names accelerates communication.
From five years of production experience, here are my top three patterns.
I built a chat app with this bug:
// Creating new DB connection on every message
function sendMessage(text) {
const db = new Database(); // New connection every time
db.insert({ message: text });
}
// User sends 100 messages...
// 100 DB connections created 💀
// Memory explosion + server crash
Testing worked fine, but with 100 concurrent users, the server died.
Why? Creating new database connection objects repeatedly filled memory until it exploded.
class Database {
static instance = null;
constructor() {
if (Database.instance) {
return Database.instance; // Return existing if present
}
Database.instance = this;
this.connection = this.createConnection();
console.log("Database connection created");
}
static getInstance() {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
createConnection() {
return { connected: true };
}
}
// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
const db3 = Database.getInstance();
console.log(db1 === db2); // true
console.log(db2 === db3); // true
// No matter how many calls, only 1 instance
// Log: "Database connection created" (only once)
class Config {
static instance = null;
constructor() {
if (Config.instance) return Config.instance;
this.settings = {
apiUrl: process.env.API_URL,
apiKey: process.env.API_KEY,
timeout: 5000
};
Config.instance = this;
}
static getInstance() {
if (!Config.instance) {
Config.instance = new Config();
}
return Config.instance;
}
get(key) {
return this.settings[key];
}
}
// Same config object everywhere
const config1 = Config.getInstance();
const config2 = Config.getInstance();
console.log(config1.get("apiUrl"));
console.log(config1 === config2); // true
2. Logger (Prevent Log File Duplication)
class Logger {
static instance = null;
constructor() {
if (Logger.instance) return Logger.instance;
this.logs = [];
Logger.instance = this;
}
log(message) {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(`[${timestamp}] ${message}`);
}
getLogs() {
return this.logs;
}
}
// Multiple modules share same Logger
const logger1 = new Logger();
logger1.log("User logged in");
const logger2 = new Logger();
logger2.log("Data fetched");
console.log(logger1.getLogs()); // Contains both logs
console.log(logger1 === logger2); // true
Singleton's downside: testing is tricky.
// ❌ Tests share state
test("first test", () => {
const db = Database.getInstance();
db.insert({ id: 1 });
expect(db.count()).toBe(1);
});
test("second test", () => {
const db = Database.getInstance(); // Same instance!
expect(db.count()).toBe(0); // ❌ Fails (previous test data remains)
});
Solution: Add reset() method
class Database {
// ...
static reset() {
Database.instance = null;
}
}
// Reset after each test
afterEach(() => {
Database.reset();
});
Building a cross-platform app, I encountered this:
// Different buttons per platform
function createButton(platform, text) {
let button;
if (platform === "ios") {
button = new IOSButton(text);
button.setStyle({ borderRadius: 10, shadow: true });
} else if (platform === "android") {
button = new AndroidButton(text);
button.setStyle({ elevation: 5, ripple: true });
} else if (platform === "web") {
button = new WebButton(text);
button.setStyle({ hover: true, transition: "0.3s" });
} else if (platform === "windows") {
button = new WindowsButton(text);
button.setStyle({ flat: true });
}
return button;
}
// Problems:
// 1. Adding new platform requires finding and modifying this function
// 2. If buttons are created in 100 places? Modify all 100
// 3. iOS button style changes? Hunt through all locations
Real experience: Adding macOS support required modifying 200 files. Took two weeks.
// Step 1: Unified button interface
class Button {
constructor(text) {
this.text = text;
}
render() {
throw new Error("Must implement render()");
}
}
class IOSButton extends Button {
render() {
return `<button class="ios">${this.text}</button>`;
}
}
class AndroidButton extends Button {
render() {
return `<button class="android">${this.text}</button>`;
}
}
class WebButton extends Button {
render() {
return `<button class="web">${this.text}</button>`;
}
}
// Step 2: Create Factory
class ButtonFactory {
static createButton(platform, text) {
switch(platform) {
case "ios":
return new IOSButton(text);
case "android":
return new AndroidButton(text);
case "web":
return new WebButton(text);
default:
throw new Error(`Unknown platform: ${platform}`);
}
}
}
// Usage
const platform = detectPlatform();
const button = ButtonFactory.createButton(platform, "Submit");
button.render();
// Adding new platform:
// 1. Create MacOSButton class
// 2. Add one case to ButtonFactory
// 3. Done! (no existing code changes)
Building an e-commerce app, I implemented payments with Factory:
class PaymentFactory {
static createPayment(method) {
switch(method) {
case "creditCard":
return new CreditCardPayment();
case "paypal":
return new PayPalPayment();
case "kakao":
return new KakaoPayPayment();
case "toss":
return new TossPayment();
default:
throw new Error(`Unknown payment method: ${method}`);
}
}
}
// Usage
function checkout(amount, paymentMethod) {
const payment = PaymentFactory.createPayment(paymentMethod);
try {
const result = payment.process(amount);
if (result.success) {
console.log("Payment successful!");
}
} catch (error) {
console.error("Payment failed:", error);
}
}
checkout(10000, "kakao");
checkout(20000, "toss");
Benefits:
When Naver Pay was added later, only the Factory needed updates:
class PaymentFactory {
static createPayment(method) {
switch(method) {
// ... existing code
case "naver": // Just add this line
return new NaverPayment();
default:
throw new Error(`Unknown payment method: ${method}`);
}
}
}
Modifying 200 files → Modifying 1 file.
Building user profiles, I wrote this code:
// Changing user name
function updateUserName(newName) {
user.name = newName;
// Manually update all UI
updateHeaderUI(newName);
updateProfileUI(newName);
updateSidebarUI(newName);
updateChatUI(newName);
updateNotificationUI(newName);
sendAnalyticsEvent(newName);
}
// Problems:
// 1. Adding new UI requires modifying this function
// 2. Forgetting one = bug
// 3. Email, avatar changes too? Triple the functions
Actual bug I shipped: Forgot to update chat UI, so only the chat displayed the old name. Bug shipped for 3 weeks.
// Subject: Observable target
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(o => o !== observer);
}
notify(data) {
this.observers.forEach(observer => {
observer.update(data);
});
}
}
// User class
class User extends Subject {
constructor(name) {
super();
this._name = name;
}
setName(name) {
this._name = name;
this.notify({ name }); // Auto-notify all subscribers
}
getName() {
return this._name;
}
}
// Observers
const headerUI = {
update: (data) => {
console.log(`Header updated: ${data.name}`);
document.querySelector("#header-name").textContent = data.name;
}
};
const profileUI = {
update: (data) => {
console.log(`Profile updated: ${data.name}`);
document.querySelector("#profile-name").textContent = data.name;
}
};
const chatUI = {
update: (data) => {
console.log(`Chat updated: ${data.name}`);
document.querySelector("#chat-name").textContent = data.name;
}
};
// Subscribe
const user = new User("Alice");
user.subscribe(headerUI);
user.subscribe(profileUI);
user.subscribe(chatUI);
// Change name
user.setName("Bob");
// Output:
// Header updated: Bob
// Profile updated: Bob
// Chat updated: Bob
// New UI? Just subscribe
const sidebarUI = {
update: (data) => console.log(`Sidebar: ${data.name}`)
};
user.subscribe(sidebarUI);
user.setName("Charlie");
// Sidebar auto-updates too
Building a stock ticker app, I used Observer pattern:
class StockMarket extends Subject {
constructor() {
super();
this.stocks = {};
}
updatePrice(symbol, price) {
this.stocks[symbol] = price;
this.notify({ symbol, price });
}
}
// Observer: Price chart
const priceChart = {
update: (data) => {
console.log(`Chart: ${data.symbol} = ${data.price}`);
// Draw chart
}
};
// Observer: Price alert
const priceAlert = {
update: (data) => {
if (data.price > 100) {
console.log(`Alert: ${data.symbol} exceeded $100!`);
}
}
};
// Observer: Auto-trading
const autoTrader = {
update: (data) => {
if (data.price < 50) {
console.log(`Auto-buy ${data.symbol} at ${data.price}`);
}
}
};
const market = new StockMarket();
market.subscribe(priceChart);
market.subscribe(priceAlert);
market.subscribe(autoTrader);
// Price changes
market.updatePrice("AAPL", 150);
// Output:
// Chart: AAPL = $150
// Alert: AAPL exceeded $100!
market.updatePrice("TSLA", 45);
// Output:
// Chart: TSLA = $45
// Auto-buy TSLA at $45
You're already using Observer pattern:
1. JavaScript Event Listeners// addEventListener = subscribe
// dispatchEvent = notify
const button = document.querySelector("#btn");
button.addEventListener("click", () => {
console.log("Clicked!");
}); // Subscribe
button.click(); // Notify
2. React's useState
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Count changed: ${count}`);
}, [count]); // "Subscribe" to count
setCount(1); // "Notify" → useEffect executes
3. Redux Store
store.subscribe(() => {
console.log("State changed:", store.getState());
}); // Subscribe
store.dispatch({ type: "INCREMENT" }); // Notify
You've been using these patterns. You just didn't know the names.
Patterns I occasionally use in production.
// ❌ Too many parameters
class User {
constructor(name, email, age, phone, address, city, country, postalCode, avatar) {
this.name = name;
this.email = email;
this.age = age;
this.phone = phone;
this.address = address;
this.city = city;
this.country = country;
this.postalCode = postalCode;
this.avatar = avatar;
}
}
// Confusing to use
const user = new User(
"Alice",
"alice@example.com",
25,
"010-1234-5678",
"123 Main St",
"Seoul",
"Korea",
"12345",
"avatar.png"
);
// Can't remember the order 💀
class UserBuilder {
constructor() {
this.user = {};
}
setName(name) {
this.user.name = name;
return this; // Return this for chaining
}
setEmail(email) {
this.user.email = email;
return this;
}
setAge(age) {
this.user.age = age;
return this;
}
setPhone(phone) {
this.user.phone = phone;
return this;
}
setAddress(address, city, country, postalCode) {
this.user.address = { address, city, country, postalCode };
return this;
}
setAvatar(avatar) {
this.user.avatar = avatar;
return this;
}
build() {
return this.user;
}
}
// Usage (intuitive!)
const user = new UserBuilder()
.setName("Alice")
.setEmail("alice@example.com")
.setAge(25)
.setPhone("010-1234-5678")
.setAddress("123 Main St", "Seoul", "Korea", "12345")
.setAvatar("avatar.png")
.build();
// Optional fields easy to handle
const simpleUser = new UserBuilder()
.setName("Bob")
.setEmail("bob@example.com")
.build(); // Skip the rest
class RequestBuilder {
constructor(url) {
this.url = url;
this.method = "GET";
this.headers = {};
this.body = null;
}
setMethod(method) {
this.method = method;
return this;
}
setHeader(key, value) {
this.headers[key] = value;
return this;
}
setBody(body) {
this.body = body;
return this;
}
async send() {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.body
});
return response.json();
}
}
// Usage
const data = await new RequestBuilder("https://api.example.com/users")
.setMethod("POST")
.setHeader("Content-Type", "application/json")
.setHeader("Authorization", "Bearer token123")
.setBody(JSON.stringify({ name: "Alice" }))
.send();
Look familiar? jQuery, Axios, Fetch API—all Builder patterns.
function processPayment(amount, method) {
if (method === "creditCard") {
console.log("Processing credit card...");
// 100 lines of code
} else if (method === "paypal") {
console.log("Processing PayPal...");
// 100 lines of code
} else if (method === "kakao") {
console.log("Processing Kakao Pay...");
// 100 lines of code
}
// Adding payment methods makes this 500+ lines
}
// Strategy interface
class PaymentStrategy {
pay(amount) {
throw new Error("Must implement pay()");
}
}
// Concrete strategies
class CreditCardStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Credit card: ${amount}`);
}
}
class PayPalStrategy extends PaymentStrategy {
pay(amount) {
console.log(`PayPal: ${amount}`);
}
}
class KakaoPayStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Kakao Pay: ${amount}`);
}
}
// Context
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
this.strategy.pay(amount);
}
}
// Usage
const payment = new PaymentContext(new CreditCardStrategy());
payment.processPayment(100);
// Swap strategy
payment.setStrategy(new PayPalStrategy());
payment.processPayment(200);
payment.setStrategy(new KakaoPayStrategy());
payment.processPayment(300);
class SortStrategy {
sort(data) {
throw new Error("Must implement sort()");
}
}
class BubbleSort extends SortStrategy {
sort(data) {
console.log("Using Bubble Sort");
return data.sort((a, b) => a - b);
}
}
class QuickSort extends SortStrategy {
sort(data) {
console.log("Using Quick Sort");
return data.sort((a, b) => a - b);
}
}
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
sort(data) {
return this.strategy.sort(data);
}
}
// Usage
const data = [5, 2, 8, 1, 9];
const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort(data));
// Large data? Switch to QuickSort
const bigData = Array.from({ length: 10000 }, () => Math.random());
sorter.strategy = new QuickSort();
console.log(sorter.sort(bigData));
Already used in production:
Array.sort() (inject comparison function = inject strategy)fetch() (Request options = strategy)We switched payment APIs. Problem: existing code spread across 100 files.
// Old API
class OldPaymentAPI {
processTransaction(user, amount) {
return {
status: "success",
transactionId: "12345",
user: user,
amount: amount
};
}
}
// New API (completely different format)
class NewPaymentAPI {
pay(userId, amountInCents) {
return {
success: true,
id: "67890",
user_id: userId,
amount_cents: amountInCents
};
}
}
// Problem: Existing code in 100 places looks like this
const result = oldAPI.processTransaction("user123", 100);
if (result.status === "success") {
console.log(result.transactionId);
}
// Switching to new API requires changing 100 places 💀
// Adapter: Convert old API format to new API
class PaymentAdapter {
constructor(newAPI) {
this.newAPI = newAPI;
}
processTransaction(user, amount) {
// Call new API (convert to cents)
const result = this.newAPI.pay(user, amount * 100);
// Convert to old API format and return
return {
status: result.success ? "success" : "failed",
transactionId: result.id,
user: result.user_id,
amount: result.amount_cents / 100
};
}
}
// Usage
const newAPI = new NewPaymentAPI();
const adapter = new PaymentAdapter(newAPI);
// Existing code works unchanged!
const result = adapter.processTransaction("user123", 100);
if (result.status === "success") {
console.log(result.transactionId); // Works!
}
// No need to modify 100 files!
When migrating from jQuery to React:
// Wrap jQuery code for React
class JQueryAdapter {
constructor(selector) {
this.element = document.querySelector(selector);
}
// jQuery-style methods
text(value) {
if (value === undefined) {
return this.element.textContent;
}
this.element.textContent = value;
return this;
}
addClass(className) {
this.element.classList.add(className);
return this;
}
removeClass(className) {
this.element.classList.remove(className);
return this;
}
on(event, handler) {
this.element.addEventListener(event, handler);
return this;
}
}
// Original jQuery code
// $("#header").text("Hello").addClass("active");
// Progressive migration with Adapter
const $ = (selector) => new JQueryAdapter(selector);
$("#header").text("Hello").addClass("active");
// Works without jQuery!
Production tip: Using Adapter for library changes enables progressive migration. Much safer than rewriting everything at once.
"To a man with a hammer, everything looks like a nail."
Here's my embarrassing mistake when I first learned design patterns.
Before (pre-patterns):// Simple calculator
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
console.log(add(5, 3)); // 8
After (just learned patterns):
// ❌ Pattern overuse
class CalculationStrategy {
calculate(a, b) {
throw new Error("Must implement");
}
}
class AdditionStrategy extends CalculationStrategy {
calculate(a, b) {
return a + b;
}
}
class SubtractionStrategy extends CalculationStrategy {
calculate(a, b) {
return a - b;
}
}
class CalculatorFactory {
static createCalculator(type) {
switch(type) {
case "add":
return new AdditionStrategy();
case "subtract":
return new SubtractionStrategy();
}
}
}
class CalculatorContext {
constructor(strategy) {
this.strategy = strategy;
}
execute(a, b) {
return this.strategy.calculate(a, b);
}
}
// Usage (overcomplicated)
const factory = CalculatorFactory;
const addStrategy = factory.createCalculator("add");
const calculator = new CalculatorContext(addStrategy);
console.log(calculator.execute(5, 3)); // 8
// Simple addition: 5 classes + 50 lines 💀
Senior's code review:
"What is this? Why make simple addition so complex?"
I learned: Patterns are tools. Overuse becomes poison.
My criteria from experience:
// ❌ Over-engineering for the future
class UserFactory {
static createUser(name, type = "normal") {
switch(type) {
case "normal":
return new NormalUser(name);
case "admin": // Not used now, but "might need later"
return new AdminUser(name);
case "guest": // This too
return new GuestUser(name);
case "premium": // And this
return new PremiumUser(name);
}
}
}
// ✅ Only what's needed now
class User {
constructor(name) {
this.name = name;
}
}
const user = new User("Alice");
Lesson: "Might need later" usually means never. Add it when actually needed.
// ❌ Overcomplicating simple logger
class LoggerSingleton {
static instance = null;
constructor() {
if (LoggerSingleton.instance) {
return LoggerSingleton.instance;
}
this.logs = [];
LoggerSingleton.instance = this;
}
log(message) {
this.logs.push(message);
}
}
// ✅ Just use console.log
console.log("Hello");
// Use Singleton only when log persistence is truly needed
// ❌ Pattern abuse in game loop
class GameLoop {
update() {
// Running at 60fps
const renderer = RendererFactory.createRenderer("webgl"); // Create every frame 💀
const physics = PhysicsFactory.createEngine("box2d"); // Slow
renderer.render();
physics.update();
}
}
// ✅ Create once
class GameLoop {
constructor() {
this.renderer = new WebGLRenderer(); // Once during init
this.physics = new Box2DEngine();
}
update() {
this.renderer.render(); // Reuse
this.physics.update();
}
}
Worst code I've seen (company name withheld):
// Real code I encountered
class SingletonFactoryObserverStrategyAdapterProxy {
// ...800 lines
}
This class combined:
Six patterns in one class.
Result:Lesson: Pattern 1 = Good Pattern 2 = OK Pattern 3+ = Suspicious Pattern 6 = Needs redesign
My five-year production guide:
| Problem | Pattern | Real Example | When NOT to Use? |
|---|---|---|---|
| Only 1 instance needed | Singleton | DB, Config, Logger | Test-heavy code (hard to mock) |
| Platform/type-specific object creation | Factory | Platform UI, Payment methods | 2 or fewer types |
| Complex object step-by-step creation | Builder | HTTP Request, Query Builder | 3 or fewer parameters |
| Multiple updates on data change | Observer | Events, State management | 1-2 subscribers only |
| Swap algorithms | Strategy | Payment methods, Sort algorithms | Only 1 algorithm |
| Interface mismatch | Adapter | API migration, Library wrapping | Code you can modify directly |
The biggest lesson from studying design patterns: true value lies in communication, not code.
Before (didn't know patterns):Me: "This class needs to exist only once in the program,
so make the constructor private,
store instance in static variable,
return via getInstance() method..."
Senior: "OK, so?"
Me: "So... globally accessible while
maintaining single instance..."
Senior: "Hmm..." (3 minutes pass)
After (learned patterns):
Me: "I'll use Singleton."
Senior: "OK." (1 second)
I understood: 1-hour explanations compressed to 1 second. Magic.
As a 5-year developer in a feature planning meeting:
PM: "How should we implement user notifications?"
Before (no patterns):
Me: "Um... when user data changes,
update header, profile, sidebar separately..."
→ 30-minute meeting
After (with patterns):
Me: "Observer pattern works here.
User object as Subject,
each UI as Observer."
Senior: "Yeah, do that."
→ 5-minute meeting
Takeaway: Design patterns are developers' shared vocabulary. Speaking the same language accelerates communication.
When I first suggested in code review, "How about Factory pattern here?", the senior smiled and said:
"Now you're speaking developer language. Welcome."
That moment, it all clicked.
Design patterns aren't about "writing good code"—they're a developer's rite of passage.
When you naturally use words like "Singleton," "Factory," "Observer," someone will tell you:
"Now you're a real developer."