LLD Series: Design Patterns That Actually Come Up in LLD Rounds
Overview
- 1. Strategy
- 2. Factory (Simple Factory / Factory Method)
- 3. Abstract Factory
- 4. Builder
- 5. Singleton (thread-safe variants)
- 6. Observer
- 7. State
- 8. Command
- 9. Decorator
- 10. Chain of Responsibility
- 11. Template Method
- 12. Adapter
- 13. Facade
- 14. Proxy
- 15. Repository
- Pattern selection cheat sheet
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.
| Pattern | Category | Typical LLD problem |
|---|---|---|
| Strategy | Behavioral | Pricing, fee calc, payment routing, eviction policy, rate-limit algo |
| Factory / Abstract Factory | Creational | Vehicle/payment-method/notification creation, per-network families |
| Builder | Creational | Complex requests (payment request, HTTP request, Pizza) |
| Singleton | Creational | Logger, config, ParkingLot instance, ID generator |
| Observer | Behavioral | Pub-sub, notifications, stock ticker, event bus |
| State | Behavioral | Vending machine, ATM, payment/order lifecycle, elevator |
| Command | Behavioral | Undo/redo, ATM ops, transaction queue, remote |
| Decorator | Structural | Pizza toppings, logging/fraud/retry around processor |
| Chain of Responsibility | Behavioral | Validation pipeline, ATM cash dispenser, logger levels, middleware |
| Template Method | Behavioral | Payment flow skeleton, report generation |
| Adapter | Structural | Wrapping third-party/legacy gateway SDK |
| Facade | Structural | Checkout/booking orchestration entry point |
| Proxy | Structural | Access control, caching, rate limiting, lazy load |
| Repository | Architectural | Persistence 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/elseon 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.