vishal maurya

LLD Series: Design Patterns That Actually Come Up in LLD Rounds

Overview

Part of the Interview Prep series. Dotted-underline words are glossary terms: hover for the meaning or open the Glossary. Track your progress on the Interview Prep tracker. Series index: Start here.

Rule: name the pattern only after you have stated the problem it solves. "Payment methods vary and I don't want if/else on type, so Strategy" beats "I'll use Strategy".

All snippets are Java 11, compiled and run. Money is long paise or BigDecimal, never double.

PatternCategoryTypical LLD problem
StrategyBehavioralPricing, fee calc, payment routing, eviction policy, rate-limit algo
Factory / Abstract FactoryCreationalVehicle/payment-method/notification creation, per-network families
BuilderCreationalComplex requests (payment request, HTTP request, Pizza)
SingletonCreationalLogger, config, ParkingLot instance, ID generator
ObserverBehavioralPub-sub, notifications, stock ticker, event bus
StateBehavioralVending machine, ATM, payment/order lifecycle, elevator
CommandBehavioralUndo/redo, ATM ops, transaction queue, remote
DecoratorStructuralPizza toppings, logging/fraud/retry around processor
Chain of ResponsibilityBehavioralValidation pipeline, ATM cash dispenser, logger levels, middleware
Template MethodBehavioralPayment flow skeleton, report generation
AdapterStructuralWrapping third-party/legacy gateway SDK
FacadeStructuralCheckout/booking orchestration entry point
ProxyStructuralAccess control, caching, rate limiting, lazy load
RepositoryArchitecturalPersistence abstraction, in-memory store for tests

1. Strategy

Intent: define a family of interchangeable algorithms, encapsulate each, swap at runtime. Use when: you see if (type == X) ... else if (type == Y) over behavior; behavior must change at runtime or be added without touching callers (OCP). Interview: fee/discount calculation, payment gateway routing, cache eviction (LRU/LFU), rate limiting algorithms, elevator scheduling, parking pricing.

 Checkout --has-a--> <<interface>> FeeStrategy
                        ^            ^
                   FlatFee      PercentFee
// File: StrategyDemo.java
import java.math.BigDecimal;

interface FeeStrategy { BigDecimal fee(BigDecimal amount); }

class FlatFee implements FeeStrategy {
    public BigDecimal fee(BigDecimal amount) { return new BigDecimal("2.00"); }
}

class PercentFee implements FeeStrategy {
    private final BigDecimal pct;
    PercentFee(String pct) { this.pct = new BigDecimal(pct); }
    public BigDecimal fee(BigDecimal amount) { return amount.multiply(pct); }
}

class Checkout {
    private FeeStrategy strategy;
    Checkout(FeeStrategy s) { this.strategy = s; }
    void setStrategy(FeeStrategy s) { this.strategy = s; }
    BigDecimal total(BigDecimal amount) { return amount.add(strategy.fee(amount)); }
}

public class StrategyDemo {
    public static void main(String[] args) {
        Checkout c = new Checkout(new FlatFee());
        System.out.println(c.total(new BigDecimal("100")));      // 102.00
        c.setStrategy(new PercentFee("0.02"));
        System.out.println(c.total(new BigDecimal("100")));      // 102.00 (2%)
        c.setStrategy(a -> BigDecimal.ZERO);                     // lambda = strategy
        System.out.println(c.total(new BigDecimal("100")));      // 100
    }
}

Tip: a functional interface lets lambdas act as strategies. Strategy differs from State: the client picks the strategy; in State the object transitions itself.


2. Factory (Simple Factory / Factory Method)

Intent: centralize object creation behind a method; callers depend on an abstraction. Use when: creation logic depends on input (type string, config), or you want to hide concrete classes. Interview: VehicleFactory, PaymentMethodFactory, NotificationFactory, ShapeFactory.

 Client --> PaymentMethodFactory.create(type) --> <<PaymentMethod>> <- Upi | Card | NetBanking
// File: FactoryDemo.java
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

interface PaymentMethod { String pay(long paise); }
class Upi implements PaymentMethod { public String pay(long p) { return "UPI paid " + p; } }
class Card implements PaymentMethod { public String pay(long p) { return "CARD paid " + p; } }

class PaymentMethodFactory {
    // Registry-based: adding a method = register(), no edit to create() (OCP)
    private static final Map<String, Supplier<PaymentMethod>> REGISTRY = new HashMap<>();
    static {
        register("UPI", Upi::new);
        register("CARD", Card::new);
    }
    static void register(String type, Supplier<PaymentMethod> s) { REGISTRY.put(type.toUpperCase(), s); }
    static PaymentMethod create(String type) {
        Supplier<PaymentMethod> s = REGISTRY.get(type.toUpperCase());
        if (s == null) throw new IllegalArgumentException("Unknown payment type: " + type);
        return s.get();
    }
}

public class FactoryDemo {
    public static void main(String[] args) {
        System.out.println(PaymentMethodFactory.create("upi").pay(500));
        PaymentMethodFactory.register("WALLET", () -> p -> "WALLET paid " + p);
        System.out.println(PaymentMethodFactory.create("wallet").pay(700));
        try { PaymentMethodFactory.create("bitcoin"); }
        catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }
    }
}

Factory Method (GoF) = subclass decides what to create (abstract Parser createParser()); Simple Factory = one static method as above.


3. Abstract Factory

Intent: create families of related objects without naming concrete classes; guarantees the products match. Use when: several products must be consistent (Visa authorizer + Visa tokenizer; same-theme UI widgets; per-region tax + invoice). Interview: card network integration (Visa/Mastercard/RuPay), cross-platform UI kit.

 NetworkFactory <<interface>>        Authorizer   Tokenizer
   ^        ^                         ^   ^        ^    ^
 VisaFactory MasterFactory      VisaAuth MCAuth VisaTok MCTok
 (creates VisaAuth+VisaTok)     (each factory returns a matching pair)
// File: AbstractFactoryDemo.java
interface Authorizer { String authorize(String pan); }
interface Tokenizer { String tokenize(String pan); }

interface NetworkFactory {
    Authorizer authorizer();
    Tokenizer tokenizer();
}

class VisaFactory implements NetworkFactory {
    public Authorizer authorizer() { return pan -> "VISA-AUTH(" + pan + ")"; }
    public Tokenizer tokenizer() { return pan -> "VTS-" + pan.substring(pan.length() - 4); }
}

class MastercardFactory implements NetworkFactory {
    public Authorizer authorizer() { return pan -> "MC-AUTH(" + pan + ")"; }
    public Tokenizer tokenizer() { return pan -> "MDES-" + pan.substring(pan.length() - 4); }
}

class CardService {
    private final NetworkFactory f;
    CardService(NetworkFactory f) { this.f = f; }
    String onboard(String pan) { return f.authorizer().authorize(pan) + " -> " + f.tokenizer().tokenize(pan); }
}

public class AbstractFactoryDemo {
    static NetworkFactory forPan(String pan) {      // BIN-based selection
        return pan.startsWith("4") ? new VisaFactory() : new MastercardFactory();
    }
    public static void main(String[] args) {
        System.out.println(new CardService(forPan("4111111111111111")).onboard("4111111111111111"));
        System.out.println(new CardService(forPan("5500000000000004")).onboard("5500000000000004"));
    }
}

Factory vs Abstract Factory: one product vs a family of products.


4. Builder

Intent: construct complex immutable objects step by step; avoid telescoping constructors; validate in build(). Use when: >4 parameters, many optional, immutability required. Interview: PaymentRequest, HttpRequest, Pizza, Order.

// File: BuilderDemo.java
import java.util.Objects;

final class PaymentRequest {
    private final String merchantId;
    private final long amountPaise;          // required
    private final String currency;           // optional, default INR
    private final String description;        // optional
    private final String idempotencyKey;     // optional

    private PaymentRequest(Builder b) {
        merchantId = b.merchantId; amountPaise = b.amountPaise; currency = b.currency;
        description = b.description; idempotencyKey = b.idempotencyKey;
    }
    public static Builder builder(String merchantId, long amountPaise) { return new Builder(merchantId, amountPaise); }

    public static final class Builder {
        private final String merchantId; private final long amountPaise;
        private String currency = "INR"; private String description = ""; private String idempotencyKey;
        private Builder(String m, long a) { merchantId = m; amountPaise = a; }
        public Builder currency(String c) { currency = c; return this; }
        public Builder description(String d) { description = d; return this; }
        public Builder idempotencyKey(String k) { idempotencyKey = k; return this; }
        public PaymentRequest build() {
            Objects.requireNonNull(merchantId, "merchantId");
            if (amountPaise <= 0) throw new IllegalStateException("amount must be > 0");
            return new PaymentRequest(this);
        }
    }
    @Override public String toString() {
        return merchantId + " " + amountPaise + " " + currency + " '" + description + "' key=" + idempotencyKey;
    }
}

public class BuilderDemo {
    public static void main(String[] args) {
        PaymentRequest r = PaymentRequest.builder("M1", 49900).description("order 7").idempotencyKey("k-1").build();
        System.out.println(r);
        try { PaymentRequest.builder("M1", 0).build(); } catch (IllegalStateException e) { System.out.println(e.getMessage()); }
    }
}

Required fields go in the builder's constructor so you cannot forget them.


5. Singleton (thread-safe variants)

Intent: exactly one instance, global access point. Use when: truly shared stateless/coordinating resource (config, logger registry). Otherwise prefer DI: singleton hurts testability and hides dependencies. Interview: Logger, ParkingLot, ConfigManager, ID generator. Expect "make it thread-safe" and "can reflection/serialization break it?".

// File: SingletonDemo.java
// 1) Enum: simplest, safe against reflection and serialization (Effective Java #3)
enum EnumConfig { INSTANCE; String env() { return "prod"; } }

// 2) Initialization-on-demand holder: lazy + thread-safe via class loading, no locks
class HolderLogger {
    private HolderLogger() {}
    private static class Holder { static final HolderLogger I = new HolderLogger(); }
    static HolderLogger get() { return Holder.I; }
}

// 3) Double-checked locking: needs volatile, or other threads can see a half-built object
class DclRegistry {
    private static volatile DclRegistry instance;
    private DclRegistry() {}
    static DclRegistry get() {
        DclRegistry r = instance;                 // single volatile read on fast path
        if (r == null) {
            synchronized (DclRegistry.class) {
                r = instance;
                if (r == null) instance = r = new DclRegistry();
            }
        }
        return r;
    }
}

public class SingletonDemo {
    public static void main(String[] args) throws Exception {
        System.out.println(EnumConfig.INSTANCE.env());
        System.out.println(HolderLogger.get() == HolderLogger.get());
        Thread[] ts = new Thread[8]; DclRegistry[] seen = new DclRegistry[8];
        for (int i = 0; i < 8; i++) { final int k = i; ts[i] = new Thread(() -> seen[k] = DclRegistry.get()); ts[i].start(); }
        boolean same = true;
        for (int i = 0; i < 8; i++) { ts[i].join(); same &= seen[i] == seen[0]; }
        System.out.println("all same: " + same);
    }
}

Eager static final INSTANCE = new X() is also thread-safe and fine if cheap. A plain synchronized getInstance() is correct but locks on every call.


6. Observer

Intent: one-to-many dependency; subject notifies subscribers on state change without knowing them. Use when: multiple independent reactions to an event (email + SMS + ledger on payment success). Interview: pub-sub, notification service, stock ticker, order status updates.

 Subject(EventBus) --notifies--> <<Listener>> <- EmailListener | AuditListener
// File: ObserverDemo.java
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

interface PaymentListener { void onPaymentSucceeded(String paymentId, long paise); }

class PaymentEvents {
    // CopyOnWriteArrayList: safe to iterate while listeners (un)subscribe from other threads
    private final List<PaymentListener> listeners = new CopyOnWriteArrayList<>();
    void subscribe(PaymentListener l) { listeners.add(l); }
    void unsubscribe(PaymentListener l) { listeners.remove(l); }
    void publish(String id, long paise) {
        for (PaymentListener l : listeners) {
            try { l.onPaymentSucceeded(id, paise); }
            catch (RuntimeException e) { System.out.println("listener failed: " + e.getMessage()); } // isolate failures
        }
    }
}

public class ObserverDemo {
    public static void main(String[] args) {
        PaymentEvents bus = new PaymentEvents();
        bus.subscribe((id, p) -> System.out.println("EMAIL receipt for " + id));
        bus.subscribe((id, p) -> { throw new RuntimeException("sms down"); });
        bus.subscribe((id, p) -> System.out.println("LEDGER credit " + p));
        bus.publish("pay-1", 1000);
    }
}

Pull vs push model, sync vs async delivery (executor), ordering, retry/DLQ are the follow-ups.


7. State

Intent: an object changes behavior when its internal state changes; each state is a class, removing giant switch(state). Use when: lifecycle with legal/illegal transitions. Interview: vending machine, ATM, payment/order lifecycle, elevator, traffic light, document workflow.

 CREATED --authorize--> AUTHORIZED --capture--> CAPTURED --refund--> REFUNDED
 (illegal op in any state => IllegalStateException)
// File: StateDemo.java
interface PaymentState {
    PaymentState authorize();
    PaymentState capture();
    PaymentState refund();
    String name();
}

abstract class BaseState implements PaymentState {
    public PaymentState authorize() { throw bad("authorize"); }
    public PaymentState capture()   { throw bad("capture"); }
    public PaymentState refund()    { throw bad("refund"); }
    private IllegalStateException bad(String op) { return new IllegalStateException(op + " not allowed in " + name()); }
}
class Created extends BaseState    { public PaymentState authorize() { return new Authorized(); } public String name() { return "CREATED"; } }
class Authorized extends BaseState { public PaymentState capture()   { return new Captured(); }   public String name() { return "AUTHORIZED"; } }
class Captured extends BaseState   { public PaymentState refund()    { return new Refunded(); }   public String name() { return "CAPTURED"; } }
class Refunded extends BaseState   { public String name() { return "REFUNDED"; } }

class Payment {
    private PaymentState state = new Created();
    void authorize() { state = state.authorize(); }
    void capture()   { state = state.capture(); }
    void refund()    { state = state.refund(); }
    String status()  { return state.name(); }
}

public class StateDemo {
    public static void main(String[] args) {
        Payment p = new Payment();
        p.authorize(); p.capture();
        System.out.println(p.status());
        try { p.authorize(); } catch (IllegalStateException e) { System.out.println(e.getMessage()); }
        p.refund();
        System.out.println(p.status());
    }
}

Lighter alternative: enum State with EnumMap<State, Set<State>> transition table when states carry no behavior.


8. Command

Intent: encapsulate a request as an object: queue it, log it, retry it, undo it. Use when: undo/redo, deferred/queued execution, audit trail, macro commands. Interview: ATM operations, text editor undo, smart-home remote, transaction journal.

// File: CommandDemo.java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.TreeMap;

interface Command { void execute(); void undo(); }

class TransferCommand implements Command {
    private final Map<String, Long> bal; private final String from, to; private final long amt;
    TransferCommand(Map<String, Long> bal, String from, String to, long amt) { this.bal = bal; this.from = from; this.to = to; this.amt = amt; }
    public void execute() {
        if (bal.get(from) < amt) throw new IllegalStateException("insufficient funds");
        bal.merge(from, -amt, Long::sum); bal.merge(to, amt, Long::sum);
    }
    public void undo() { bal.merge(from, amt, Long::sum); bal.merge(to, -amt, Long::sum); }
}

class CommandInvoker {
    private final Deque<Command> history = new ArrayDeque<>();
    void run(Command c) { c.execute(); history.push(c); }     // push only after success
    void undoLast() { if (!history.isEmpty()) history.pop().undo(); }
}

public class CommandDemo {
    public static void main(String[] args) {
        Map<String, Long> bal = new TreeMap<>(); bal.put("A", 100L); bal.put("B", 0L);
        CommandInvoker inv = new CommandInvoker();
        inv.run(new TransferCommand(bal, "A", "B", 40));
        System.out.println(bal);                  // {A=60, B=40}
        try { inv.run(new TransferCommand(bal, "A", "B", 500)); } catch (IllegalStateException e) { System.out.println(e.getMessage()); }
        inv.undoLast();
        System.out.println(bal);                  // {A=100, B=0}
    }
}

9. Decorator

Intent: add responsibilities to an object dynamically by wrapping it in an object with the same interface. Use when: cross-cutting features combinable in any order (logging, fraud check, retry, metrics) and subclass explosion is the alternative. Interview: Pizza/Coffee toppings, payment processor with logging/retry, input streams.

 PaymentProcessor <|-- Basic
 PaymentProcessor <|-- Decorator(has-a PaymentProcessor) <|-- Logging | FraudCheck
// File: DecoratorDemo.java
interface PaymentProcessor { String pay(String id, long paise); }

class BasicProcessor implements PaymentProcessor {
    public String pay(String id, long paise) { return "PAID " + id + " " + paise; }
}

abstract class ProcessorDecorator implements PaymentProcessor {
    protected final PaymentProcessor inner;
    ProcessorDecorator(PaymentProcessor inner) { this.inner = inner; }
}

class LoggingProcessor extends ProcessorDecorator {
    LoggingProcessor(PaymentProcessor in) { super(in); }
    public String pay(String id, long paise) {
        System.out.println("log: start " + id);
        String r = inner.pay(id, paise);
        System.out.println("log: end " + r);
        return r;
    }
}

class FraudCheckProcessor extends ProcessorDecorator {
    private final long limit;
    FraudCheckProcessor(PaymentProcessor in, long limit) { super(in); this.limit = limit; }
    public String pay(String id, long paise) {
        if (paise > limit) return "BLOCKED " + id;
        return inner.pay(id, paise);
    }
}

public class DecoratorDemo {
    public static void main(String[] args) {
        PaymentProcessor p = new LoggingProcessor(new FraudCheckProcessor(new BasicProcessor(), 10_000));
        System.out.println(p.pay("t1", 500));
        System.out.println(p.pay("t2", 99_999));
    }
}

Decorator vs Proxy: Decorator adds behavior and is composed by the client; Proxy controls access to the same object and usually builds/owns it.


10. Chain of Responsibility

Intent: pass a request along a chain of handlers; each handles it or forwards. Use when: ordered processing steps that should be pluggable/reorderable. Interview: validation pipeline, ATM cash dispenser (2000 -> 500 -> 100), logger levels, approval workflow, middleware.

// File: ChainDemo.java
class TxnRequest {
    final long paise; final String cardExpiry; final boolean blacklisted;
    TxnRequest(long p, String e, boolean b) { paise = p; cardExpiry = e; blacklisted = b; }
}

abstract class Handler {
    private Handler next;
    Handler then(Handler n) { this.next = n; return n; }          // fluent chain building
    final boolean handle(TxnRequest r) {
        if (!check(r)) return false;                               // stop on first failure
        return next == null || next.handle(r);
    }
    protected abstract boolean check(TxnRequest r);
}

class AmountHandler extends Handler {
    protected boolean check(TxnRequest r) { if (r.paise <= 0) { System.out.println("reject: amount"); return false; } return true; }
}
class ExpiryHandler extends Handler {
    protected boolean check(TxnRequest r) { if (r.cardExpiry.compareTo("2026-01") < 0) { System.out.println("reject: expired"); return false; } return true; }
}
class BlacklistHandler extends Handler {
    protected boolean check(TxnRequest r) { if (r.blacklisted) { System.out.println("reject: blacklisted"); return false; } return true; }
}

public class ChainDemo {
    public static void main(String[] args) {
        Handler head = new AmountHandler();
        head.then(new ExpiryHandler()).then(new BlacklistHandler());
        System.out.println(head.handle(new TxnRequest(100, "2030-05", false)));   // true
        System.out.println(head.handle(new TxnRequest(100, "2020-05", false)));   // reject: expired
        System.out.println(head.handle(new TxnRequest(100, "2030-05", true)));    // reject: blacklisted
    }
}

ATM variant: each handler dispenses what it can (n = amt / denom) and forwards the remainder.


11. Template Method

Intent: define the algorithm skeleton in a base class, let subclasses override steps. Use when: same workflow, different steps (UPI vs card flow). Prefer Strategy if you need runtime swap or want to avoid inheritance.

// File: TemplateDemo.java
abstract class PaymentFlow {
    // final: the skeleton must not be overridden
    public final String process(long paise) {
        validate(paise);
        String ref = authorize(paise);
        capture(ref);
        afterSuccess(ref);                 // hook with default no-op
        return ref;
    }
    protected void validate(long paise) { if (paise <= 0) throw new IllegalArgumentException("bad amount"); }
    protected abstract String authorize(long paise);
    protected abstract void capture(String ref);
    protected void afterSuccess(String ref) {}
}

class UpiFlow extends PaymentFlow {
    protected String authorize(long p) { return "UPI-REF-" + p; }
    protected void capture(String ref) { System.out.println("UPI collect " + ref); }
}

class CardFlow extends PaymentFlow {
    protected String authorize(long p) { return "CARD-AUTH-" + p; }
    protected void capture(String ref) { System.out.println("Card capture " + ref); }
    @Override protected void afterSuccess(String ref) { System.out.println("3DS receipt " + ref); }
}

public class TemplateDemo {
    public static void main(String[] args) {
        System.out.println(new UpiFlow().process(100));
        System.out.println(new CardFlow().process(250));
    }
}

12. Adapter

Intent: convert one interface into the one clients expect. Use when: integrating a third-party/legacy SDK you cannot change; keeps your domain clean (anti-corruption layer). Interview: wrapping multiple gateways (Razorpay, Stripe, legacy bank API) behind one PaymentGateway.

// File: AdapterDemo.java
// Third-party SDK we cannot modify: dollars as double, returns int status code
class LegacyBankSdk {
    int makePayment(String card, double dollars) { return dollars < 1000 ? 200 : 402; }
}

// Our domain interface
interface PaymentGateway { boolean charge(String card, long cents); }

class LegacyBankAdapter implements PaymentGateway {
    private final LegacyBankSdk sdk;
    LegacyBankAdapter(LegacyBankSdk sdk) { this.sdk = sdk; }
    public boolean charge(String card, long cents) {
        int code = sdk.makePayment(card, cents / 100.0);   // unit + status translation lives here only
        return code == 200;
    }
}

public class AdapterDemo {
    public static void main(String[] args) {
        PaymentGateway g = new LegacyBankAdapter(new LegacyBankSdk());
        System.out.println(g.charge("4111", 50_000));     // true  ($500)
        System.out.println(g.charge("4111", 500_000));    // false ($5000)
    }
}

13. Facade

Intent: one simple entry point over a complex subsystem. Use when: callers should not orchestrate 5 services; defines the public API of a module. Interview: BookingFacade, CheckoutService, ATMFacade.

// File: FacadeDemo.java
class InventoryService { boolean reserve(String sku) { System.out.println("reserved " + sku); return true; } void release(String sku) { System.out.println("released " + sku); } }
class PaymentService  { boolean charge(long paise) { System.out.println("charged " + paise); return paise < 100_000; } }
class NotificationService { void send(String msg) { System.out.println("notify: " + msg); } }

class CheckoutFacade {
    private final InventoryService inv = new InventoryService();
    private final PaymentService pay = new PaymentService();
    private final NotificationService notif = new NotificationService();

    boolean checkout(String sku, long paise) {
        if (!inv.reserve(sku)) return false;
        if (!pay.charge(paise)) { inv.release(sku); notif.send("payment failed"); return false; }  // compensation
        notif.send("order placed for " + sku);
        return true;
    }
}

public class FacadeDemo {
    public static void main(String[] args) {
        CheckoutFacade f = new CheckoutFacade();
        System.out.println(f.checkout("SKU1", 5_000));
        System.out.println(f.checkout("SKU2", 500_000));
    }
}

Facade does not forbid direct subsystem access; it simplifies. Adapter changes an interface; Facade simplifies many.


14. Proxy

Intent: a stand-in with the same interface that controls access (auth, caching, lazy init, remote, rate limiting). Interview: access-controlled account service, caching proxy for rate/FX lookups, rate-limiting proxy.

// File: ProxyDemo.java
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

interface FxService { double rate(String pair); }

class RemoteFxService implements FxService {
    public double rate(String pair) { System.out.println("  remote call for " + pair); return 83.5; }
}

// Static proxy: caching
class CachingFxProxy implements FxService {
    private final FxService real; private final Map<String, Double> cache = new HashMap<>();
    CachingFxProxy(FxService real) { this.real = real; }
    public double rate(String pair) { return cache.computeIfAbsent(pair, real::rate); }
}

public class ProxyDemo {
    public static void main(String[] args) {
        FxService s = new CachingFxProxy(new RemoteFxService());
        s.rate("USD/INR"); s.rate("USD/INR");                // only one remote call

        // Dynamic proxy (JDK): generic cross-cutting logging, what Spring AOP does under the hood
        FxService real = new RemoteFxService();
        FxService logged = (FxService) Proxy.newProxyInstance(
            FxService.class.getClassLoader(), new Class<?>[]{FxService.class},
            (proxy, method, a) -> { System.out.println("-> " + method.getName()); return method.invoke(real, a); });
        System.out.println(logged.rate("EUR/INR"));
    }
}

15. Repository

Intent: collection-like abstraction over persistence; domain code does not know if storage is SQL, Mongo or a Map. Use when: you need testability and to defer DB choice. In machine-coding rounds always use an in-memory implementation behind the interface.

// File: RepositoryDemo.java
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

interface Repository<T, ID> {
    T save(T entity);
    Optional<T> findById(ID id);
    List<T> findAll();
    void deleteById(ID id);
}

class InMemoryRepository<T, ID> implements Repository<T, ID> {
    private final Map<ID, T> store = new ConcurrentHashMap<>();
    private final Function<T, ID> idOf;
    InMemoryRepository(Function<T, ID> idOf) { this.idOf = idOf; }
    public T save(T e) { store.put(idOf.apply(e), e); return e; }
    public Optional<T> findById(ID id) { return Optional.ofNullable(store.get(id)); }
    public List<T> findAll() { return store.values().stream().collect(Collectors.toList()); }
    public void deleteById(ID id) { store.remove(id); }
}

class Txn {
    final String id; final long paise;
    Txn(String id, long paise) { this.id = id; this.paise = paise; }
    public String toString() { return id + ":" + paise; }
}

public class RepositoryDemo {
    public static void main(String[] args) {
        Repository<Txn, String> repo = new InMemoryRepository<>(t -> t.id);
        repo.save(new Txn("t1", 100)); repo.save(new Txn("t2", 200));
        System.out.println(repo.findById("t1").map(Object::toString).orElse("none"));
        repo.deleteById("t1");
        System.out.println(repo.findById("t1").isPresent() + " " + repo.findAll());
    }
}

Pattern selection cheat sheet

  • Many if/else on type deciding behavior -> Strategy. On state -> State.
  • Creation depends on input -> Factory; families -> Abstract Factory; many params -> Builder.
  • Fan-out of events -> Observer. Undo/queue -> Command. Wrap with extras -> Decorator.
  • Ordered steps that can short-circuit -> Chain. Fixed skeleton -> Template Method.
  • Third-party mismatch -> Adapter. Simplify subsystem -> Facade. Gate access/cache -> Proxy.
  • Don't force patterns: if only one implementation exists and none is foreseen, YAGNI.