Senior Engineer's Guide
From OOP fundamentals to battle-tested patterns โ taught with real-world examples by a senior engineer who's actually shipped these in production.
The four pillars of OOP are the grammar of software design. Master these and patterns become obvious.
// โ Bad: Everything is public. Anyone can mess with balance directly. class BadBankAccount { balance: number = 0; pin: string = "1234"; // ๐ exposed PIN! } // โ Good: Encapsulate state. Only expose safe operations. class BankAccount { private balance: number; private pin: string; private transactionHistory: string[] = []; constructor(initialBalance: number, pin: string) { this.balance = initialBalance; this.pin = pin; } deposit(amount: number): void { if (amount <= 0) throw new Error("Deposit must be positive"); this.balance += amount; this.logTransaction(`Deposited โน${amount}`); } withdraw(amount: number, pin: string): void { if (pin !== this.pin) throw new Error("Wrong PIN"); if (amount > this.balance) throw new Error("Insufficient funds"); this.balance -= amount; this.logTransaction(`Withdrew โน${amount}`); } getBalance(): number { return this.balance; // read-only access } private logTransaction(msg: string): void { this.transactionHistory.push(`[${new Date().toISOString()}] ${msg}`); } } const account = new BankAccount(10000, "9876"); account.deposit(5000); account.withdraw(2000, "9876"); console.log(account.getBalance()); // 13000 // account.pin โ TS Error: 'pin' is private โ
// Abstract class defines the "what" โ enforces structure abstract class PaymentProcessor { // Template method โ orchestrates the algorithm processPayment(amount: number): boolean { this.validateAmount(amount); const authorized = this.authorize(amount); if (authorized) this.chargeCustomer(amount); this.sendReceipt(amount); return authorized; } private validateAmount(amount: number): void { if (amount <= 0) throw new Error("Invalid amount"); } // The "how" is deferred to subclasses protected abstract authorize(amount: number): boolean; protected abstract chargeCustomer(amount: number): void; protected abstract sendReceipt(amount: number): void; } class RazorpayProcessor extends PaymentProcessor { protected authorize(amount: number): boolean { console.log(`Razorpay: Authorizing โน${amount}...`); return true; // calls Razorpay API internally } protected chargeCustomer(amount: number): void { console.log(`Razorpay: Charging โน${amount}`); } protected sendReceipt(amount: number): void { console.log(`Razorpay: SMS receipt for โน${amount}`); } } const processor = new RazorpayProcessor(); processor.processPayment(1999); // caller doesn't care HOW it works
class Notification { constructor( protected title: string, protected body: string, private readonly timestamp: Date = new Date() ) {} format(): string { return `[${this.timestamp.toLocaleTimeString()}] ${this.title}: ${this.body}`; } send(): void { console.log(`Sending: ${this.format()}`); } } class PushNotification extends Notification { constructor(title: string, body: string, private deviceToken: string) { super(title, body); // must call parent constructor } send(): void { console.log(`[PUSH โ ${this.deviceToken}] ${this.format()}`); // calls FCM / APNS here... } } class EmailNotification extends Notification { constructor(title: string, body: string, private email: string) { super(title, body); } send(): void { console.log(`[EMAIL โ ${this.email}] ${this.format()}`); // calls SendGrid / SES here... } } // Polymorphism in action โ same interface, different behavior const notifications: Notification[] = [ new PushNotification("Order Shipped", "Your order is on the way!", "tok_abc123"), new EmailNotification("Order Shipped", "Your order is on the way!", "user@example.com"), ]; notifications.forEach(n => n.send());
// Interface = contract (the "play button") interface StorageProvider { upload(file: File): Promise<string>; download(key: string): Promise<Buffer>; delete(key: string): Promise<void>; } class S3Provider implements StorageProvider { async upload(file: File): Promise<string> { console.log("Uploading to AWS S3..."); return `s3://my-bucket/${file.name}`; } async download(key: string): Promise<Buffer> { return Buffer.from(""); } async delete(key: string): Promise<void> { console.log(`S3: Deleted ${key}`); } } class GCSProvider implements StorageProvider { async upload(file: File): Promise<string> { console.log("Uploading to Google Cloud Storage..."); return `gs://my-bucket/${file.name}`; } async download(key: string): Promise<Buffer> { return Buffer.from(""); } async delete(key: string): Promise<void> { console.log(`GCS: Deleted ${key}`); } } // FileService doesn't know or care WHICH provider it gets class FileService { constructor(private storage: StorageProvider) {} async saveUserAvatar(file: File): Promise<string> { console.log("Validating file type..."); return this.storage.upload(file); // polymorphic call } } // Swap providers without touching FileService const devService = new FileService(new GCSProvider()); const prodService = new FileService(new S3Provider());
Creational patterns control object creation โ making systems independent of how their objects are created, composed, and represented.
class DatabaseConnection { private static instance: DatabaseConnection | null = null; private connectionCount = 0; // Private constructor โ no one can call `new DatabaseConnection()` private constructor(private connectionString: string) { console.log(`๐ DB connected: ${this.connectionString}`); } // The ONLY way to get a DatabaseConnection static getInstance(connectionString: string): DatabaseConnection { if (!DatabaseConnection.instance) { DatabaseConnection.instance = new DatabaseConnection(connectionString); } return DatabaseConnection.instance; } query(sql: string): string { this.connectionCount++; return `Result of: ${sql} (query #${this.connectionCount})`; } } // Across your entire app, these all return the SAME instance const db1 = DatabaseConnection.getInstance("postgres://localhost/mydb"); const db2 = DatabaseConnection.getInstance("postgres://localhost/mydb"); console.log(db1 === db2); // true โ db1.query("SELECT * FROM users"); db2.query("SELECT * FROM orders"); // query #2 on same connection
tsyringe.
createTransport(). For local orders it creates Trucks. For international orders it creates Ships. For urgent ones, Planes. Callers just call createTransport().
interface Logger { log(message: string): void; error(message: string): void; } class ConsoleLogger implements Logger { log(msg: string) { console.log(`[CONSOLE] โน ${msg}`); } error(msg: string) { console.error(`[CONSOLE] โ ${msg}`); } } class FileLogger implements Logger { log(msg: string) { console.log(`[FILE] Writing: ${msg}`); } error(msg: string) { console.log(`[FILE] Error: ${msg}`); } } class CloudLogger implements Logger { log(msg: string) { console.log(`[DATADOG] Sending metric: ${msg}`); } error(msg: string) { console.log(`[DATADOG] Alert: ${msg}`); } } // Factory โ single place to decide which logger to create type LoggerType = "console" | "file" | "cloud"; function createLogger(type: LoggerType): Logger { const factories: Record<LoggerType, () => Logger> = { console: () => new ConsoleLogger(), file: () => new FileLogger(), cloud: () => new CloudLogger(), }; return factories[type](); } // Config-driven โ switch logger without changing callers const env = process.env.NODE_ENV; const logger = createLogger(env === "production" ? "cloud" : "console"); logger.log("App started"); logger.error("Something went wrong");
interface HttpRequestConfig { url: string; method: string; headers: Record<string, string>; body?: unknown; timeout?: number; retries?: number; } class HttpRequestBuilder { private config: Partial<HttpRequestConfig> = { method: "GET", headers: {}, }; url(url: string): this { this.config.url = url; return this; // return this for chaining } method(method: "GET" | "POST" | "PUT" | "DELETE"): this { this.config.method = method; return this; } header(key: string, value: string): this { this.config.headers![key] = value; return this; } bearerToken(token: string): this { return this.header("Authorization", `Bearer ${token}`); } body(data: unknown): this { this.config.body = data; return this.header("Content-Type", "application/json"); } timeout(ms: number): this { this.config.timeout = ms; return this; } retries(count: number): this { this.config.retries = count; return this; } build(): HttpRequestConfig { if (!this.config.url) throw new Error("URL is required"); return this.config as HttpRequestConfig; } } // Readable, fluent API โ like writing English const request = new HttpRequestBuilder() .url("https://api.example.com/users") .method("POST") .bearerToken("eyJ...") .body({ name: "Ravi", email: "ravi@example.com" }) .timeout(5000) .retries(3) .build(); console.log(request);
Structural patterns deal with object composition โ creating relationships between objects to form larger structures.
// What YOUR system expects interface AnalyticsService { trackEvent(event: string, userId: string, props: object): void; } // What MIXPANEL actually provides (their SDK) class MixpanelSDK { track(eventName: string, distinctId: string, metadata: object): void { console.log(`[Mixpanel] ${eventName} for ${distinctId}:`, metadata); } } // What AMPLITUDE actually provides class AmplitudeSDK { logEvent(eventType: string, userIdentifier: string, eventProperties: object): void { console.log(`[Amplitude] ${eventType} for ${userIdentifier}:`, eventProperties); } } // ADAPTERS โ bridge the gap class MixpanelAdapter implements AnalyticsService { constructor(private mixpanel: MixpanelSDK) {} trackEvent(event: string, userId: string, props: object): void { // Translate YOUR interface โ Mixpanel's interface this.mixpanel.track(event, userId, props); } } class AmplitudeAdapter implements AnalyticsService { constructor(private amplitude: AmplitudeSDK) {} trackEvent(event: string, userId: string, props: object): void { this.amplitude.logEvent(event, userId, props); } } // Your app code never changes โ just swap the adapter class UserService { constructor(private analytics: AnalyticsService) {} onUserSignup(userId: string): void { // No Mixpanel/Amplitude knowledge here this.analytics.trackEvent("user_signed_up", userId, { source: "organic" }); } } const service = new UserService(new MixpanelAdapter(new MixpanelSDK())); service.onUserSignup("user_42");
interface DataSource { write(data: string): void; read(): string; } // Concrete component class FileDataSource implements DataSource { private data: string = ""; write(data: string): void { this.data = data; } read(): string { return this.data; } } // Base decorator โ wraps a DataSource abstract class DataSourceDecorator implements DataSource { constructor(protected wrapped: DataSource) {} write(data: string): void { this.wrapped.write(data); } read(): string { return this.wrapped.read(); } } // Concrete decorator 1: adds encryption class EncryptionDecorator extends DataSourceDecorator { write(data: string): void { const encrypted = Buffer.from(data).toString("base64"); console.log("๐ Encrypting..."); super.write(encrypted); } read(): string { const data = super.read(); console.log("๐ Decrypting..."); return Buffer.from(data, "base64").toString(); } } // Concrete decorator 2: adds compression class CompressionDecorator extends DataSourceDecorator { write(data: string): void { console.log("๐ Compressing..."); super.write(`[compressed:${data}]`); // simplified } read(): string { const raw = super.read(); console.log("๐ฆ Decompressing..."); return raw.replace(/\[compressed:(.*)\]/, "$1"); } } // Layer decorators โ file โ encrypt โ compress const source = new CompressionDecorator( new EncryptionDecorator( new FileDataSource() ) ); source.write("user sensitive data"); console.log(source.read()); // "user sensitive data"
// Complex subsystems class InventoryService { reserve(productId: string, qty: number): boolean { console.log(`Reserved ${qty}x ${productId}`); return true; } } class PaymentService { charge(userId: string, amount: number): boolean { console.log(`Charged โน${amount} to ${userId}`); return true; } } class ShippingService { schedule(orderId: string, address: string): string { console.log(`Scheduled delivery for ${orderId}`); return `TRACK-${Date.now()}`; } } class NotificationService { sendOrderConfirmation(userId: string, orderId: string): void { console.log(`๐ง Confirmation sent to ${userId} for ${orderId}`); } } // FACADE โ simple interface over the complexity class OrderFacade { private inventory = new InventoryService(); private payment = new PaymentService(); private shipping = new ShippingService(); private notifications = new NotificationService(); placeOrder(userId: string, productId: string, qty: number, address: string): string { const orderId = `ORD-${Date.now()}`; const amount = qty * 299; // simplified pricing if (!this.inventory.reserve(productId, qty)) throw new Error("Out of stock"); if (!this.payment.charge(userId, amount)) throw new Error("Payment failed"); const trackingId = this.shipping.schedule(orderId, address); this.notifications.sendOrderConfirmation(userId, orderId); return trackingId; } } // Client code is beautifully simple const orders = new OrderFacade(); const tracking = orders.placeOrder("user_1", "iPhone15", 1, "MG Road, Bengaluru"); console.log(`Track your order: ${tracking}`);
Behavioral patterns are about algorithms and assignment of responsibilities between objects.
type EventHandler<T> = (data: T) => void; class EventEmitter<Events extends Record<string, unknown>> { private listeners = new Map<string, Set<EventHandler<unknown>>>(); on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void { if (!this.listeners.has(event as string)) { this.listeners.set(event as string, new Set()); } this.listeners.get(event as string)!.add(handler as EventHandler<unknown>); } off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void { this.listeners.get(event as string)?.delete(handler as EventHandler<unknown>); } emit<K extends keyof Events>(event: K, data: Events[K]): void { this.listeners.get(event as string)?.forEach(handler => handler(data)); } } // Strongly typed events โ no magic strings interface OrderEvents { "order:created": { orderId: string; userId: string; amount: number }; "order:shipped": { orderId: string; trackingId: string }; "order:cancelled": { orderId: string; reason: string }; } const eventBus = new EventEmitter<OrderEvents>(); // Subscribers register independently eventBus.on("order:created", ({ orderId, amount }) => { console.log(`๐ง Email: Order ${orderId} confirmed for โน${amount}`); }); eventBus.on("order:created", ({ orderId, userId }) => { console.log(`๐ Analytics: New order ${orderId} from ${userId}`); }); eventBus.on("order:shipped", ({ orderId, trackingId }) => { console.log(`๐ฆ Push: Your order ${orderId} is shipped! Track: ${trackingId}`); }); // Publisher doesn't know WHO is listening eventBus.emit("order:created", { orderId: "ORD-1", userId: "u_42", amount: 1299 });
// Strategy interface interface DiscountStrategy { calculate(price: number): number; readonly name: string; } // Concrete strategies class NoDiscount implements DiscountStrategy { readonly name = "No Discount"; calculate(price: number): number { return price; } } class PercentageDiscount implements DiscountStrategy { readonly name: string; constructor(private percent: number) { this.name = `${percent}% Off`; } calculate(price: number): number { return price * (1 - this.percent / 100); } } class BuyOneGetOneFree implements DiscountStrategy { readonly name = "Buy 1 Get 1 Free"; calculate(price: number): number { return price / 2; // half price per unit when buying 2 } } class FlatDiscount implements DiscountStrategy { readonly name: string; constructor(private amount: number) { this.name = `โน${amount} Flat Off`; } calculate(price: number): number { return Math.max(0, price - this.amount); } } // Context โ uses a strategy class ShoppingCart { private strategy: DiscountStrategy = new NoDiscount(); setDiscountStrategy(strategy: DiscountStrategy): void { this.strategy = strategy; } checkout(price: number): void { const final = this.strategy.calculate(price); console.log(`[${this.strategy.name}] โน${price} โ โน${final.toFixed(2)}`); } } const cart = new ShoppingCart(); // Swap strategies at runtime based on user segment cart.checkout(1000); // โน1000 cart.setDiscountStrategy(new PercentageDiscount(20)); cart.checkout(1000); // โน800 cart.setDiscountStrategy(new FlatDiscount(150)); cart.checkout(1000); // โน850 cart.setDiscountStrategy(new BuyOneGetOneFree()); cart.checkout(1000); // โน500
interface HttpRequest { path: string; method: string; headers: Record<string, string>; body?: unknown; user?: { id: string; role: string }; } type NextFn = () => void; abstract class Middleware { protected next: Middleware | null = null; setNext(middleware: Middleware): Middleware { this.next = middleware; return middleware; // allows chaining: a.setNext(b).setNext(c) } protected pass(req: HttpRequest): void { this.next?.handle(req); } abstract handle(req: HttpRequest): void; } class RateLimiterMiddleware extends Middleware { private requestCounts = new Map<string, number>(); handle(req: HttpRequest): void { const ip = req.headers["x-forwarded-for"] || "unknown"; const count = (this.requestCounts.get(ip) || 0) + 1; this.requestCounts.set(ip, count); if (count > 100) { console.log("๐ซ Rate limit exceeded"); return; } console.log("โ Rate limit OK"); this.pass(req); } } class AuthMiddleware extends Middleware { handle(req: HttpRequest): void { const token = req.headers["authorization"]; if (!token?.startsWith("Bearer ")) { console.log("๐ซ Unauthorized"); return; } req.user = { id: "user_1", role: "admin" }; // attach user to request console.log("โ Authenticated"); this.pass(req); } } class LoggingMiddleware extends Middleware { handle(req: HttpRequest): void { console.log(`๐ ${req.method} ${req.path} by ${req.user?.id}`); this.pass(req); } } class RouteHandler extends Middleware { handle(req: HttpRequest): void { console.log(`๐ฏ Handling ${req.path}...`); } } // Wire up the chain const rateLimiter = new RateLimiterMiddleware(); rateLimiter .setNext(new AuthMiddleware()) .setNext(new LoggingMiddleware()) .setNext(new RouteHandler()); // Every incoming request runs through the chain rateLimiter.handle({ path: "/api/orders", method: "GET", headers: { authorization: "Bearer eyJ...", "x-forwarded-for": "1.2.3.4" } });
A production-grade order processing system that uses every pattern we've covered. This is how real systems look.
An order processing system modelling Flipkart/Amazon's core flow: authentication, order creation, payment processing, notifications, and analytics โ all using design patterns.
// ============================================================ // OrderFlow Engine โ uses ALL patterns in one cohesive system // ============================================================ // โโ SINGLETON: App Config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ class AppConfig { private static instance: AppConfig; readonly env: string; readonly paymentGateway: string; readonly analyticsProvider: string; private constructor() { this.env = process.env.NODE_ENV || "development"; this.paymentGateway = process.env.PAYMENT_GW || "razorpay"; this.analyticsProvider = process.env.ANALYTICS || "mixpanel"; } static getInstance(): AppConfig { if (!AppConfig.instance) AppConfig.instance = new AppConfig(); return AppConfig.instance; } } // โโ BUILDER: Order โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ interface OrderItem { productId: string; qty: number; price: number; } interface Order { id: string; userId: string; items: OrderItem[]; shippingAddress: string; totalAmount: number; couponCode?: string; } class OrderBuilder { private order: Partial<Order> = { items: [] }; forUser(userId: string): this { this.order.userId = userId; return this; } addItem(item: OrderItem): this { this.order.items!.push(item); return this; } shipTo(address: string): this { this.order.shippingAddress = address; return this; } applyCoupon(code: string): this { this.order.couponCode = code; return this; } build(): Order { if (!this.order.userId || !this.order.shippingAddress) throw new Error("Order requires userId and shippingAddress"); if (this.order.items!.length === 0) throw new Error("Order must have at least one item"); this.order.id = `ORD-${Date.now()}`; this.order.totalAmount = this.order.items!.reduce((sum, i) => sum + i.price * i.qty, 0); return this.order as Order; } } // โโ FACTORY: Payment Gateway โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ interface PaymentGateway { charge(userId: string, amount: number): Promise<string>; } class RazorpayGateway implements PaymentGateway { async charge(userId: string, amount: number): Promise<string> { console.log(`[Razorpay] Charging โน${amount}`); return `rzp_${Date.now()}`; } } class StripeGateway implements PaymentGateway { async charge(userId: string, amount: number): Promise<string> { console.log(`[Stripe] Charging $${amount}`); return `pi_${Date.now()}`; } } function createPaymentGateway(type: string): PaymentGateway { if (type === "stripe") return new StripeGateway(); return new RazorpayGateway(); // default } // โโ OBSERVER: Event Bus โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ interface SystemEvents { "order:placed": Order; "payment:completed": { orderId: string; txnId: string }; "order:failed": { orderId: string; reason: string }; } class EventBus { private static instance = new EventBus(); // Singleton + Observer private subs = new Map<string, Function[]>(); static get() { return this.instance; } on<K extends keyof SystemEvents>(e: K, fn: (d: SystemEvents[K]) => void): void { (this.subs.get(e as string) || this.subs.set(e as string, []).get(e as string)!).push(fn); } emit<K extends keyof SystemEvents>(e: K, data: SystemEvents[K]): void { this.subs.get(e as string)?.forEach(fn => fn(data)); } } // โโ STRATEGY: Discounts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ interface DiscountStrategy { apply(amount: number): number; } class NoDiscount implements DiscountStrategy { apply(a: number) { return a; } } class CouponDiscount implements DiscountStrategy { private coupons: Record<string, number> = { SAVE10: 10, SALE20: 20, FLAT50: 50 }; constructor(private code: string) {} apply(amount: number): number { const pct = this.coupons[this.code] || 0; return amount * (1 - pct / 100); } } // โโ FACADE: Order Orchestrator โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ class OrderService { private config = AppConfig.getInstance(); private gateway = createPaymentGateway(this.config.paymentGateway); private events = EventBus.get(); async placeOrder(order: Order): Promise<void> { console.log(`\n๐ Processing order ${order.id}...`); // Strategy: pick discount const discount: DiscountStrategy = order.couponCode ? new CouponDiscount(order.couponCode) : new NoDiscount(); const finalAmount = discount.apply(order.totalAmount); try { // Factory: payment gateway handles the charge const txnId = await this.gateway.charge(order.userId, finalAmount); // Observer: broadcast success to all listeners this.events.emit("order:placed", order); this.events.emit("payment:completed", { orderId: order.id, txnId }); console.log(`โ Order ${order.id} complete! Txn: ${txnId}`); } catch (err: unknown) { const reason = err instanceof Error ? err.message : "Unknown error"; this.events.emit("order:failed", { orderId: order.id, reason }); throw err; } } } // โโ WIRE IT ALL TOGETHER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ async function main() { const bus = EventBus.get(); // Register event listeners (Observer) bus.on("order:placed", (order) => console.log(`๐ฆ Inventory: Reserve items for ${order.id}`)); bus.on("payment:completed", ({ orderId, txnId }) => console.log(`๐ง Email: Receipt for ${orderId}, txn ${txnId}`)); bus.on("payment:completed", ({ orderId }) => console.log(`๐ฑ Push: Your order ${orderId} is confirmed!`)); bus.on("order:failed", ({ orderId, reason }) => console.log(`๐จ Alert: Order ${orderId} failed: ${reason}`)); // Build an order (Builder) const order = new OrderBuilder() .forUser("user_ravi") .addItem({ productId: "LAPTOP-001", qty: 1, price: 75000 }) .addItem({ productId: "MOUSE-002", qty: 2, price: 1500 }) .shipTo("101 MG Road, Bengaluru - 560001") .applyCoupon("SAVE10") .build(); // Process via facade (Facade) const service = new OrderService(); await service.placeOrder(order); } main().catch(console.error);
The only way to truly internalize patterns is to implement them yourself. Start with Easy, work up to Hard.
Create a ConfigManager singleton that stores environment variables. It should have get(key) and set(key, value) methods. Verify that two instances always refer to the same underlying config object.
getInstance() method. Test it with ConfigManager.getInstance() === ConfigManager.getInstance().
Build a PizzaBuilder with methods: size("small"|"medium"|"large"), crust("thin"|"thick"), addTopping(name), extraCheese(), build(). The build() method should throw if no size is set.
private pizza: Partial<Pizza> object. Each method sets a field and returns this. build() validates and returns the completed object as Pizza.
Create a TransportFactory that creates Car, Bike, and Truck objects based on a string type. All implement a Transport interface with move(distance: number): string and getCostPerKm(): number. Add a TripPlanner class that accepts any Transport and calculates total trip cost.
Transport interface type. TripPlanner takes Transport in its constructor (dependency injection). This tests both Factory and Polymorphism.
Build a generic, strongly-typed EventEmitter<T> class. Define a ChatEvents interface with events: "message:sent", "user:joined", "user:left". Wire up 3 handlers for different events and prove type safety by attempting to emit a non-existent event (should give a TypeScript error).
keyof T to constrain event names. Store handlers as Map<string, Set<Function>>. The generic parameter K extends keyof T on both on() and emit() ensures type safety end-to-end.
You have an internal MessageSender interface with send(to: string, text: string): void. Two external SDKs: TwilioSDK (which uses createMessage(phone, body, from)) and MSG91SDK (which uses sendSMS(mobile, message, senderId)). Write adapters for both and a NotificationService that uses only MessageSender.
MessageSender and holds a reference to the SDK. The adapter's send() translates parameters and calls the SDK's native method.
Build a mini banking system combining: Singleton (database), Builder (create account/transaction), Factory (account types: savings, current, FD), Decorator (add features: overdraft protection, interest calculation), Observer (fraud alerts, statement emails on transactions), Strategy (interest calculation varies by account type), Chain of Responsibility (transaction validation: amount check โ balance check โ daily limit check โ fraud check).
โ HardBankingService. Build incrementally โ don't try to do it all at once!
Model a simplified Netflix-like system: Singleton (content catalog cache), Factory (create different content types: Movie, Series, Documentary), Builder (construct user watch sessions), Decorator (add subtitle support, HD quality, download capability to content), Strategy (recommendation algorithm: trending, personalized, genre-based), Observer (update watch history, send continue-watching reminders), Facade (StreamingService: one method to start playing given a contentId and userId).
โ HardContent interface should have play(userId), getMetadata(). Decorators add capabilities without changing the core interface. The RecommendationEngine takes a Strategy. The WatchSession is built by a Builder. The EventBus handles history and reminders.