Design Patterns in JavaScript, Python, and Rust
Back in 2011, I wrote a walkthrough of design patterns with PHP and JavaScript examples. PHP and ES5 have aged a lot since then. The patterns have not.
This article is a companion to the open-source repository design-patterns-for-web-developer, which covers the 5 SOLID principles and 23 GoF patterns with examples in modern JavaScript (ES2020+), Python (3.10+), and Rust (2021 edition). For each topic I show a condensed version of the code and link to the full source.
The point of comparing three languages is not to say one is better. It is to show that the same structural ideas appear in dynamically typed scripting languages, an OO/functional hybrid, and a systems language with no inheritance at all. When the same solution emerges in all three, you know you are looking at a real pattern, not a language quirk.
SOLID Principles
SOLID is a set of five design rules for writing code that does not rot when requirements change. They apply at the class and module level, and they show up in almost every well-designed codebase regardless of language.
Single Responsibility
A class should have one reason to change.
When a class handles multiple concerns (persistence, business logic, notification), a schema change forces you to touch auth code. A hashing algorithm upgrade risks breaking email logic. Keep each class responsible for exactly one thing.
Violation: a User class that saves itself, hashes passwords, and sends email all at once.
Applied: three separate classes.
// JavaScript
class UserRepository {
save({ email, passwordHash }) { /* db write */ }
}
class AuthService {
async hash(plaintext) { return bcrypt.hash(plaintext, 12); }
async verify(plaintext, hash) { return bcrypt.compare(plaintext, hash); }
}
class Mailer {
async sendWelcome(email) { /* send email */ }
}
async function registerUser({ email, password }, { db, transport }) {
const auth = new AuthService();
const repo = new UserRepository(db);
const mail = new Mailer(transport);
const passwordHash = await auth.hash(password);
await repo.save({ email, passwordHash });
await mail.sendWelcome(email);
}
# Python
class UserRepository:
def insert(self, record: UserRecord) -> None:
self._conn.execute(
"INSERT INTO users (email, password_hash) VALUES (?, ?)",
(record.email, record.password_hash),
)
class AuthService:
def hash(self, plaintext: str) -> str:
return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt(12)).decode()
class Mailer:
def send_welcome(self, email: str) -> None:
self._transport.send(email, "Welcome", "Your account is ready.")
def register_user(email, password, *, repo, auth, mailer):
password_hash = auth.hash(password)
repo.insert(UserRecord(email=email, password_hash=password_hash))
mailer.send_welcome(email)
// Rust: src/register_user.rs
pub async fn register_user<T: Transport>(
email: &str,
password: &str,
repo: &UserRepository,
auth: &AuthService,
mailer: &Mailer<T>,
) -> Result<()> {
if repo.find_by_email(email).await?.is_some() {
bail!("Email already registered");
}
let password_hash = auth.hash_password(password)?;
repo.insert(&UserRecord { email: email.to_string(), password_hash }).await?;
mailer.send_welcome(email)?;
Ok(())
}
Open/Closed
Open for extension, closed for modification.
Adding a new behavior should not require editing existing, tested code. The classic violation is a function or class that grows a new branch every time requirements change.
Violation: calculateDiscount with a growing chain of if statements.
Applied: each discount type is a separate object. Adding a new one means adding a new class, nothing else.
// JavaScript
class SummerDiscount { apply(price) { return price * 0.8; } }
class LoyaltyDiscount {
constructor(years) { this.rate = Math.min(0.05 * years, 0.3); }
apply(price) { return price * (1 - this.rate); }
}
class OrderCalculator {
constructor(discount = { apply: p => p }) { this.discount = discount; }
total(items) {
const sub = items.reduce((s, i) => s + i.price * i.qty, 0);
return this.discount.apply(sub);
}
}
# Python
from abc import ABC, abstractmethod
class Discount(ABC):
@abstractmethod
def apply(self, price: float) -> float: ...
class SummerDiscount(Discount):
def apply(self, price: float) -> float:
return price * 0.8
class LoyaltyDiscount(Discount):
def __init__(self, years: int):
self._rate = min(0.05 * years, 0.30)
def apply(self, price: float) -> float:
return price * (1 - self._rate)
class OrderCalculator:
def __init__(self, discount: Discount):
self._discount = discount
def total(self, items) -> float:
sub = sum(i.price * i.qty for i in items)
return self._discount.apply(sub)
// Rust
pub trait Discount {
fn apply(&self, price: f64) -> f64;
}
pub struct SummerDiscount;
impl Discount for SummerDiscount {
fn apply(&self, price: f64) -> f64 { price * 0.8 }
}
pub struct LoyaltyDiscount { rate: f64 }
impl LoyaltyDiscount {
pub fn new(years: u32) -> Self {
Self { rate: (0.05 * years as f64).min(0.30) }
}
}
impl Discount for LoyaltyDiscount {
fn apply(&self, price: f64) -> f64 { price * (1.0 - self.rate) }
}
pub struct OrderCalculator<D: Discount> { discount: D }
impl<D: Discount> OrderCalculator<D> {
pub fn total(&self, items: &[LineItem]) -> f64 {
let sub: f64 = items.iter().map(|i| i.price * i.qty as f64).sum();
self.discount.apply(sub)
}
}
Liskov Substitution
Subtypes must be substitutable for their base types.
Code that works with a base type must work correctly with any derived type, without knowing the difference. The classic trap is inheriting Square from Rectangle and overriding setters in a way that breaks callers who set width and height independently.
Applied: treat them as separate, unrelated types sharing only the abstract Shape interface.
// JavaScript — no formal interface, but the contract is implicit
class Rectangle {
constructor(width, height) { this.width = width; this.height = height; }
area() { return this.width * this.height; }
perimeter() { return 2 * (this.width + this.height); }
scale(f) { return new Rectangle(this.width * f, this.height * f); }
}
class Square {
constructor(side) { this.side = side; }
area() { return this.side ** 2; }
perimeter() { return 4 * this.side; }
scale(f) { return new Square(this.side * f); }
}
// Works correctly for any object with area/perimeter/scale.
function describeShape(shape) {
return { area: shape.area(), perimeter: shape.perimeter() };
}
# Python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
@abstractmethod
def scale(self, factor: float) -> "Shape": ...
class Rectangle(Shape):
def __init__(self, w, h): self._w, self._h = w, h
def area(self): return self._w * self._h
def perimeter(self): return 2 * (self._w + self._h)
def scale(self, f): return Rectangle(self._w * f, self._h * f)
class Square(Shape):
def __init__(self, s): self._s = s
def area(self): return self._s ** 2
def perimeter(self): return 4 * self._s
def scale(self, f): return Square(self._s * f)
// Rust: traits enforce the contract at compile time.
// Class-based inheritance that could produce the Rectangle/Square trap
// does not exist in Rust, so the violation cannot occur.
trait Shape {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
fn scale(&self, factor: f64) -> Box<dyn Shape>;
}
struct Rectangle { width: f64, height: f64 }
impl Shape for Rectangle {
fn area(&self) -> f64 { self.width * self.height }
fn perimeter(&self) -> f64 { 2.0 * (self.width + self.height) }
fn scale(&self, f: f64) -> Box<dyn Shape> {
Box::new(Rectangle { width: self.width * f, height: self.height * f })
}
}
struct Square { side: f64 }
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
fn perimeter(&self) -> f64 { 4.0 * self.side }
fn scale(&self, f: f64) -> Box<dyn Shape> {
Box::new(Square { side: self.side * f })
}
}
Interface Segregation
No client should depend on methods it does not use.
Fat interfaces force implementors to stub out methods that do not apply to them. Robot implementing eat() and sleep() is a red flag. Split the interface into narrow capabilities and let types opt in to only what makes sense.
// JavaScript — narrow capability contracts via duck typing
class HumanWorker {
work() { console.log("working"); }
eat(meal) { console.log(`eating ${meal}`); }
sleep(hours) { console.log(`sleeping ${hours}h`); }
}
class RobotWorker {
work() { console.log("working"); }
recharge(pct) { console.log(`recharging to ${pct}%`); }
}
function runShift(workers) { workers.forEach(w => w.work()); }
function lunchBreak(workers) { workers.forEach(w => w.eat("sandwich")); }
runShift([new HumanWorker(), new RobotWorker()]);
lunchBreak([new HumanWorker()]);
// RobotWorker is simply never passed to lunchBreak.
# Python — structural Protocols, no inheritance required
from typing import Protocol, runtime_checkable
@runtime_checkable
class Workable(Protocol):
def work(self) -> None: ...
@runtime_checkable
class Feedable(Protocol):
def eat(self, meal: str) -> None: ...
class HumanWorker:
def work(self): print("working")
def eat(self, meal): print(f"eating {meal}")
class RobotWorker:
def work(self): print("working")
def run_shift(workers: list[Workable]) -> None:
for w in workers: w.work()
def lunch_break(workers: list[Feedable]) -> None:
for w in workers: w.eat("sandwich")
// Rust — traits are narrow by default; each type opts in selectively
trait Workable { fn work(&self); }
trait Feedable { fn eat(&self, meal: &str); }
trait Restable { fn sleep(&self, hours: u32); }
struct HumanWorker { name: String }
impl Workable for HumanWorker { fn work(&self) { println!("{} working", self.name); } }
impl Feedable for HumanWorker { fn eat(&self, meal: &str) { println!("eating {meal}"); } }
impl Restable for HumanWorker { fn sleep(&self, h: u32) { println!("sleeping {h}h"); } }
struct RobotWorker { id: String }
impl Workable for RobotWorker { fn work(&self) { println!("Robot {} working", self.id); } }
// RobotWorker simply does not implement Feedable or Restable.
// Any attempt to call eat() on it is a compile error, not a runtime panic.
Dependency Inversion
Depend on abstractions, not concretions.
High-level modules (business logic) should not reach down and instantiate low-level modules directly. Both should depend on an interface. This is what makes code testable and what lets you swap MySQL for SQLite in tests without touching the logic.
// JavaScript — OrderService accepts any object with the right shape
class OrderService {
constructor(db) { this.db = db; }
async place(order) {
if (order.total <= 0) throw new Error("Total must be positive");
return this.db.save(order);
}
}
// Production
const service = new OrderService(new MySQLDatabase(pool));
// Tests — no mocking framework needed
const service = new OrderService(new InMemoryDatabase());
# Python — Database is a Protocol; OrderService never imports a driver
from typing import Protocol
class Database(Protocol):
def save(self, order: Order) -> Order: ...
def find_by_customer(self, customer_id: int) -> list[Order]: ...
class OrderService:
def __init__(self, db: Database):
self._db = db
def place(self, customer_id: int, total: float) -> Order:
if total <= 0:
raise ValueError("Total must be positive")
return self._db.save(Order(customer_id=customer_id, total=total))
// Rust — generic over any Database implementation, zero-cost at runtime
pub trait Database {
fn save(&mut self, order: Order) -> Result<Order>;
fn find_by_customer(&self, customer_id: u64) -> Result<Vec<Order>>;
}
pub struct OrderService<D: Database> { db: D }
impl<D: Database> OrderService<D> {
pub fn place(&mut self, customer_id: u64, total: f64) -> Result<Order> {
if total <= 0.0 { bail!("Total must be positive"); }
self.db.save(Order { id: None, customer_id, total })
}
}
// Tests: OrderService::new(InMemoryDatabase::default())
// Prod: OrderService::new(MySQLDatabase::new(pool))
Creational Patterns
Creational patterns deal with how objects get made. They decouple your code from the specifics of construction, which matters when the right type to create is not known until runtime or when building the object has real complexity.
Singleton
Ensure a class has only one instance and provide a global point of access to it. Typical uses: application config, shared logger, connection pool.
// JavaScript
class Config {
static #instance = null;
#data;
constructor() {
this.#data = {
apiUrl: process.env.API_URL ?? "http://localhost:3000",
timeout: Number(process.env.TIMEOUT ?? 5000),
};
}
static getInstance() {
if (!Config.#instance) Config.#instance = new Config();
return Config.#instance;
}
get(key) { return this.#data[key]; }
}
// Simpler: in JS a module is already a singleton.
// export const config = { apiUrl: process.env.API_URL ?? "..." };
# Python
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._data = {
"api_url": os.getenv("API_URL", "http://localhost:3000"),
}
return cls._instance
def get(self, key): return self._data[key]
# Simpler: a module-level variable is a singleton in Python too.
// Rust — OnceLock is the idiomatic singleton
use std::sync::OnceLock;
struct Config { api_url: String, timeout: u64 }
static CONFIG: OnceLock<Config> = OnceLock::new();
fn get_config() -> &'static Config {
CONFIG.get_or_init(|| Config {
api_url: std::env::var("API_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string()),
timeout: 5000,
})
}
Factory Method
Define an interface for creating an object, but let subclasses or a factory function decide which concrete type to instantiate. The caller never names the class directly.
// JavaScript
function createNotifier(type, target) {
switch (type) {
case "email": return new EmailNotifier(target);
case "sms": return new SMSNotifier(target);
case "push": return new PushNotifier(target);
default: throw new Error(`Unknown type: ${type}`);
}
}
const notifier = createNotifier(user.preferredChannel, user.contact);
notifier.send("Your order shipped.");
# Python
def create_notifier(type_: str, target: str) -> Notifier:
match type_:
case "email": return EmailNotifier(target)
case "sms": return SMSNotifier(target)
case "push": return PushNotifier(target)
case _: raise ValueError(f"Unknown type: {type_!r}")
notifier = create_notifier("sms", "+1-555-0100")
notifier.send("Your order shipped.")
// Rust — returns Box<dyn Notifier> for runtime polymorphism
fn create_notifier(kind: &str, target: &str) -> Box<dyn Notifier> {
match kind {
"email" => Box::new(EmailNotifier { address: target.to_string() }),
"sms" => Box::new(SmsNotifier { phone: target.to_string() }),
"push" => Box::new(PushNotifier { device: target.to_string() }),
other => panic!("Unknown notifier type: {}", other),
}
}
let notifier = create_notifier("push", "device-abc");
notifier.send("Your order shipped.");
Abstract Factory
Create families of related objects without specifying their concrete classes. Useful when components within a family must be compatible (e.g., a UI theme where buttons and inputs share the same visual style).
// JavaScript
const LightFactory = {
createButton: label => ({ theme: "light", label }),
createInput: placeholder => ({ theme: "light", placeholder }),
};
const DarkFactory = {
createButton: label => ({ theme: "dark", label }),
createInput: placeholder => ({ theme: "dark", placeholder }),
};
function buildLoginForm(factory) {
return {
button: factory.createButton("Sign in"),
input: factory.createInput("Enter your email"),
};
}
const theme = process.env.THEME === "dark" ? DarkFactory : LightFactory;
const form = buildLoginForm(theme);
# Python
from abc import ABC, abstractmethod
class UIFactory(ABC):
@abstractmethod
def create_button(self, label: str) -> Button: ...
@abstractmethod
def create_input(self, placeholder: str) -> Input: ...
class DarkFactory(UIFactory):
def create_button(self, label): return Button("dark", label)
def create_input(self, placeholder): return Input("dark", placeholder)
def build_login_form(factory: UIFactory) -> dict:
return {
"button": factory.create_button("Sign in"),
"input": factory.create_input("Enter your email"),
}
// Rust
trait UIFactory {
fn create_button(&self, label: &str) -> Box<dyn Button>;
fn create_input(&self, placeholder: &str) -> Box<dyn Input>;
}
fn build_login_form(factory: &dyn UIFactory) {
let button = factory.create_button("Sign in");
let input = factory.create_input("Enter your email");
println!("{}", button.render());
println!("{}", input.render());
}
fn main() {
let factory: Box<dyn UIFactory> = if std::env::var("THEME").unwrap_or_default() == "dark" {
Box::new(DarkFactory)
} else {
Box::new(LightFactory)
};
build_login_form(factory.as_ref());
}
Builder
Separate the construction of a complex object from its representation. Useful when an object has many optional fields, because it replaces a constructor with a dozen parameters with a readable, chainable API.
// JavaScript
const request = new RequestBuilder("https://api.example.com/orders")
.method("POST")
.header("Content-Type", "application/json")
.auth("Bearer token-xyz")
.body(JSON.stringify({ item: "book", qty: 2 }))
.timeout(3000)
.build();
# Python
request = (
RequestBuilder("https://api.example.com/orders")
.method("POST")
.header("Content-Type", "application/json")
.auth("Bearer token-xyz")
.body(json.dumps({"item": "book", "qty": 2}))
.timeout(3000)
.build()
)
// Rust — the builder pattern is idiomatic Rust, used across the ecosystem
let request = RequestBuilder::new("https://api.example.com/orders")
.method("POST")
.header("Content-Type", "application/json")
.auth("Bearer token-xyz")
.body(r#"{"item":"book","qty":2}"#)
.timeout(Duration::from_secs(3))
.build()
.expect("URL is required");
Prototype
Create new objects by cloning an existing one. The clone starts with all the state of the prototype and you adjust only what differs. Useful for spawning many similar objects (game entities, test fixtures, email templates) without expensive reinitialisation.
// JavaScript
class Enemy {
constructor({ type, health, speed, position, weapon }) {
Object.assign(this, { type, health, speed, weapon });
this.position = { ...position };
}
clone() { return new Enemy({ ...this, position: { ...this.position } }); }
}
const archerPrototype = new Enemy({ type: "archer", health: 80, speed: 1.2,
position: { x: 0, y: 0 }, weapon: "bow" });
function spawnEnemy(proto, x, y, healthMod = 1) {
const e = proto.clone();
e.position.x = x; e.position.y = y;
e.health = Math.round(e.health * healthMod);
return e;
}
const elite = spawnEnemy(archerPrototype, 30, 15, 1.5);
# Python
import copy
@dataclass
class Enemy:
type: str; health: int; speed: float; position: Position; weapon: str
def clone(self) -> "Enemy":
return copy.deepcopy(self)
def spawn_enemy(proto: Enemy, x, y, health_mod=1.0) -> Enemy:
e = proto.clone()
e.position.x = x; e.position.y = y
e.health = round(e.health * health_mod)
return e
// Rust — #[derive(Clone)] generates clone() automatically
#[derive(Clone)]
struct Enemy {
kind: String, health: u32, speed: f32,
x: f32, y: f32, weapon: String,
}
fn spawn_enemy(proto: &Enemy, x: f32, y: f32, health_mod: f32) -> Enemy {
Enemy { x, y, health: (proto.health as f32 * health_mod) as u32, ..proto.clone() }
}
Structural Patterns
Structural patterns describe how to compose classes and objects into larger, more useful structures. They tend to solve problems of incompatible interfaces, excessive subclassing, and unnecessary memory usage.
Adapter
Convert the interface of a class into another interface clients expect. Lets you use code with an incompatible API without touching it.
// JavaScript — wraps a legacy gateway that expects cents
class PaymentAdapter {
#gateway;
constructor(gateway) { this.#gateway = gateway; }
processPayment({ amount, currency }) {
return this.#gateway.charge(Math.round(amount * 100), currency.toUpperCase());
}
}
const adapter = new PaymentAdapter(new LegacyGateway());
adapter.processPayment({ amount: 19.99, currency: "usd" });
# Python
class PaymentAdapter:
def __init__(self, gateway: LegacyGateway) -> None:
self._gateway = gateway
def process_payment(self, amount: float, currency: str) -> dict:
return self._gateway.charge(round(amount * 100), currency.upper())
// Rust
struct LegacyGatewayAdapter { gateway: LegacyGateway }
impl PaymentGateway for LegacyGatewayAdapter {
fn process_payment(&self, amount: f64, currency: &str) -> Result<String, String> {
let cents = (amount * 100.0).round() as u64;
self.gateway.charge(cents, ¤cy.to_uppercase())
}
}
Bridge
Decouple an abstraction from its implementation so the two can vary independently. Prevents a class explosion when you have two or more orthogonal dimensions of variation (e.g., message format x delivery channel).
// JavaScript — format and channel are independent
class PlainNotification {
constructor(channel) { this.channel = channel; }
notify(text) { this.channel.send(text); }
}
class HtmlNotification {
constructor(channel) { this.channel = channel; }
notify(text) { this.channel.send(`<p>${text}</p>`); }
}
new PlainNotification(new EmailChannel()).notify("Server is down");
new HtmlNotification(new SmsChannel()).notify("Server is down");
// 4 combinations from 2+2 classes, not 4 subclasses.
# Python
class PlainNotification(Notification):
def notify(self, text: str) -> None:
self._channel.send(text)
class HtmlNotification(Notification):
def notify(self, text: str) -> None:
self._channel.send(f"<p>{text}</p>")
// Rust
trait Channel { fn send(&self, message: &str); }
trait Notification { fn notify(&self, text: &str); }
struct PlainNotification<C: Channel> { channel: C }
impl<C: Channel> Notification for PlainNotification<C> {
fn notify(&self, text: &str) { self.channel.send(text); }
}
struct HtmlNotification<C: Channel> { channel: C }
impl<C: Channel> Notification for HtmlNotification<C> {
fn notify(&self, text: &str) { self.channel.send(&format!("<p>{text}</p>")); }
}
Composite
Compose objects into tree structures to represent part-whole hierarchies. Clients treat individual objects and compositions uniformly through a shared interface.
// JavaScript
class File {
constructor(name, bytes) { this.name = name; this.bytes = bytes; }
size() { return this.bytes; }
}
class Directory {
#children = [];
constructor(name) { this.name = name; }
add(child) { this.#children.push(child); return this; }
size() { return this.#children.reduce((t, c) => t + c.size(), 0); }
}
const root = new Directory("root");
root.add(new Directory("src").add(new File("index.js", 1200)).add(new File("utils.js", 800)))
.add(new File("README.md", 340));
console.log(root.size()); // 2340
# Python
@dataclass
class File:
name: str; bytes: int
def size(self): return self.bytes
@dataclass
class Directory:
name: str; _children: list = field(default_factory=list)
def add(self, child): self._children.append(child); return self
def size(self): return sum(c.size() for c in self._children)
// Rust — recursive enum with Box to keep the size known at compile time
enum FileSystem {
File { name: String, bytes: u64 },
Dir { name: String, children: Vec<FileSystem> },
}
impl FileSystem {
fn size(&self) -> u64 {
match self {
Self::File { bytes, .. } => *bytes,
Self::Dir { children, .. } => children.iter().map(|c| c.size()).sum(),
}
}
}
Decorator
Attach additional responsibilities to an object dynamically, without subclassing. Cross-cutting concerns like logging, auth, and rate limiting are natural candidates, since you can stack them in any combination.
// JavaScript — decorators as higher-order functions
function withLogging(fn) {
return (req) => {
console.log(`${req.method} ${req.path}`);
return fn(req);
};
}
function withAuth(fn) {
return (req) => {
if (!req.token) return { status: 401, body: "Unauthorized" };
return fn(req);
};
}
const secureHandler = withLogging(withAuth(handler));
# Python — @decorator syntax is the Decorator pattern built into the language
@log
@auth
@rate_limit
def secure_handler(req: dict) -> dict:
return handler(req)
# Equivalent to: secure_handler = log(auth(rate_limit(handler)))
// Rust — generic wrapper structs
struct LoggingHandler<T: Handler> { inner: T }
impl<T: Handler> Handler for LoggingHandler<T> {
fn handle(&self, req: &Request) -> Response {
println!("GET {}", req.path);
self.inner.handle(req)
}
}
struct AuthHandler<T: Handler> { inner: T }
impl<T: Handler> Handler for AuthHandler<T> {
fn handle(&self, req: &Request) -> Response {
if req.token.is_none() {
return Response { status: 401, body: "Unauthorized".into() };
}
self.inner.handle(req)
}
}
let h = LoggingHandler { inner: AuthHandler { inner: BaseHandler } };
Facade
Provide a simple interface to a complex subsystem. Callers should not need to know that transcoding a video involves codec detection, resolution scaling, audio normalization, and encoding as separate steps.
// JavaScript
class VideoTranscoder {
#codec = new CodecDetector();
#scaler = new ResolutionScaler();
#audio = new AudioNormalizer();
#encoder = new Encoder();
transcode(inputPath, outputPath, { width = 1920 } = {}) {
const codec = this.#codec.detect(inputPath);
const videoSpec = this.#scaler.scale(codec, width);
const lufs = this.#audio.normalize(inputPath);
const ok = this.#encoder.encode(videoSpec, lufs, outputPath);
return { success: ok, outputPath };
}
}
// Caller sees one method.
const result = new VideoTranscoder().transcode("input.mov", "output.mp4", { width: 1280 });
# Python
class VideoTranscoder:
def transcode(self, input_path, output_path, width=1920):
codec = CodecDetector().detect(input_path)
video = ResolutionScaler().scale(codec, width)
lufs = AudioNormalizer().normalize(input_path)
ok = Encoder().encode(video, lufs, output_path)
return {"success": ok, "output": output_path}
// Rust
pub struct VideoTranscoder;
impl VideoTranscoder {
pub fn transcode(&self, input: &Path, output: &Path, width: u32) -> Result<()> {
let codec = CodecDetector::detect(input)?;
let video_spec = ResolutionScaler::scale(&codec, width)?;
let lufs = AudioNormalizer::normalize(input)?;
Encoder::encode(&video_spec, lufs, output)
}
}
Flyweight
Share fine-grained objects to save memory. Separate intrinsic state (immutable, shareable) from extrinsic state (unique per use). Classic example: a text document with 10,000 characters but only a handful of distinct styles.
// JavaScript
class StyleCache {
#cache = new Map();
get(font, size, color) {
const key = `${font}|${size}|${color}`;
if (!this.#cache.has(key)) this.#cache.set(key, { font, size, color });
return this.#cache.get(key);
}
}
const cache = new StyleCache();
const normal = cache.get("Arial", 14, "#000");
const bold = cache.get("Arial-Bold", 14, "#000");
// 10,000 characters, but only 2 style objects in memory.
const chars = Array.from({ length: 10_000 }, (_, i) => ({
glyph: "a", x: i % 80, y: Math.floor(i / 80),
style: i % 10 === 0 ? bold : normal,
}));
# Python — @dataclass(frozen=True) makes instances hashable and safely shareable
@dataclass(frozen=True)
class Style:
font: str; size: int; color: str
class StyleCache:
def __init__(self): self._cache = {}
def get(self, font, size, color):
key = (font, size, color)
if key not in self._cache: self._cache[key] = Style(font, size, color)
return self._cache[key]
// Rust — Arc<Style> shares ownership across many Character structs at no copy cost
use std::sync::Arc;
use std::collections::HashMap;
struct Style { font: String, size: u32, color: String }
struct StyleCache { cache: HashMap<String, Arc<Style>> }
impl StyleCache {
fn get(&mut self, font: &str, size: u32, color: &str) -> Arc<Style> {
let key = format!("{font}|{size}|{color}");
self.cache.entry(key).or_insert_with(|| Arc::new(Style {
font: font.to_string(), size, color: color.to_string()
})).clone()
}
}
Proxy
Provide a surrogate for another object to control access to it. Common variants: caching proxy (avoid redundant work), virtual proxy (lazy initialization), protection proxy (access control).
// JavaScript — caching proxy, same interface as the real service
class CachingProxy {
#service; #cache = new Map();
constructor(service) { this.#service = service; }
async fetch(id) {
if (this.#cache.has(id)) return this.#cache.get(id);
const result = await this.#service.fetch(id);
this.#cache.set(id, result);
return result;
}
}
// Native ES6 Proxy: intercepting without a wrapper class
function createCachingProxy(service) {
const cache = new Map();
return new Proxy(service, {
get(target, prop) {
const fn = target[prop];
if (typeof fn !== "function") return fn;
return async (...args) => {
const key = `${prop}:${JSON.stringify(args)}`;
if (cache.has(key)) return cache.get(key);
const result = await fn.apply(target, args);
cache.set(key, result);
return result;
};
},
});
}
# Python
class CachingProxy:
def __init__(self, service): self._service = service; self._cache = {}
def fetch(self, id):
if id not in self._cache:
self._cache[id] = self._service.fetch(id)
return self._cache[id]
// Rust
struct CachingProxy<S: DataService> {
service: S,
cache: HashMap<u64, Data>,
}
impl<S: DataService> DataService for CachingProxy<S> {
fn fetch(&mut self, id: u64) -> Data {
if let Some(cached) = self.cache.get(&id) {
return cached.clone();
}
let result = self.service.fetch(id);
self.cache.insert(id, result.clone());
result
}
}
Behavioral Patterns
Behavioral patterns are about communication between objects. They describe how responsibilities get distributed, how objects talk to each other, and how algorithms get encapsulated so they can vary independently from the clients that use them.
Chain of Responsibility
Pass a request along a chain of handlers. Each handler decides to process it or forward it. The sender does not know which handler will ultimately respond. HTTP middleware pipelines are the textbook application.
// JavaScript
const logger = (req, next) => { console.log(req.path); return next(); };
const auth = (req, next) => {
if (!req.headers?.authorization) return { status: 401, body: "Unauthorized" };
return next();
};
const handler = (req) => ({ status: 200, body: `Hello from ${req.path}` });
function createPipeline(...fns) {
return (req, i = 0) => {
if (i >= fns.length) return { status: 404 };
return fns[i](req, () => createPipeline(...fns)(req, i + 1));
};
}
const pipeline = createPipeline(logger, auth, handler);
# Python
Handler = Callable[[Request], Response]
Middleware = Callable[[Request, Handler], Response]
def logger(req: Request, next_: Handler) -> Response:
print(f"{req.method} {req.path}")
return next_(req)
def auth(req: Request, next_: Handler) -> Response:
if not req.headers.get("authorization"):
return Response(401, "Unauthorized")
return next_(req)
def build_pipeline(*middlewares: Middleware, final: Handler) -> Handler:
def call(req, idx=0):
if idx >= len(middlewares): return final(req)
return middlewares[idx](req, lambda r: call(r, idx + 1))
return call
// Rust — closures as handlers in a Vec
type Handler = Box<dyn Fn(&Request) -> Option<Response>>;
fn build_pipeline(handlers: Vec<Handler>) -> impl Fn(&Request) -> Response {
move |req| {
for h in &handlers {
if let Some(resp) = h(req) { return resp; }
}
Response { status: 404, body: "Not found".into() }
}
}
Command
Encapsulate a request as an object. Decouples the invoker from the receiver, enables undo/redo, and lets you queue or log operations.
// JavaScript
class Editor {
text = ""; history = [];
execute(cmd) { cmd.execute(); this.history.push(cmd); }
undo() { this.history.pop()?.undo(); }
}
class InsertCommand {
constructor(editor, pos, chars) { Object.assign(this, { editor, pos, chars }); }
execute() {
const t = this.editor.text;
this.editor.text = t.slice(0, this.pos) + this.chars + t.slice(this.pos);
}
undo() {
const t = this.editor.text;
this.editor.text = t.slice(0, this.pos) + t.slice(this.pos + this.chars.length);
}
}
const e = new Editor();
e.execute(new InsertCommand(e, 0, "Hello, world!"));
console.log(e.text); // "Hello, world!"
e.undo();
console.log(e.text); // ""
# Python
class Editor:
def __init__(self): self.text = ""; self._history = []
def execute(self, cmd): cmd.execute(); self._history.append(cmd)
def undo(self):
if self._history: self._history.pop().undo()
class InsertCommand:
def __init__(self, editor, pos, chars): self._e, self._pos, self._chars = editor, pos, chars
def execute(self):
t = self._e.text
self._e.text = t[:self._pos] + self._chars + t[self._pos:]
def undo(self):
t = self._e.text
self._e.text = t[:self._pos] + t[self._pos + len(self._chars):]
// Rust
trait Command { fn execute(&mut self); fn undo(&mut self); }
struct Editor { pub text: String }
struct InsertCommand<'a> { editor: &'a mut Editor, pos: usize, chars: String }
impl<'a> Command for InsertCommand<'a> {
fn execute(&mut self) {
self.editor.text.insert_str(self.pos, &self.chars);
}
fn undo(&mut self) {
self.editor.text.drain(self.pos..self.pos + self.chars.len());
}
}
Iterator
Provide sequential access to elements of a collection without exposing its underlying representation. In all three languages, generators make this feel natural for lazy or paginated data.
// JavaScript — async generator for paginated API
async function* paginate(url) {
let nextUrl = url;
while (nextUrl) {
const { items, nextPageUrl } = await fetch(nextUrl).then(r => r.json());
yield items;
nextUrl = nextPageUrl ?? null;
}
}
for await (const page of paginate("https://api.example.com/orders?limit=50")) {
for (const order of page) console.log(order.id);
}
# Python — generator function
def paginate(start_url: str):
url = start_url
while url:
page = fetch_page(url)
yield page.items
url = page.next_url
for page in paginate("https://api.example.com/orders?limit=50"):
for order in page: print(order["id"])
// Rust — implementing Iterator manually on a struct
struct Paginator { current_url: Option<String> }
impl Iterator for Paginator {
type Item = Vec<String>;
fn next(&mut self) -> Option<Self::Item> {
let url = self.current_url.take()?;
let page = fetch_page(&url); // returns Page { items, next_url }
self.current_url = page.next_url;
Some(page.items)
}
}
Mediator
Define an object that encapsulates how a set of objects interact. Instead of components talking to each other directly, all communication goes through the mediator. This reduces n-to-n relationships to a star topology.
// JavaScript — chat room as mediator
class ChatRoom {
#users = new Map();
join(user) { this.#users.set(user.name, user); user.room = this; }
send(msg, sender) {
for (const user of this.#users.values()) {
if (user !== sender) user.receive(msg, sender.name);
}
}
}
class User {
constructor(name) { this.name = name; }
say(msg) { this.room.send(msg, this); }
receive(msg, from) { console.log(`[${this.name}] ${from}: ${msg}`); }
}
const room = new ChatRoom();
const alice = new User("Alice"); const bob = new User("Bob");
room.join(alice); room.join(bob);
alice.say("Hey!"); // [Bob] Alice: Hey!
# Python
class ChatRoom:
def __init__(self): self._users: dict[str, "User"] = {}
def join(self, user): self._users[user.name] = user; user.room = self
def send(self, msg, sender):
for u in self._users.values():
if u is not sender: u.receive(msg, sender.name)
class User:
def __init__(self, name): self.name = name; self.room = None
def say(self, msg): self.room.send(msg, self)
def receive(self, msg, from_): print(f"[{self.name}] {from_}: {msg}")
// Rust — message passing via channels (idiomatic async alternative)
use std::sync::{Arc, Mutex};
struct ChatRoom { users: Vec<Arc<Mutex<User>>> }
impl ChatRoom {
fn broadcast(&self, message: &str, from: &str) {
for user in &self.users {
let u = user.lock().unwrap();
if u.name != from { println!("[{}] {}: {}", u.name, from, message); }
}
}
}
Memento
Capture an object’s internal state externally so it can be restored later, without exposing internals. The object controls what gets saved; callers only see an opaque snapshot. Undo/redo is the obvious use case.
// JavaScript — plain object as memento, no extra class needed
class Editor {
#text = ""; #cursor = 0; #history = [];
insert(chars) {
this.#history.push({ text: this.#text, cursor: this.#cursor });
this.#text = this.#text.slice(0, this.#cursor) + chars + this.#text.slice(this.#cursor);
this.#cursor += chars.length;
}
undo() {
const snap = this.#history.pop();
if (snap) { this.#text = snap.text; this.#cursor = snap.cursor; }
}
get text() { return this.#text; }
}
const e = new Editor();
e.insert("Hello"); e.insert(", world!"); e.undo();
console.log(e.text); // "Hello"
# Python
@dataclass(frozen=True)
class EditorSnapshot: text: str; cursor: int
class Editor:
def __init__(self): self._text = ""; self._cursor = 0; self._history = []
def insert(self, chars):
self._history.append(EditorSnapshot(self._text, self._cursor))
self._text = self._text[:self._cursor] + chars + self._text[self._cursor:]
self._cursor += len(chars)
def undo(self):
if self._history:
snap = self._history.pop()
self._text, self._cursor = snap.text, snap.cursor
// Rust
struct EditorSnapshot { text: String, cursor: usize }
struct Editor { text: String, cursor: usize, history: Vec<EditorSnapshot> }
impl Editor {
fn insert(&mut self, chars: &str) {
self.history.push(EditorSnapshot { text: self.text.clone(), cursor: self.cursor });
self.text.insert_str(self.cursor, chars);
self.cursor += chars.len();
}
fn undo(&mut self) {
if let Some(snap) = self.history.pop() {
self.text = snap.text; self.cursor = snap.cursor;
}
}
}
Observer
Define a one-to-many dependency so that when one object changes state, all its dependents are notified automatically. Used for event systems, reactive UIs, and live data feeds.
// JavaScript
class EventEmitter {
#listeners = new Map();
on(event, fn) {
if (!this.#listeners.has(event)) this.#listeners.set(event, new Set());
this.#listeners.get(event).add(fn);
return () => this.#listeners.get(event).delete(fn); // unsubscribe
}
emit(event, data) {
for (const fn of this.#listeners.get(event) ?? []) fn(data);
}
}
class StockFeed extends EventEmitter {
update(ticker, price) { this.emit("price", { ticker, price }); }
}
const feed = new StockFeed();
feed.on("price", ({ ticker, price }) => console.log(`${ticker}: $${price}`));
feed.update("AAPL", 178.5); // AAPL: $178.5
# Python
from collections import defaultdict
from typing import Callable
class EventEmitter:
def __init__(self): self._listeners = defaultdict(list)
def on(self, event, fn):
self._listeners[event].append(fn)
return lambda: self._listeners[event].remove(fn)
def emit(self, event, data=None):
for fn in list(self._listeners[event]): fn(data)
// Rust — synchronous observer list; for async use tokio::sync::broadcast
struct StockFeed { listeners: Vec<Box<dyn Fn(f64) + Send>> }
impl StockFeed {
fn subscribe(&mut self, f: impl Fn(f64) + Send + 'static) {
self.listeners.push(Box::new(f));
}
fn update(&self, price: f64) {
for f in &self.listeners { f(price); }
}
}
State
Allow an object to alter its behavior when its internal state changes. Each state is an object that handles the transitions it cares about. This removes the large switch statement keyed on a status field.
// JavaScript — vending machine
class IdleState {
insertCoin(machine, amount) {
machine.setState(new HasCoinState(amount));
}
selectProduct() { console.log("Insert a coin first."); }
}
class HasCoinState {
constructor(coins) { this.coins = coins; }
insertCoin(machine, amount) { this.coins += amount; }
selectProduct(machine, product) {
if (product.price > this.coins) { console.log("Not enough."); return; }
console.log(`Dispensing ${product.name}...`);
machine.setState(new IdleState());
}
}
class VendingMachine {
#state = new IdleState();
setState(s) { this.#state = s; }
insertCoin(amount) { this.#state.insertCoin(this, amount); }
selectProduct(p) { this.#state.selectProduct(this, p); }
}
# Python
from abc import ABC, abstractmethod
class State(ABC):
@abstractmethod
def insert_coin(self, machine, amount: float) -> None: ...
@abstractmethod
def select_product(self, machine, product) -> None: ...
class IdleState(State):
def insert_coin(self, machine, amount): machine.state = HasCoinState(amount)
def select_product(self, machine, _): print("Insert a coin first.")
class HasCoinState(State):
def __init__(self, coins): self.coins = coins
def insert_coin(self, machine, amount): self.coins += amount
def select_product(self, machine, product):
if product.price > self.coins: print("Not enough."); return
print(f"Dispensing {product.name}...")
machine.state = IdleState()
// Rust — enum per state, match on transitions
enum MachineState { Idle, HasCoin(f64), Dispensing }
struct VendingMachine { state: MachineState }
impl VendingMachine {
fn insert_coin(&mut self, amount: f64) {
self.state = match self.state {
MachineState::Idle => MachineState::HasCoin(amount),
MachineState::HasCoin(c) => MachineState::HasCoin(c + amount),
_ => { println!("Please wait."); return; }
};
}
}
Strategy
Define a family of algorithms, encapsulate each one, and make them interchangeable. The caller picks which algorithm to use at runtime without touching the surrounding code.
// JavaScript — first-class functions make formal Strategy classes unnecessary
const byPrice = (a, b) => a.price - b.price;
const byDate = (a, b) => new Date(a.date) - new Date(b.date);
const byName = (a, b) => a.name.localeCompare(b.name);
const strategies = { price: byPrice, date: byDate, name: byName };
function sortOrders(orders, strategy) {
return [...orders].sort(strategies[strategy]);
}
sortOrders(orders, "price"); // swap algorithm by changing one string
# Python — key functions as strategies, no class needed
by_price = lambda o: o.price
by_date = lambda o: o.date
by_name = lambda o: o.name.lower()
strategies = {"price": by_price, "date": by_date, "name": by_name}
def sort_orders(orders, strategy: str):
return sorted(orders, key=strategies[strategy])
// Rust — function pointers or closures as strategies
fn sort_orders<F>(orders: &mut Vec<Order>, compare: F)
where F: Fn(&Order, &Order) -> Ordering {
orders.sort_by(compare);
}
sort_orders(&mut orders, |a, b| a.price.partial_cmp(&b.price).unwrap());
sort_orders(&mut orders, |a, b| a.name.cmp(&b.name));
Template Method
Define the skeleton of an algorithm in a base class, deferring one or more steps to subclasses. The overall structure is fixed; subclasses fill in the variable parts.
// JavaScript
class ReportGenerator {
generate(data) {
const filtered = this.filter(data); // optional hook
const output = this.format(filtered); // required, must override
this.write(output); // default: console.log
}
filter(data) { return data; }
write(output) { console.log(output); }
format() { throw new Error("Implement format()"); }
}
class CsvReport extends ReportGenerator {
format(data) {
const header = Object.keys(data[0]).join(",");
return [header, ...data.map(r => Object.values(r).join(","))].join("\n");
}
}
# Python
from abc import ABC, abstractmethod
class ReportGenerator(ABC):
def generate(self, data):
filtered = self.filter_data(data)
output = self.format_data(filtered)
self.write(output)
def filter_data(self, data): return data
def write(self, output): print(output)
@abstractmethod
def format_data(self, data) -> str: ...
class CsvReport(ReportGenerator):
def format_data(self, data) -> str:
import csv, io
buf = io.StringIO()
w = csv.DictWriter(buf, fieldnames=data[0].keys())
w.writeheader(); w.writerows(data)
return buf.getvalue()
// Rust — trait with default methods for optional steps
trait ReportGenerator {
fn filter_data<'a>(&self, data: &'a [Row]) -> &'a [Row] { data }
fn format_data(&self, data: &[Row]) -> String;
fn write(&self, output: &str) { println!("{output}"); }
fn generate(&self, data: &[Row]) {
let filtered = self.filter_data(data);
let output = self.format_data(filtered);
self.write(&output);
}
}
struct CsvReport;
impl ReportGenerator for CsvReport {
fn format_data(&self, data: &[Row]) -> String {
// build CSV string
data.iter().map(|r| r.values().cloned().collect::<Vec<_>>().join(","))
.collect::<Vec<_>>().join("\n")
}
}
Visitor
Represent an operation to be performed on elements of an object structure without modifying their classes. Adding a new operation means adding a new visitor, not editing the node types.
// JavaScript — evaluator and printer as visitor objects
class NumberNode { constructor(v) { this.value = v; } accept(v) { return v.visitNumber(this); } }
class AddNode { constructor(l, r) { this.left = l; this.right = r; } accept(v) { return v.visitAdd(this); } }
const evaluator = {
visitNumber: n => n.value,
visitAdd: n => n.left.accept(evaluator) + n.right.accept(evaluator),
};
const printer = {
visitNumber: n => `${n.value}`,
visitAdd: n => `(${n.left.accept(printer)} + ${n.right.accept(printer)})`,
};
const ast = new AddNode(new NumberNode(2), new NumberNode(3));
console.log(ast.accept(printer)); // (2 + 3)
console.log(ast.accept(evaluator)); // 5
# Python — singledispatch: operations live in visitor functions, not node classes
from functools import singledispatch
from dataclasses import dataclass
@dataclass class Number: value: float
@dataclass class Add: left: object; right: object
@singledispatch
def evaluate(node): raise TypeError(f"Unknown node: {type(node)}")
@evaluate.register
def _(node: Number): return node.value
@evaluate.register
def _(node: Add): return evaluate(node.left) + evaluate(node.right)
@singledispatch
def pretty_print(node): raise TypeError(f"Unknown node: {type(node)}")
@pretty_print.register
def _(node: Number): return str(node.value)
@pretty_print.register
def _(node: Add): return f"({pretty_print(node.left)} + {pretty_print(node.right)})"
// Rust — match on enum variants; adding a new operation = new function, no node changes
enum Expr {
Number(f64),
Add(Box<Expr>, Box<Expr>),
Multiply(Box<Expr>, Box<Expr>),
}
fn evaluate(expr: &Expr) -> f64 {
match expr {
Expr::Number(v) => *v,
Expr::Add(l, r) => evaluate(l) + evaluate(r),
Expr::Multiply(l, r) => evaluate(l) * evaluate(r),
}
}
fn pretty_print(expr: &Expr) -> String {
match expr {
Expr::Number(v) => v.to_string(),
Expr::Add(l, r) => format!("({} + {})", pretty_print(l), pretty_print(r)),
Expr::Multiply(l, r) => format!("({} * {})", pretty_print(l), pretty_print(r)),
}
}
Recap
Running the same structural ideas through JavaScript, Python, and Rust reveals a few things.
Dynamic languages can express patterns more concisely. JavaScript’s first-class functions eliminate the need for Strategy classes. Python’s @decorator syntax IS the Decorator pattern. Module-level singletons in both languages are simpler than any class-based approach.
Static type systems make implicit contracts explicit. In Python, Protocol turns duck typing into something the type checker can verify. In Rust, traits do the same at compile time. Patterns like Liskov Substitution that can only be verified at runtime in JavaScript become compiler guarantees in Rust.
Rust’s type system prevents some pattern violations outright. There is no class-based inheritance that could produce the Rectangle/Square trap. The Interface Segregation violation (a type forced to implement methods it cannot support) cannot compile. Dependency Inversion via generics costs nothing at runtime.
The patterns themselves are language-agnostic. Whether you are writing an HTTP middleware chain in Node, a Protocol-typed service layer in Python, or a trait-bounded generic in Rust, you are solving the same structural problem. The vocabulary is shared. Once you know what Chain of Responsibility looks like in one language, recognizing it in another is straightforward.
The full examples, including edge cases and modern alternatives, live in the design-patterns-for-web-developer repository.