Prologue: "I've Seen This Code Before..."
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.
Why I Studied This: A Trail of Failures
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.
What Confused Me Initially
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.
The Aha Moment: "Cooking Recipes"
After a month of reading theory, I revisited production code. Shock hit me.
"Wait... these are all patterns?"
- Redux Store → Observer pattern
- React's
new Promise()→ Factory pattern - Axios Instance → Singleton pattern
Array.map()→ Strategy pattern
I'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.
1. The Big Three You Must Know
From five years of production experience, here are my top three patterns.
Singleton: "One and Only"
The Problem: Memory Bomb
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.
Solution: getInstance() Returns One Instance
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)
Real-World Examples
1. Configuration Manager
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
Caveat: Testing Difficulties
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();
});
Factory: "Order from the Factory"
The Problem: if-else Hell
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.
Solution: Order from Factory
// 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)
Real Experience: Payment System
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:
- Adding payment methods requires only Factory changes
- Each payment class managed independently
- Easy to test (inject mock objects)
Pro tip:
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.
Observer: "Subscribe & Notify"
The Problem: Manual Update Hell
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.
Solution: Subscribe-Notify Pattern
// 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
Real Example: Stock App
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
Observer Patterns You Already Use
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.
2. Other Patterns (Production Version)
Patterns I occasionally use in production.
Builder: "Custom Burger Order"
The Problem: Constructor Hell
// ❌ 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 💀
Solution: Builder Pattern
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
Production Example: HTTP Request Builder
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.
Strategy: "Swap Payment Methods"
The Problem: if-else Returns
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
}
Solution: Swappable Strategies
// 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);
Production Example: Sorting Algorithms
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)- React Context Provider (provide strategy)
Adapter: "Plug Converter"
The Problem: API Changes
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 💀
Solution: Adapter as Bridge
// 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!
Production Example: Library Migration
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.
3. Warning: Golden Hammer Anti-Pattern
"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.
When NOT to Use Patterns
My criteria from experience:
1. YAGNI (You Ain't Gonna Need It)
// ❌ 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.
2. KISS (Keep It Simple, Stupid)
// ❌ 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
3. Performance-Critical Code
// ❌ 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();
}
}
Actual Golden Hammer Case
Worst code I've seen (company name withheld):
// Real code I encountered
class SingletonFactoryObserverStrategyAdapterProxy {
// ...800 lines
}
This class combined:
- Singleton (global state)
- Factory (object creation)
- Observer (event subscription)
- Strategy (algorithm swapping)
- Adapter (API conversion)
- Proxy (access control)
Six patterns in one class.
Result:
- Nobody understood it
- Bugs impossible to fix
- Eventually completely refactored
Lesson: Pattern 1 = Good Pattern 2 = OK Pattern 3+ = Suspicious Pattern 6 = Needs redesign
4. Summary: Pattern Selection Guide
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 |
Additional Criteria:
Good to use when:
- Code duplicated in 3+ places
- Requirements change frequently
- Team collaboration
- Testing needed
Not needed when:
- Code under 10 lines
- One-time use code
- Performance critical
- Solo project
Final Thoughts: "Power of Shared Language"
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.
Real Experience: Meeting Time Reduction
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.
First Time Using Pattern Names
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."