Design Explorer

LLD Designs

A small library of low-level design walkthrough pages and a generic design-pattern guide. This page explains the patterns in a reusable way, with simple real-world examples rather than project-specific code.

Pattern Guide

Generic design patterns with examples and purpose

These examples are intentionally generic. They are not tied to Snake and Ladder or Rate Limiter. The idea is to show what each pattern looks like, why we choose it, and what pain it removes.

Creational Patterns

These patterns help when object creation becomes repetitive, complex, or dependent on runtime choices.

Singleton

One shared instance

Real-world example: one application-wide logger or configuration manager.

Wrong
Logger a = new Logger(); Logger b = new Logger();
Use Singleton
class Logger { static Logger getInstance() { ... } } Logger logger = Logger.getInstance();
Why use it: when the system should have exactly one shared instance.
Problem it solves: avoids duplicate global state and inconsistent shared resources.

Factory Method

Create objects through a chooser

Real-world example: return `EmailNotifier` or `SmsNotifier` based on user settings.

Wrong
if (type == "sms") notifier = new SmsNotifier(); else notifier = new EmailNotifier();
Use Factory
Notifier makeNotifier(type) { return type == "sms" ? new SmsNotifier() : new EmailNotifier(); }
Why use it: when the exact class should be chosen at runtime.
Problem it solves: removes large constructor selection logic from the client.

Builder

Step-by-step object construction

Real-world example: building a `Report` with title, filters, charts, and export options.

Wrong
new Report("Sales", true, false, "pdf", ...);
Use Builder
Report report = new ReportBuilder() .title("Sales") .withChart() .build();
Why use it: when an object has many optional fields or configuration steps.
Problem it solves: avoids giant constructors that are hard to read and easy to misuse.

Prototype

Clone from an existing template

Real-world example: duplicate a dashboard layout with the same widgets and tweak a few settings.

Wrong
Dashboard copy = new Dashboard(); // repeat all widget setup again
Use Prototype
Dashboard copy = dashboardTemplate.clone(); copy.setTheme("dark");
Why use it: when copying an existing configured object is easier than rebuilding it.
Problem it solves: reduces repeated setup code for similar objects.

Facade

One clean entry point

Real-world example: a `TravelBookingService` that hides flight, hotel, payment, and ticket generation details.

Wrong
flight.book(); hotel.reserve(); payment.pay(); ticket.send();
Use Facade
tripService.bookTrip(user, from, to, dates);
Why use it: when the subsystem is useful but too noisy for direct use.
Problem it solves: reduces client coupling to many internal classes.

Adapter

Make incompatible APIs fit

Real-world example: adapt a third-party payment SDK to your own `PaymentGateway` interface.

Wrong
// client depends directly on externalSdk.makePayment(...)
Use Adapter
gateway.charge(amount); // adapter forwards to external SDK
Why use it: when existing code expects one interface but the new component exposes another.
Problem it solves: avoids rewriting either side just to make them talk.

Decorator

Add behavior without changing the base class

Real-world example: wrap a file reader with caching, then compression, then encryption.

Wrong
EncryptedCachedCompressedFileReader reader;
Use Decorator
Reader reader = new EncryptedReader(new CachedReader(new FileReader()));
Why use it: when features should be layered dynamically.
Problem it solves: avoids huge inheritance trees for combinations of features.

Composite

Treat groups and single items uniformly

Real-world example: a folder can contain files and other folders, but both expose the same operations like `size()`.

Wrong
if (item is File) ... else if (item is Folder) ...
Use Composite
int total = rootFolder.size(); // same call for file or folder
Why use it: when tree-shaped data should be handled consistently.
Problem it solves: removes special-case logic for single object vs collection node.

Behavioral Patterns

These patterns help organize how objects communicate, how logic changes, and how actions are triggered.

Strategy

Swap algorithms cleanly

Real-world example: choose between credit-card discounts, festival discounts, or loyalty discounts at checkout.

Wrong
if (festival) ... else if (loyalty) ... else if (card) ...
Use Strategy
checkout.setDiscountStrategy(new LoyaltyDiscount()); checkout.total();
Why use it: when several algorithms solve the same job in different ways.
Problem it solves: avoids giant `if/else` logic in the client.

Observer

Push updates to listeners

Real-world example: users subscribed to a product get notified when the price drops.

Wrong
while (true) checkPriceAgain();
Use Observer
product.attach(user); product.setPrice(499);
Why use it: when multiple listeners should react to one state change.
Problem it solves: avoids repeated polling and keeps update flow event-driven.

Command

Wrap actions as objects

Real-world example: a text editor stores `Copy`, `Paste`, and `Undo` as command objects.

Wrong
button.onClick = () => editor.copy();
Use Command
invoker.run(new CopyCommand(editor)); history.push(command);
Why use it: when actions need queuing, undo, retry, or logging.
Problem it solves: decouples the action invoker from action execution details.

State

Change behavior based on current mode

Real-world example: an order behaves differently in `Created`, `Paid`, `Shipped`, and `Cancelled` states.

Wrong
if (state == "paid") ... else if (state == "shipped") ...
Use State
order.pay(); order.ship();
Why use it: when object behavior truly changes with state.
Problem it solves: prevents scattered conditionals across the class.

Template Method

Reuse the same high-level steps

Real-world example: report generation always does `load`, `format`, `export`, but subclasses customize the details.

Wrong
salesReport.load(); salesReport.format(); salesReport.export();
Use Template Method
generateReport() { load(); format(); export(); }
Why use it: when a workflow is shared but a few steps vary.
Problem it solves: avoids duplicating the overall algorithm in many subclasses.

Chain of Responsibility

Pass a request through handlers

Real-world example: an approval request moves through team lead, manager, and director based on amount.

Wrong
if (amount < 1000) lead(); else if (amount < 5000) manager(); else director();
Use Chain
teamLead.setNext(manager).setNext(director); teamLead.handle(request);
Why use it: when multiple handlers may process the same request in order.
Problem it solves: keeps sender and receiver loosely coupled.