Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Design Patterns Every Developer Should Know: From Principles to Practice
Sep 2, 2025
340 views
Written by Prashant Basnet
👋 Welcome to my Signature, a space between logic and curiosity.
I’m a Software Development Engineer who loves turning ideas into systems that work beautifully.
This space captures the process: the bugs, breakthroughs, and “aha” moments that keep me building.
First thing first what's difference between Design Principles vs Design Patterns?
The Why?
Design Principles are broad guidelines or philosophies for writing good software. The nature is that these are very high level, language agnostic & timeless.
Example: SOLID, DRY, KISS, YAGNI.
These are the rules of thumb, like laws of gravity in software design.
The How?
Design patterns on the other had are concrete, reusable solutions to common design problems. These are implementations that you can recognize in the code. Principles are like laws of physics whereas patterns are engineering blueprints built on top of them.
Now let’s walk through the most common patterns, grouped by category.
Design patterns are grouped into 3 categories.
Creational Patterns
1. Singleton Pattern
class Database { private static instance: Database; static getInstance(): Database { if (!Database.instance) { Database.instance = new Database(); } return Database.instance; } } // Usage const db1 = Database.getInstance(); const db2 = Database.getInstance();2. Factory Pattern
interface Database { connect(): void; query(sql: string): any[]; }class MySQLDatabase implements Database { connect() { console.log("Connected to MySQL"); } query(sql: string) { return [`MySQL result for: ${sql}`]; } }class PostgreSQLDatabase implements Database { connect() { console.log("Connected to PostgreSQL"); } query(sql: string) { return [`PostgreSQL ${sql}`]; } }class DatabaseFactory { static create(type: string): Database { switch(type) { case 'mysql': return new MySQLDatabase(); case 'postgres': return new PostgreSQLDatabase(); default: throw new Error('Unknown database type'); } } }const db = DatabaseFactory.create('mysql'); db.connect(); // "Connected to MySQL"Factory will get you the database without knowing how to build it! .
3. Builder Pattern
Problem: Repeated construction of structured responses & request context.
Example:
new PizzaBuilder() .addDough("Thin Crust") .addCheese("Mozzarella") .addTopping("Olives") .build();Benefit: Step-by-step assembly, consistent results, easy to extend.
4. Prototype Pattern
When creating a new object is expensive or complex (e.g., parsing configs, initializing heavy structures), instead of constructing from scratch, you clone an existing prototype.
How it works:
class ServiceConfig { constructor(public retries: number, public timeout: number) {} clone(): ServiceConfig { return new ServiceConfig(this.retries, this.timeout); } }Create a prototype config
Clone for a new service
Here, instead of building configs from scratch every time, you clone the prototype and tweak only what’s different.
Structural Patterns:
5. Adapter Pattern
Problem: A library’s API doesn’t match how your app wants to call it.
Example: AuthProvider adapter wrapping an external auth SDK.
class AuthProvider implements Auth { constructor(private sdk: any) {} authenticate(req: Request) { return this.sdk.api.getSession({ headers: req.headers }); } }Now the rest of your app always calls the clean interface:
Benefit: One consistent interface; swap libraries with minimal changes.
6. Decorator Pattern
In software design, the Decorator pattern is a structural pattern that:
You have a plain coffee (the base object).
app.decorate('requireAuth', (request, reply) => { if (!request.headers.authorization) { throw new Error('Not authenticated'); } });app.get('/users', { preHandler: [ app.requireAuth, // Decorated method ] });7. Composite Pattern
It lets you treat a group of objects the same way as a single object.
The composite wraps multiple child objects and forwards calls to them.
Example
You have multiple metrics writers:
Normally, you’d have to call each one separately:
But with a Composite:
You treat many writers as if they were one writer.
Benefit: Uniform interface; adding a new metrics sink is zero-effort.
8. Facade Pattern
Example (Payments Facade):
class PaymentGateway { charge() {} } class InvoiceService { createInvoice() {} } class NotificationService { sendEmail() {} } class PaymentFacade { constructor( private gateway: PaymentGateway, private invoice: InvoiceService, private notifier: NotificationService ) {} checkout() { this.gateway.charge(); this.invoice.createInvoice(); this.notifier.sendEmail(); } } new PaymentFacade().checkout();✅ Benefit: Simplifies usage for clients while keeping subsystems separate.
9. Strategy Pattern:
You need to get to work, but you can choose different strategies:
one way to do it is without Strategy Pattern, messy if else everywhere
class Commute { getToWork(method: string, distance: number) { if (method === "car") { console.log(`Driving ${distance} miles`); } else if (method === "bike") { console.log(`Biking ${distance} miles`); } else if (method === "walk") { console.log(`Walking ${distance} miles`); } // Adding new method = modify this function every time! } }✅ With Strategy Pattern - Clean & Flexible
interface TransportStrategy { travel(distance: number): void; }class CarStrategy implements TransportStrategy { travel(distance: number): void { console.log(`🚗 Driving ${distance} miles`); } }class BikeStrategy implements TransportStrategy { travel(distance: number): void { console.log(`🚲 Biking ${distance} miles`); } }class WalkStrategy implements TransportStrategy { travel(distance: number): void { console.log(`🚶 Walking ${distance} miles`); } }class Commute { private strategy: TransportStrategy; constructor(strategy: TransportStrategy) { this.strategy = strategy; } // Can change strategy anytime! setStrategy(strategy: TransportStrategy) { this.strategy = strategy; } // Delegate to current strategy getToWork(distance: number) { this.strategy.travel(distance); } }// Add new strategy without changing existing code!
class UberStrategy implements TransportStrategy { travel(distance: number): void { console.log(`🚕 Uber ${distance} miles `); } }💡 Key Points:
Behavioral Patterns
10. Observer Pattern
A behavioral design pattern where an subject maintains a list of dependents observers and notifies them automatically of any state changes. The Observer pattern is about one to many communication.
bus.emit("http.completed", { route: "/users", status: 200, durationMs: 123 });Observers (subscribers) handle these events:
bus.on("http.completed", data => console.log("Log writer", data)); bus.on("http.completed", data => prometheusWriter.write(data)); bus.on("http.completed", data => cloudWatchWriter.write(data));When the route completes, the bus notifies all observers at once.
11. Command Pattern
It's a behavioral design pattern. It wraps an action like method call into an object so that we can treat it like data. So that we can :
Instead of calling service.doSomething(), you wrap it as a Command and run it through a standard pipeline.
async function ControllerHandler(fn: (req, reply) => Promise<any>) { return async (req, reply) => { try { const result = await fn(req, reply); log.info("Controller executed", { route: req.url }); reply.send(result); } catch (err) { log.error(err); reply.status(500).send({ error: "Something went wrong" }); } }; }Command turns an action into a standard object (or function) so you can execute it consistently with extra behavior (logging, error handling, retries).
Benefit: Standardized execution pipeline
These are the most common design patterns.