Structural
Patterns
Facade · Bridge · Flyweight
Structural patterns deal with object composition — how classes and objects are assembled into larger structures. They ensure that when one part changes, the entire structure doesn't need to be redesigned.
| PATTERN | TRIGGER / SMELL | KEY MECHANISM | REAL WORLD |
|---|---|---|---|
| Adapter | Incompatible interface from third-party/legacy | Wrapper translates interface A → interface B | Payment gateways, legacy DB drivers |
| Decorator | N features × M objects = too many subclasses | IS-A + HAS-A same type; wraps and delegates | Java I/O streams, HTTP middleware |
| Proxy | Need auth/caching/logging without touching real object | Same interface, intercepts before delegating | Spring AOP, Hibernate lazy loading, CDN |
| Composite | Tree structure; want uniform treatment of leaf/branch | Component interface; Composite holds children | HTML DOM, UI widget trees, file system |
| Facade | Client must coordinate many subsystem classes | High-level class orchestrates subsystems | Spring ApplicationContext, SDK clients |
| Bridge | Two orthogonal dimensions exploding into N×M classes | Abstraction holds Implementation reference (bridge) | Notification type × channel, JDBC drivers |
| Flyweight | Millions of objects exhausting memory | Shared intrinsic state in factory pool; extrinsic passed in | Java String pool, game particles, TrueCaller |
// Your system expects this interface interface VendingMachine { void insertCoin(int amount); // amount in paise void selectProduct(String m3-code); // e.g. "A1", "B2" void dispense(); int getChange(); } // Third-party machine — incompatible interface class NewVendorMachine { public void payAmount(double rupees) { /* ... */ } public void chooseItem(int itemId) { /* ... */ } public void releaseItem() { /* ... */ } public double calculateChange() { return 5.50; } } // ADAPTER — wraps new vendor machine, speaks old interface class VendingMachineAdapter implements VendingMachine { private final NewVendorMachine adaptee; public VendingMachineAdapter(NewVendorMachine m) { this.adaptee = m; } @Override public void insertCoin(int amount) { adaptee.payAmount(amount / 100.0); // paise → rupees } @Override public void selectProduct(String m3-code) { int id = codeToId.get(m3-code); // "A1" → 1 adaptee.chooseItem(id); } @Override public void dispense() { adaptee.releaseItem(); } @Override public int getChange() { return (int)(adaptee.calculateChange() * 100); } } // Client m3-code unchanged — still speaks VendingMachine VendingMachine vm = new VendingMachineAdapter(new NewVendorMachine()); vm.insertCoin(1000); vm.selectProduct("A1"); vm.dispense();
interface Pizza { String getDescription(); double getCost(); } class MargheritaPizza implements Pizza { public String getDescription() { return "Margherita"; } public double getCost() { return 200.0; } } // ABSTRACT DECORATOR — IS-A Pizza AND HAS-A Pizza abstract class ToppingDecorator implements Pizza { protected final Pizza pizza; public ToppingDecorator(Pizza p) { this.pizza = p; } } // Concrete decorators — each adds exactly one topping class CheeseDecorator extends ToppingDecorator { public CheeseDecorator(Pizza p) { super(p); } public String getDescription() { return pizza.getDescription() + " + Cheese"; } public double getCost() { return pizza.getCost() + 50.0; } } class MushroomDecorator extends ToppingDecorator { public String getDescription() { return pizza.getDescription() + " + Mushroom"; } public double getCost() { return pizza.getCost() + 35.0; } public MushroomDecorator(Pizza p) { super(p); } } // Runtime composition — any order, any combination Pizza order = new CheeseDecorator( new MushroomDecorator( new CheeseDecorator( // double cheese! new MargheritaPizza()))); // → "Margherita + Cheese + Mushroom + Cheese" cost: 335.0
interface CarRentalService { Car rentCar(String model, User user); void returnCar(String carId, User user); } class CarRentalProxy implements CarRentalService { private final CarRentalServiceImpl real; private final AuthService auth; private final Logger log; @Override public Car rentCar(String model, User user) { // 1. Authorization (Protection Proxy) if (!auth.hasValidLicense(user)) throw new UnauthorizedException("No valid license"); // 2. Pre-logging log.log("Renting " + model + " for user " + user.getId()); // 3. Delegate to real service Car car = real.rentCar(model, user); // 4. Post-logging log.log("Assigned car " + car.getId()); return car; } // returnCar similarly delegates after logging } // Client sees same interface — proxy is completely transparent CarRentalService svc = new CarRentalProxy(real, auth, log); Car c = svc.rentCar("Camry", currentUser);
Three Proxy Types
- Virtual: Lazy initialisation — defer expensive creation
- Protection: Auth/permissions check before delegating
- Remote: Represents object in different process (gRPC stub)
Real-World Proxies
- Spring @Transactional, @Cacheable → runtime proxy
- Hibernate lazy loading → Virtual proxy
- gRPC generated stubs → Remote proxy
- CDN → Remote proxy for assets
// Uniform component interface — same for File AND Directory interface FileSystemComponent { String getName(); long getSize(); void display(String indent); void delete(); } // LEAF — no children class File implements FileSystemComponent { private final String name; private final long size; public long getSize() { return size; } public void display(String ind) { System.out.println(ind + "📄 " + name); } public void delete() { System.out.println("Delete file: " + name); } public String getName() { return name; } public File(String n, long s) { name=n; size=s; } } // COMPOSITE — holds children, operations recurse class Directory implements FileSystemComponent { private final String name; private final List<FileSystemComponent> children = new ArrayList<>(); public void add(FileSystemComponent c) { children.add(c); } // Recursive — works for any depth of nesting public long getSize() { return children.stream().mapToLong(FileSystemComponent::getSize).sum(); } public void display(String ind) { System.out.println(ind + "📁 " + name + " (" + getSize() + " B)"); children.forEach(c -> c.display(ind + " ")); } public void delete() { children.forEach(FileSystemComponent::delete); } public String getName() { return name; } public Directory(String n) { name = n; } } // Client — no instanceof, no type checks needed Directory root = new Directory("root"); root.add(new File("README.md", 256)); root.add(src); // src is a Directory — same add() call root.getSize(); // Recursively sums all nested files
// Complex subsystems — many classes, many responsibilities class ExpenseService { Expense createExpense(...) {...} } class SplitCalculator { Map calculateEqualSplit(...) {...} } class BalanceService { Map getNetBalances(...) {...} } class NotificationService { void notifyMembers(...) {...} } // FACADE — one class, simple operations, hides all complexity class SplitwiseFacade { private final ExpenseService expenses; private final SplitCalculator calculator; private final BalanceService balances; private final NotificationService notifier; // High-level operation — orchestrates 4 subsystems public void addExpenseEqualSplit(String groupId, String desc, double amount, String paidBy, List<String> members) { Expense expense = expenses.createExpense(desc, amount, paidBy); Map splits = calculator.calculateEqualSplit(expense, members); balances.updateBalances(groupId, splits, paidBy); notifier.notifyExpenseAdded(members, expense); } public List<Transaction> getSimplifiedSettlements(String groupId) { Map<String, Double> netBalances = balances.getNetBalances(groupId); return SimplifyAlgorithm.simplify(netBalances); // min transactions } } // Client — one method call does what used to take 10 SplitwiseFacade sw = new SplitwiseFacade(); sw.addExpenseEqualSplit("grp1", "Dinner", 1200.0, "ajay", members);
// IMPLEMENTATION — HOW to send (one dimension) interface NotificationSender { void send(String to, String msg); } class SMSSender implements NotificationSender { /* ... */ } class EmailSender implements NotificationSender { /* ... */ } class PushSender implements NotificationSender { /* ... */ } // ABSTRACTION — WHAT to send (other dimension) abstract class CricketNotification { protected final NotificationSender sender; // THE BRIDGE public CricketNotification(NotificationSender s) { this.sender = s; } public abstract void notify(String recipient, Object event); } // Refined abstractions — each is a notification type class WicketNotification extends CricketNotification { public WicketNotification(NotificationSender s) { super(s); } public void notify(String r, Object e) { sender.send(r, "WICKET! " + e + " is out!"); // uses bridge } } class SixNotification extends CricketNotification { public SixNotification(NotificationSender s) { super(s); } public void notify(String r, Object e) { sender.send(r, "SIX! " + e + " smashes it!"); } } // Mix and match — N types × M channels without N×M classes new WicketNotification(new SMSSender()).notify("user1", "Kohli"); new WicketNotification(new EmailSender()).notify("fan@email", "Rohit"); new SixNotification(new PushSender()).notify("device_xyz", "Dhoni");
// FLYWEIGHT — stores INTRINSIC state (shared, immutable) class ContactMetadata { private final String operatorName; // "Jio", "Airtel" — thousands share this private final String contactType; // "SPAM", "BUSINESS" — few unique values private final String spamLabel; // "Telemarketer", null — shared // All final — immutable, so safely shared across threads } // FLYWEIGHT FACTORY — pool ensures reuse class ContactMetadataFactory { private static final Map<String,ContactMetadata> pool = new HashMap<>(); public static ContactMetadata get(String op, String type, String spam) { String key = op + "|" + type + "|" + spam; return pool.computeIfAbsent(key, k -> new ContactMetadata(op, type, spam)); } } // CLIENT CONTEXT — stores EXTRINSIC state (unique per contact) class PhoneContact { private final String phoneNumber; // unique — extrinsic private final String callerName; // unique — extrinsic private final ContactMetadata meta; // SHARED — flyweight public PhoneContact(String num, String name, String op, String type, String spam) { this.phoneNumber = num; this.callerName = name; this.meta = ContactMetadataFactory.get(op, type, spam); // pool lookup } } // Memory impact (1 billion contacts): // Without Flyweight: 1B × 200B metadata = 200 GB // With Flyweight: ~1000 unique combos × 200B = 200 KB shared // + 1B × ~30B (phone + name only) = 30 GB extrinsic
Decorator: "I need to add features to this object without modifying its class"
Proxy: "I need to control who/how accesses this object"
Composite: "I have a tree and want leaf + branch to behave the same"
Facade: "I want to hide this complex subsystem behind one simple class"
Bridge: "I have two dimensions of variation and don't want N×M subclasses"
Flyweight: "I have millions of similar objects and I'm running out of memory"
| PATTERN | ADVANTAGE | TRADE-OFF |
|---|---|---|
| Adapter | Integration without touching existing m3-code | Extra indirection; translation bugs possible |
| Decorator | Infinite runtime combinations, no subclass explosion | Deep chains hard to debug; decoration order matters |
| Proxy | Transparent cross-cutting concerns | Extra indirection; proxy-related bugs subtle |
| Composite | Uniform tree operations, no instanceof | Hard to restrict component types in tree |
| Facade | Simplifies client m3-code dramatically | Can become a god object if over-loaded |
| Bridge | Two dimensions vary independently | Up-front complexity; must identify dimensions correctly |
| Flyweight | Massive memory savings | Client must manage extrinsic state; no object identity |
Complete LLD implementation with the Simplify Algorithm. This is the most architecturally rich problem in Module A3 — it naturally requires Facade + Algorithm + Adapter + Decorator + Composite.
Problem: Given a group's net balances (positive = owed money, negative = owes money), find the minimum number of transactions to settle all debts.
- 1Calculate net balance per person: sum what they paid, minus what they owe across all expenses.
- 2Separate into creditors (positive balance — owed money) and debtors (negative balance — owe money).
- 3Use two priority queues: max-heap of creditors, min-heap of debtors.
- 4Greedy loop: take largest creditor + largest debtor. Settle min(credit, debt). If credit > debt, creditor still has balance → re-insert remainder.
- 5Each loop iteration = one transaction. Loop ends when all queues empty = all debts settled.
Naive: up to 4 transactions. Algorithm: 3 minimum
→ Ram pays Ajay 400 | Priya pays Ajay 200 | Sita pays Rahul 100
| COMPONENT | PATTERN | WHY THIS PATTERN |
|---|---|---|
| SplitwiseFacade | Facade | Unified API hiding UserService, ExpenseService, BalanceService, Notifier |
| EqualSplit / PctSplit / ExactSplit | Strategy (preview A4) | Interchangeable algorithms for splitting an expense |
| TaxDecorator, ServiceChargeDecorator | Decorator | Add charges to base expense dynamically at runtime |
| WhatsAppAdapter, EmailAdapter | Adapter | Normalize incompatible third-party notification APIs |
| User + Group (for notifications) | Composite | Notify individual or entire group with same call |
Identify the correct Structural pattern. One sentence justification each.
1. Payment lib accepts PaymentRequest, your system has Order objects. 2. Security system logs all DB access attempts without modifying DB class. 3. Panel can contain Button, Label, or another Panel. All support render(). 4. HomeController.leaveHome() controls Lights, Security, Climate at once. 5. 10,000 bullets/sec — each bullet has unique position, shared appearance. 6. Notification type (Alert/Reminder) independent of channel (SMS/Email/Push).
Implement Logger decorators that compose in any order.
Base: ConsoleLogger — prints to stdout Decorators: TimestampDecorator — prepends "[2024-01-15 14:23:05]" LevelDecorator — prepends [INFO] / [WARN] / [ERROR] FileDecorator — ALSO writes to a log file Test all 4 combinations: new TimestampDecorator(new LevelDecorator(new ConsoleLogger())) new LevelDecorator(new TimestampDecorator(new ConsoleLogger())) new FileDecorator(new TimestampDecorator(new ConsoleLogger())) new FileDecorator(new LevelDecorator(new TimestampDecorator(new ConsoleLogger()))) Show output for each — confirm composition is correct.
Implement a Virtual+Caching Proxy for an expensive weather API.
interface WeatherService {
WeatherData getWeather(String city); // expensive HTTP call
}
class RealWeatherService implements WeatherService {
// Simulate 500ms HTTP call
}
class WeatherServiceProxy implements WeatherService {
// Cache: Map<city, CacheEntry(data, timestamp)>
// TTL: 5 minutes
// Hit: return cache, log "cache hit"
// Miss: call real service, store, log "cache miss"
}
Test: Call getWeather("Mumbai") 3 times within 5 min
→ only 1 real HTTP call, 2 cache hits
Wait 5 min, call again → cache miss, new HTTP call
Complete LLD implementation of Splitwise clone.
Implement: 1. SplitwiseFacade with all 4 subsystems 2. EqualSplit, PercentageSplit, ExactSplit strategies 3. SimplifyAlgorithm (greedy, priority queue approach) 4. TaxDecorator and ServiceChargeDecorator for Expense 5. NotificationAdapter for at least 2 channels 6. UML class diagram showing all patterns Demo scenario: - 5 members: Ajay, Ram, Priya, Rahul, Sita - Add 6 expenses (mix of split types) - Print net balances - Print simplified settlement plan - Settle one transaction, reprint balances