System Design
1. System Prerequisites

Design Patterns

⏱ 0s active on this topic

What Are Design Patterns?

Design patterns are reusable solutions to commonly occurring problems in software design. They are not code you copy — they are blueprints you adapt.

The 23 classic patterns come from a 1994 book, Design Patterns, written by four authors — nicknamed the "Gang of Four" (often shortened to GoF). So whenever you see "GoF patterns", it just means "the 23 patterns from that book".

CategoryPatterns in this filePurpose
CreationalFactory, Singleton, BuilderHow objects are created
BehavioralObserver, StrategyHow objects communicate & behave


1. Factory Design Pattern

Category: Creational


What is Factory Pattern?

The Factory Pattern creates objects without exposing the instantiation logic to the caller. The caller asks for an object; the factory decides which class to instantiate.

Think of a vehicle factory. You say "give me a vehicle for city roads." The factory decides whether to give you a scooter, bicycle, or car. You don't know — or care — how it was built.


The Core Idea

Without Factory:   caller does  →  new EmailNotification()
                                   new SMSNotification()
                                   new PushNotification()
                                   (caller knows all classes, tightly coupled)

With Factory:      caller does  →  NotificationFactory.create("EMAIL")
                                   (caller knows nothing about concrete classes)

❌ Without Factory Pattern

class OrderService {
    public void notifyUser(String type, String userId, String message) {
        if (type.equals("EMAIL")) {
            EmailNotification n = new EmailNotification(); // tightly coupled
            n.send(userId, message);
        } else if (type.equals("SMS")) {
            SMSNotification n = new SMSNotification();     // tightly coupled
            n.send(userId, message);
        } else if (type.equals("PUSH")) {
            PushNotification n = new PushNotification();   // tightly coupled
            n.send(userId, message);
        }
        // Add WhatsApp? Modify OrderService — OCP violated
    }
}

Problems:

  • OrderService knows about every concrete notification class
  • Adding new type = modify OrderService — OCP violated
  • Hard to test — can't swap implementations

✅ With Factory Pattern

// Step 1: Define the interface (product)
interface Notification {
    void send(String userId, String message);
}

// Step 2: Concrete implementations (products)
class EmailNotification implements Notification {
    public void send(String userId, String message) {
        System.out.println("Sending EMAIL to " + userId + ": " + message);
    }
}

class SMSNotification implements Notification {
    public void send(String userId, String message) {
        System.out.println("Sending SMS to " + userId + ": " + message);
    }
}

class PushNotification implements Notification {
    public void send(String userId, String message) {
        System.out.println("Sending PUSH to " + userId + ": " + message);
    }
}

// Step 3: The Factory — creation logic lives here only
class NotificationFactory {
    public static Notification create(String type) {
        switch (type) {
            case "EMAIL": return new EmailNotification();
            case "SMS":   return new SMSNotification();
            case "PUSH":  return new PushNotification();
            default: throw new IllegalArgumentException("Unknown type: " + type);
        }
    }
}

// Step 4: Caller — clean, knows nothing about concrete classes
class OrderService {
    public void notifyUser(String type, String userId, String message) {
        Notification notification = NotificationFactory.create(type);
        notification.send(userId, message);
        // OrderService is unchanged when you add WhatsApp later
    }
}

Real-World System Design Examples

1. Database Connection Factory

interface DBConnection { void query(String sql); }

class MySQLConnection implements DBConnection { ... }
class PostgresConnection implements DBConnection { ... }
class MongoConnection implements DBConnection { ... }

class DBConnectionFactory {
    public static DBConnection create(String dbType) {
        switch (dbType) {
            case "MYSQL":    return new MySQLConnection();
            case "POSTGRES": return new PostgresConnection();
            case "MONGO":    return new MongoConnection();
        }
    }
}
// Read dbType from config — swap DB by changing config, not code

2. Payment Gateway Factory

PaymentGatewayFactory.create("RAZORPAY") → RazorpayGateway
PaymentGatewayFactory.create("STRIPE")   → StripeGateway
PaymentGatewayFactory.create("PAYTM")    → PaytmGateway

3. Logger Factory

LoggerFactory.create("FILE")    → FileLogger
LoggerFactory.create("CONSOLE") → ConsoleLogger
LoggerFactory.create("CLOUD")   → CloudWatchLogger

Interview Talking Point

"I use Factory when object creation logic is complex or the type of object isn't known until runtime. For a notification system, instead of if/else chains in OrderService, a NotificationFactory.create(type) centralises all creation logic. Add a new channel = add a class + one line in the factory. OrderService never changes. It also respects OCP — the factory is the only place that changes when you add new types."


Quick Revision

  1. Factory creates objects without the caller knowing the concrete class
  2. Creation logic lives in ONE place — the factory
  3. Caller depends on interface, not concrete classes
  4. Adding new type = new class + one line in factory (OCP respected)
  5. Used for: notification channels, DB connections, payment gateways, loggers


2. Singleton Design Pattern

Category: Creational


What is Singleton Pattern?

Singleton ensures only one instance of a class exists throughout the application's lifetime, and provides a global access point to that instance.

Think of a country's president. There is only one at a time. Everyone who asks "who is the president?" gets the same person.


The Core Idea

Normal class:   new DatabasePool() → new instance every time
Singleton:      DatabasePool.getInstance() → always the SAME instance

Use Singleton when having multiple instances would cause problems:

  • Multiple DB connection pools wasting resources
  • Multiple config loaders causing inconsistency
  • Multiple loggers writing to the same file simultaneously

Basic Implementation (Not Thread-Safe — Don't Use in Production)

class DatabaseConnectionPool {
    private static DatabaseConnectionPool instance;  // single instance stored here

    private DatabaseConnectionPool() {
        // private constructor — prevents new DatabaseConnectionPool() from outside
        System.out.println("Connection pool initialized");
    }

    public static DatabaseConnectionPool getInstance() {
        if (instance == null) {
            instance = new DatabaseConnectionPool();  // created only once
        }
        return instance;
    }

    public void executeQuery(String sql) {
        System.out.println("Executing: " + sql);
    }
}

// Usage
DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();
System.out.println(pool1 == pool2);  // true — same instance

✅ Thread-Safe Singleton (Use This in Production)

The basic version breaks in multi-threaded environments — two threads can both pass the if (instance == null) check and create two instances.

Option 1: Double-Checked Locking (Most Common in Interviews)

class DatabaseConnectionPool {
    // volatile ensures visibility across threads
    private static volatile DatabaseConnectionPool instance;

    private DatabaseConnectionPool() { }

    public static DatabaseConnectionPool getInstance() {
        if (instance == null) {                      // First check (no lock)
            synchronized (DatabaseConnectionPool.class) {
                if (instance == null) {              // Second check (with lock)
                    instance = new DatabaseConnectionPool();
                }
            }
        }
        return instance;
    }
}

Why double-check? First check avoids locking every time (performance). Second check inside the lock prevents duplicate creation by racing threads.

Option 2: Bill Pugh Singleton (Cleanest — Preferred)

class ConfigManager {
    private ConfigManager() { }

    // Inner static class — loaded only when getInstance() is called
    private static class SingletonHolder {
        private static final ConfigManager INSTANCE = new ConfigManager();
    }

    public static ConfigManager getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Thread-safe by JVM class-loading guarantee. No synchronized needed. Lazy-loaded.

Option 3: Enum Singleton (Safest — Prevents Reflection Attacks)

enum AppConfig {
    INSTANCE;

    private String dbUrl = "jdbc:mysql://localhost/mydb";

    public String getDbUrl() { return dbUrl; }
}

// Usage
AppConfig.INSTANCE.getDbUrl();

Enum guarantees one instance even under serialization and reflection.


Real-World System Design Examples

Where Singleton Is Used

1. Database Connection Pool     → one pool shared across all requests
2. Configuration Manager        → one source of truth for app config
3. Logger                       → one logger instance writing to one file
4. Cache Manager (in-process)   → one shared in-memory cache
5. Thread Pool                  → one pool of worker threads
6. Service Registry             → one registry for service discovery

Spring Framework — Everything is Singleton by Default

@Service  // Spring creates ONE instance, shared across the entire application
class PaymentService {
    @Autowired
    private PaymentRepository repo;  // also a singleton
}

In Spring, all @Service, @Repository, @Controller beans are singletons by default.


Problems With Singleton (Know These for Interviews)

ProblemWhy it matters
Global stateAny part of the app can modify it — hard to track bugs
Hard to testCan't easily mock/replace in unit tests
Hidden dependencyClasses silently depend on it — not visible in constructor
Concurrency issuesMutable state in singleton needs careful synchronisation
Violates SRP sometimesSingletons often accumulate too many responsibilities

In interviews: say "I'd use Singleton for stateless or read-only shared resources like config or logger. For stateful resources, I'd use dependency injection with a singleton scope managed by the framework (like Spring), not a hand-rolled Singleton."


Interview Talking Point

"Singleton ensures one instance for the lifetime of the app. Classic use cases: DB connection pool, config manager, logger. In production Java, I prefer the Bill Pugh pattern — thread-safe, lazy-loaded, no synchronisation overhead. Or I let Spring manage singleton scope via @Service, which also makes it injectable and testable. The risk with Singleton is global mutable state — I keep singletons stateless or read-only wherever possible."


Quick Revision

  1. One instance, global access point, created once
  2. Private constructor + static getInstance() method
  3. Thread-safety: use Double-Checked Locking or Bill Pugh pattern
  4. Spring beans are singletons by default — framework manages lifecycle
  5. Risk: global state, hard to test — keep singletons stateless or read-only


3. Builder Design Pattern

Category: Creational


What is Builder Pattern?

Builder Pattern constructs complex objects step by step, letting you produce different types and representations of an object using the same construction process.

Think of ordering a custom burger. You say: add bun, add patty, add cheese, add lettuce, no onions. The cashier (builder) assembles it step by step. You get exactly what you specified — nothing unwanted.


The Core Idea

Problem: A class with 10 fields. Constructor becomes monstrous.
         new User("Alice", "alice@email.com", null, null, 25, null, "ADMIN", true, null, "IN")

Solution: Build it step by step, set only what you need.
         new User.Builder()
             .name("Alice")
             .email("alice@email.com")
             .age(25)
             .role("ADMIN")
             .country("IN")
             .build();

❌ Without Builder — Telescoping Constructor Problem

class HttpRequest {
    private String url;
    private String method;
    private String body;
    private Map<String, String> headers;
    private int timeoutMs;
    private boolean followRedirects;
    private String authToken;
    private int retryCount;

    // You need multiple constructors for different combinations — telescoping
    public HttpRequest(String url, String method) { ... }
    public HttpRequest(String url, String method, String body) { ... }
    public HttpRequest(String url, String method, String body, int timeoutMs) { ... }
    public HttpRequest(String url, String method, String body, int timeoutMs, String authToken) { ... }
    // ... grows forever, becomes unreadable
}

// Caller has no idea what each argument means
HttpRequest req = new HttpRequest("https://api.example.com", "POST", "{}", 5000, null, true, "token123", 3);
//                                                                           ↑ timeout? retries? what is this?

✅ With Builder Pattern

class HttpRequest {
    // All fields private, set only via builder
    private final String url;
    private final String method;
    private final String body;
    private final Map<String, String> headers;
    private final int timeoutMs;
    private final boolean followRedirects;
    private final String authToken;
    private final int retryCount;

    // Private constructor — only Builder can call this
    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.body = builder.body;
        this.headers = builder.headers;
        this.timeoutMs = builder.timeoutMs;
        this.followRedirects = builder.followRedirects;
        this.authToken = builder.authToken;
        this.retryCount = builder.retryCount;
    }

    // Static inner Builder class
    public static class Builder {
        private String url;              // required
        private String method = "GET";   // optional with default
        private String body;
        private Map<String, String> headers = new HashMap<>();
        private int timeoutMs = 3000;    // default
        private boolean followRedirects = true;
        private String authToken;
        private int retryCount = 0;

        public Builder(String url) {     // required field in constructor
            this.url = url;
        }

        public Builder method(String method) { this.method = method; return this; }
        public Builder body(String body) { this.body = body; return this; }
        public Builder header(String key, String value) { headers.put(key, value); return this; }
        public Builder timeoutMs(int ms) { this.timeoutMs = ms; return this; }
        public Builder followRedirects(boolean follow) { this.followRedirects = follow; return this; }
        public Builder authToken(String token) { this.authToken = token; return this; }
        public Builder retryCount(int count) { this.retryCount = count; return this; }

        public HttpRequest build() {
            // Validation before building
            if (url == null || url.isEmpty()) throw new IllegalStateException("URL is required");
            return new HttpRequest(this);
        }
    }
}

// Usage — readable, self-documenting, only set what you need
HttpRequest request = new HttpRequest.Builder("https://api.example.com/orders")
    .method("POST")
    .body("{\"item\": \"shoes\"}")
    .authToken("Bearer abc123")
    .timeoutMs(5000)
    .retryCount(3)
    .build();

Real-World System Design Examples

1. SQL Query Builder

String query = new QueryBuilder()
    .select("id", "name", "email")
    .from("users")
    .where("age > 18")
    .and("country = 'IN'")
    .orderBy("name", "ASC")
    .limit(50)
    .build();
// "SELECT id, name, email FROM users WHERE age > 18 AND country = 'IN' ORDER BY name ASC LIMIT 50"

2. Kafka Message Builder

KafkaMessage message = new KafkaMessage.Builder("order-events")
    .key(orderId)
    .payload(orderJson)
    .header("source", "order-service")
    .header("version", "v2")
    .retentionMs(86400000)
    .build();

3. API Response Builder

ApiResponse response = new ApiResponse.Builder()
    .status(200)
    .message("Order placed successfully")
    .data(order)
    .meta(new PageMeta(1, 10, 100))
    .build();

4. Used in Java Standard Library

StringBuilder  →  append().append().toString()
Stream.Builder →  add().add().build()
URI.Builder    →  scheme().host().path().build()

Interview Talking Point

"I use Builder when an object has many optional parameters or a complex construction process. The telescoping constructor anti-pattern — having 8 overloaded constructors for different combinations — is the sign that Builder is needed. With Builder, the caller sets only what's relevant, it's readable (method names are self-documenting), and I can add validation in build() before the object is created. I've used it for HTTP request clients, API response wrappers, and query builders."


Quick Revision

  1. Builder = construct complex objects step by step, set only what you need
  2. Solves telescoping constructor problem — too many constructor overloads
  3. return this in each setter method enables method chaining (fluent API)
  4. Validation goes in build() — object is valid before it's created
  5. Used in: HTTP clients, query builders, Kafka producers, API responses


4. Observer Design Pattern

Category: Behavioral


What is Observer Pattern?

Observer defines a one-to-many dependency between objects. When one object (Subject/Publisher) changes state, all its dependents (Observers/Subscribers) are automatically notified and updated.

Think of a YouTube channel. You subscribe (register as observer). When the creator uploads a video (state change), all subscribers get notified. Creator doesn't know who the subscribers are — it just notifies all of them.


The Core Idea

Subject (Publisher)       Observers (Subscribers)
──────────────────        ──────────────────────
Maintains a list of  →    EmailNotifier
observers                 SMSNotifier
                          PushNotifier
                          AnalyticsTracker

When state changes:
subject.notifyAll()  →    every observer's update() is called

✅ Implementation

// Observer interface — all observers implement this
interface Observer {
    void update(String event, Object data);
}

// Subject interface — the publisher
interface Subject {
    void subscribe(Observer observer);
    void unsubscribe(Observer observer);
    void notifyObservers(String event, Object data);
}

// Concrete Subject — Order in this case
class OrderService implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private Order currentOrder;

    public void subscribe(Observer observer) {
        observers.add(observer);
    }

    public void unsubscribe(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers(String event, Object data) {
        for (Observer observer : observers) {
            observer.update(event, data);   // notify all registered observers
        }
    }

    public void placeOrder(Order order) {
        this.currentOrder = order;
        // Save order...
        notifyObservers("ORDER_PLACED", order);   // triggers all observers
    }

    public void cancelOrder(String orderId) {
        // Cancel logic...
        notifyObservers("ORDER_CANCELLED", orderId);
    }
}

// Concrete Observers — each reacts differently to the same event
class EmailNotifier implements Observer {
    public void update(String event, Object data) {
        if (event.equals("ORDER_PLACED")) {
            Order order = (Order) data;
            System.out.println("EMAIL: Sending order confirmation for " + order.getId());
        }
    }
}

class InventoryManager implements Observer {
    public void update(String event, Object data) {
        if (event.equals("ORDER_PLACED")) {
            Order order = (Order) data;
            System.out.println("INVENTORY: Reducing stock for order " + order.getId());
        }
    }
}

class AnalyticsTracker implements Observer {
    public void update(String event, Object data) {
        System.out.println("ANALYTICS: Tracking event: " + event);
    }
}

// Wiring — register observers
OrderService orderService = new OrderService();
orderService.subscribe(new EmailNotifier());
orderService.subscribe(new InventoryManager());
orderService.subscribe(new AnalyticsTracker());

// Trigger — all three are notified automatically
orderService.placeOrder(new Order("ORD001", "user123", 1500.00));

Real-World System Design Examples

1. Event-Driven Microservices (Observer at Scale)

OrderService places order
        ↓
  Publishes event to Kafka topic: "order.placed"
        ↓
  ┌─────────────────────────────┐
  │  EmailService subscribes    │ → sends confirmation email
  │  InventoryService subscribes│ → reduces stock
  │  LoyaltyService subscribes  │ → adds reward points
  │  AnalyticsService subscribes│ → logs conversion
  └─────────────────────────────┘

Each service is an Observer. Kafka is the Subject/broker. OrderService has no idea who is listening — completely decoupled.

2. Stock Price Alert System

StockPriceService (Subject)
    → PriceAlertObserver (notifies users when threshold crossed)
    → PortfolioObserver (recalculates portfolio value)
    → TradingBotObserver (triggers buy/sell automatically)

3. UI Event Systems (React/Angular)

Button click (Subject)
    → FormValidator (Observer)
    → SubmitHandler (Observer)
    → AnalyticsLogger (Observer)

Observer vs Pub/Sub — Know the Difference

AspectObserverPub/Sub (Kafka, RabbitMQ)
CouplingSubject knows observers (indirect)Publisher doesn't know subscribers
CommunicationSynchronous (direct call)Asynchronous (via broker/queue)
ScaleIn-process, same applicationAcross services/systems
ExampleJava EventListener, React stateKafka, RabbitMQ, AWS SNS

Interview Talking Point

"Observer defines a one-to-many relationship — when an order is placed, the OrderService notifies all registered observers: email service, inventory manager, analytics tracker. None of them are hardcoded into OrderService — they register themselves. Adding a loyalty points service is just registering a new observer. At the system level, this is how Kafka works — order service publishes an event, multiple downstream services subscribe independently. The key benefit is decoupling: OrderService doesn't know or care who's listening."


Quick Revision

  1. Observer = one publisher (Subject), many subscribers (Observers)
  2. Subject maintains observer list; notifies all on state change
  3. Observers subscribe/unsubscribe at runtime — dynamic
  4. Decouples publisher from all consumers — publisher doesn't know who listens
  5. At system scale: Kafka topics, webhooks, event-driven microservices


5. Strategy Design Pattern

Category: Behavioral


What is Strategy Pattern?

Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The algorithm can be selected at runtime without changing the client that uses it.

Think of a GPS app. You choose your route strategy: fastest, shortest, avoid tolls, avoid highways. The app doesn't change — you just swap the routing strategy.


The Core Idea

Without Strategy:
   if (type == "FASTEST")     { ... routing algorithm A ... }
   if (type == "SHORTEST")    { ... routing algorithm B ... }
   if (type == "AVOID_TOLLS") { ... routing algorithm C ... }
   (OCP violated — new algorithm = modify existing code)

With Strategy:
   interface RouteStrategy { Route calculate(Point from, Point to); }
   FastestRoute implements RouteStrategy
   ShortestRoute implements RouteStrategy
   AvoidTollsRoute implements RouteStrategy
   (Add new algorithm = new class only)

✅ Implementation

// Strategy interface — defines the algorithm contract
interface SortStrategy {
    void sort(int[] array);
}

// Concrete strategies — different algorithms
class BubbleSortStrategy implements SortStrategy {
    public void sort(int[] array) {
        System.out.println("Sorting with Bubble Sort");
        // bubble sort logic
    }
}

class QuickSortStrategy implements SortStrategy {
    public void sort(int[] array) {
        System.out.println("Sorting with Quick Sort");
        // quicksort logic
    }
}

class MergeSortStrategy implements SortStrategy {
    public void sort(int[] array) {
        System.out.println("Sorting with Merge Sort");
        // merge sort logic
    }
}

// Context — uses a strategy, doesn't know the algorithm details
class DataSorter {
    private SortStrategy strategy;

    public DataSorter(SortStrategy strategy) {
        this.strategy = strategy;
    }

    // Strategy can be swapped at runtime
    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;
    }

    public void sort(int[] array) {
        strategy.sort(array);   // delegates to whichever strategy is set
    }
}

// Usage — swap strategy without changing DataSorter
DataSorter sorter = new DataSorter(new QuickSortStrategy());
sorter.sort(new int[]{5, 3, 1, 4, 2});   // uses QuickSort

sorter.setStrategy(new MergeSortStrategy());
sorter.sort(new int[]{5, 3, 1, 4, 2});   // now uses MergeSort — same sorter, new algorithm

Practical Example — Payment Strategy

// Strategy interface
interface PaymentStrategy {
    boolean pay(int amount);
}

// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber, cvv;
    public CreditCardPayment(String cardNumber, String cvv) { ... }

    public boolean pay(int amount) {
        System.out.println("Paid ₹" + amount + " via Credit Card ending " + cardNumber.substring(12));
        return true;
    }
}

class UPIPayment implements PaymentStrategy {
    private String upiId;
    public UPIPayment(String upiId) { this.upiId = upiId; }

    public boolean pay(int amount) {
        System.out.println("Paid ₹" + amount + " via UPI: " + upiId);
        return true;
    }
}

class WalletPayment implements PaymentStrategy {
    private double walletBalance;

    public boolean pay(int amount) {
        if (walletBalance < amount) return false;
        walletBalance -= amount;
        System.out.println("Paid ₹" + amount + " via Wallet");
        return true;
    }
}

// Context — Checkout
class CheckoutService {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(Cart cart) {
        boolean success = paymentStrategy.pay(cart.getTotal());
        if (success) System.out.println("Order confirmed!");
        else System.out.println("Payment failed!");
    }
}

// Usage — user picks payment method at runtime
CheckoutService checkout = new CheckoutService();

// User selects UPI
checkout.setPaymentStrategy(new UPIPayment("alice@okicici"));
checkout.checkout(cart);

// User switches to credit card
checkout.setPaymentStrategy(new CreditCardPayment("1234567890123456", "123"));
checkout.checkout(cart);

Strategy vs Factory — Know the Difference

AspectFactory PatternStrategy Pattern
PurposeDecides which object to createDecides which algorithm to run
FocusObject creationBehaviour / algorithm selection
When decidedUsually at creation timeCan be changed at runtime
They work togetherFactory can create the Strategy object to inject

Real-World System Design Examples

1. Discount Calculation Strategy (E-commerce)

interface DiscountStrategy { double apply(double price); }

FlatDiscountStrategy    → flat ₹100 off
PercentageStrategy      → 15% off
LoyaltyStrategy         → based on user tier
FlashSaleStrategy       → time-based discount
CouponStrategy          → coupon code discount

2. Log Routing Strategy

interface LogStrategy { void write(String log); }

ConsoleLogStrategy    → development
FileLogStrategy       → staging
CloudWatchStrategy    → production AWS
DatadogStrategy       → production monitoring

3. Load Balancing Strategy

interface LoadBalancingStrategy { Server select(List<Server> servers); }

RoundRobinStrategy     → rotate through servers
LeastConnectionsStrategy → pick server with fewest active connections
WeightedStrategy       → route more to powerful servers
RandomStrategy         → random selection

4. Authentication Strategy (Passport.js style)

interface AuthStrategy { User authenticate(Request req); }

JWTStrategy          → validate JWT token
OAuthStrategy        → Google / GitHub OAuth
BasicAuthStrategy    → username + password
APIKeyStrategy       → API key validation

Interview Talking Point

"Strategy encapsulates algorithms behind an interface and makes them swappable at runtime. For payments, I define a PaymentStrategy interface — CreditCardPayment, UPIPayment, WalletPayment each implement it. The checkout service holds a reference to the interface and delegates to whichever strategy the user selected. No if/else in checkout logic. Adding a new payment method = one new class, zero changes elsewhere. At the system level, load balancers use this — Round Robin, Least Connections, Weighted — all are swappable routing strategies."


Quick Revision

  1. Strategy = family of algorithms, each in its own class, swappable at runtime
  2. Client (Context) holds a reference to the Strategy interface — not a concrete class
  3. Eliminates if/else chains that check "type" to pick an algorithm
  4. Strategy can be changed at runtime via setter (unlike Factory which decides at creation)
  5. Used for: payment methods, discount rules, sorting, routing, load balancing, auth


How This Connects

  • Polymorphism / Abstraction — every pattern here is "define an interface, vary the implementation". Patterns are named recipes for applying those two.
  • SOLID — Strategy & Factory are how you get Open/Closed; Repository & Gateway (Factory + Dependency Injection) are how you get Dependency Inversion.
  • Dependency Injection — the "wiring" step of Factory/Strategy: something outside picks the implementation and hands it in. Spring's container is a Factory + Singleton manager.
  • Composition vs Inheritance — Strategy, Observer, Builder are all composition; they let you vary behaviour without inheritance chains.
  • Module 3 — HLD / Module 12 — Async — Observer scales up to Kafka topics and webhooks; Strategy scales up to swappable load-balancing / routing / auth; Factory scales up to "pick the implementation from config".

Design Patterns — Complete Summary Table

PatternTypeProblem it solvesKey mechanism
FactoryCreationalCaller shouldn't know which class to instantiateFactory method returns interface type
SingletonCreationalNeed exactly one shared instancePrivate constructor + static instance
BuilderCreationalComplex object construction with many optionsFluent API with step-by-step setters
ObserverBehavioralNotify multiple objects when state changesSubscribe/notify with observer list
StrategyBehavioralMultiple algorithms for same task, swappableInterface + swappable implementation

Combined Interview Answer (If Asked "Name Design Patterns You Use")

"The patterns I reach for most in system design:

Factory when I need to create objects based on runtime conditions — like a NotificationFactory that returns Email, SMS, or Push implementations.

Singleton for shared resources — DB connection pool, config manager. In Spring I rely on the framework's singleton scope instead of hand-rolling it.

Builder when constructing complex objects — HTTP request clients, API response wrappers, Kafka message producers with many optional fields.

Observer for event-driven flows — when an order is placed, I want inventory, email, and analytics to react without OrderService knowing about any of them. This scales into Kafka topics at the system level.

Strategy to eliminate if/else logic for swappable behaviour — payment methods, discount rules, load balancing algorithms. Each algorithm gets its own class; the context just holds the interface."

Quick-revision quiz

Question 1 of 18
Design patterns are grouped into three categories. Name the three, with two examples each.