Low Level Design
Comprehensive Low-Level Design guide: OOP principles, SOLID principles, GoF design patterns (Creational, Structural, Behavioral), and real-world system modeling.
Low Level Design#
Hey, welcome to the course. I hope this course provides a great learning experience.
This guide covers Low Level Design (LLD) from basics to advanced, structured for students and developers preparing for software engineering interviews and real-world system implementation.
Table of contents#
-
Getting Started
-
Chapter I — OOP Foundations
-
Chapter II — SOLID Principles
-
Chapter III — UML and Modeling
-
Chapter IV — Design Patterns: Creational
-
Chapter V — Design Patterns: Structural
-
Chapter VI — Design Patterns: Behavioral
-
Chapter VII — Advanced OOP Concepts
-
Chapter VIII — LLD Case Studies
-
Appendix
What is Low Level Design?#
Before we start this course, let's talk about what Low Level Design actually means.
Low Level Design (LLD) is the process of designing the internal logic, class structure, and data flow of individual components within a system. While High Level Design (HLD) focuses on the macro-architecture — services, databases, and how they communicate — LLD zooms into the micro-architecture: how classes are structured, how objects interact, what design patterns apply, and how code is organized for maintainability and extensibility.
LLD answers questions like:
- What classes and interfaces should we define?
- How do these classes relate to each other?
- Which design pattern fits this use case?
- How do we ensure the code is flexible and easy to change?
Why is Low Level Design so important?#
LLD is what separates a working program from a well-engineered one. Poor LLD leads to tightly coupled, fragile code that breaks whenever requirements change. Good LLD results in clean, modular code that is easy to read, test, and extend.
It is a critical skill evaluated in software engineering interviews at product-based companies. Interviewers use LLD problems to assess whether a candidate can think in abstractions, apply OOP principles, and make pragmatic design decisions under time pressure.
Object-Oriented Programming (OOP)#
Object-Oriented Programming (OOP) is a programming paradigm centered around the concept of objects — entities that bundle together state (data) and behavior (methods). OOP is the foundation of Low Level Design.
The four core pillars of OOP are:
- Encapsulation — hiding internal state and exposing behavior through a well-defined interface.
- Abstraction — representing essential features without exposing implementation details.
- Inheritance — allowing a class to derive properties and behavior from another class.
- Polymorphism — enabling different classes to be treated through a common interface.
OOP models real-world entities as objects, making code more intuitive, reusable, and maintainable. Most major languages — Java, Python, C++, TypeScript — are OOP-friendly.
Why OOP?#
Procedural code works, but it does not scale gracefully. As requirements grow, procedural programs become harder to extend without breaking existing functionality. OOP provides structure: responsibility is clearly assigned to classes, and changes are localized rather than sprawling.
Classes and Objects#
A class is a blueprint that defines the properties (attributes) and behaviors (methods) an object will have. An object is a concrete instance of that class.
javaclass Car {
private String brand;
private int speed;
public Car(String brand, int speed) {
this.brand = brand;
this.speed = speed;
}
public void accelerate(int amount) {
this.speed += amount;
}
public int getSpeed() {
return speed;
}
}
// Object instantiation
Car myCar = new Car("Toyota", 0);
myCar.accelerate(60);
System.out.println(myCar.getSpeed()); // 60
Key Terminology#
| Term | Definition |
|---|---|
| Class | Template or blueprint for objects |
| Object | Instance of a class |
| Attribute | Data/state stored in an object |
| Method | Behavior or function defined in a class |
| Constructor | Special method to initialize objects |
| Instance | A specific object created from a class |
Encapsulation#
Encapsulation is the practice of bundling data (attributes) and methods that operate on that data within a single unit (class), and restricting direct access to some of the object's internals. It is achieved using access modifiers.
javaclass BankAccount {
private double balance; // hidden from outside
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount > 0) balance += amount;
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) balance -= amount;
}
public double getBalance() {
return balance;
}
}
Access Modifiers#
| Modifier | Same Class | Same Package | Subclass | Everywhere |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
| (default) | ✅ | ✅ | ❌ | ❌ |
Benefits#
- Data integrity — internal state can only be changed in controlled ways.
- Reduced coupling — external code depends on the interface, not the implementation.
- Easier refactoring — internal changes don't affect external users of the class.
Abstraction#
Abstraction means showing only the essential details to the user and hiding the underlying complexity. It lets you work with high-level concepts without worrying about how they are implemented.
In OOP, abstraction is achieved through:
- Abstract classes — classes that cannot be instantiated and may have both concrete and abstract methods.
- Interfaces — contracts that define what a class must do, without specifying how.
javaabstract class Shape {
abstract double area(); // must be implemented by subclass
public void printArea() {
System.out.println("Area: " + area());
}
}
class Circle extends Shape {
private double radius;
Circle(double radius) { this.radius = radius; }
@Override
double area() {
return Math.PI * radius * radius;
}
}
Inheritance#
Inheritance allows a class (child/subclass) to acquire the properties and methods of another class (parent/superclass). It promotes code reuse.
javaclass Animal {
String name;
void eat() {
System.out.println(name + " is eating.");
}
}
class Dog extends Animal {
void bark() {
System.out.println(name + " is barking.");
}
}
Dog d = new Dog();
d.name = "Rex";
d.eat(); // inherited
d.bark(); // own method
Types of Inheritance#
| Type | Description |
|---|---|
| Single | One child inherits from one parent |
| Multilevel | Chain: A → B → C |
| Hierarchical | Multiple children from one parent |
| Multiple | One child from multiple parents (supported in C++; via interfaces in Java) |
When to use Inheritance#
Use inheritance when the relationship is truly "is-a". A Dog is an Animal. A Car is a Vehicle. Avoid using inheritance just to reuse code — that's where Composition shines (see Composition vs Inheritance).
Polymorphism#
Polymorphism means "many forms". It allows objects of different types to be treated through a common interface, with each type responding in its own way.
Compile-time Polymorphism (Method Overloading)#
javaclass Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
}
Runtime Polymorphism (Method Overriding)#
javaclass Animal {
void speak() { System.out.println("Some sound"); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("Woof!"); }
}
class Cat extends Animal {
@Override
void speak() { System.out.println("Meow!"); }
}
Animal a = new Dog();
a.speak(); // Woof! — resolved at runtime
Interfaces and Abstract Classes#
Interfaces#
An interface is a contract — it defines what methods a class must implement, without any implementation detail. A class can implement multiple interfaces.
javainterface Printable {
void print();
}
interface Saveable {
void save();
}
class Document implements Printable, Saveable {
public void print() { System.out.println("Printing..."); }
public void save() { System.out.println("Saving..."); }
}
Abstract Classes#
An abstract class can have both abstract (unimplemented) methods and concrete (implemented) methods. A class can only extend one abstract class.
javaabstract class Vehicle {
String brand;
abstract void fuelType(); // must implement
void start() { System.out.println("Starting " + brand); } // concrete
}
Interface vs Abstract Class#
| Feature | Interface | Abstract Class |
|---|---|---|
| Methods | Abstract by default (Java 8+ allows default) | Can be abstract or concrete |
| Variables | public static final | Any access modifier |
| Multiple inheritance | ✅ | ❌ |
| Constructor | ❌ | ✅ |
| Use case | Define a contract/capability | Share common base logic |
Composition vs Inheritance#
Composition means building complex objects by combining simpler ones — "has-a" relationship. Inheritance is "is-a" relationship.
The rule of thumb: Favor composition over inheritance.
java// Inheritance (tight coupling)
class FlyingDuck extends Duck {
void fly() { ... }
}
// Composition (flexible)
interface FlyBehavior {
void fly();
}
class FlyWithWings implements FlyBehavior {
public void fly() { System.out.println("Flying with wings!"); }
}
class Duck {
FlyBehavior flyBehavior; // composed
Duck(FlyBehavior fb) { this.flyBehavior = fb; }
void performFly() { flyBehavior.fly(); }
}
Composition makes behavior swappable at runtime, whereas inheritance locks it at compile time.
Single Responsibility Principle (SRP)#
"A class should have one, and only one, reason to change." — Robert C. Martin
Every class should do exactly one thing. If a class handles user authentication AND sends emails AND logs activity, it has multiple reasons to change. Split responsibilities across multiple classes.
java// ❌ Bad — multiple responsibilities
class UserService {
void registerUser(User u) { ... }
void sendWelcomeEmail(User u) { ... }
void logRegistration(User u) { ... }
}
// ✅ Good — single responsibility each
class UserRegistrationService {
void registerUser(User u) { ... }
}
class EmailService {
void sendWelcomeEmail(User u) { ... }
}
class AuditLogger {
void log(String event) { ... }
}
Open/Closed Principle (OCP)#
"Software entities should be open for extension, but closed for modification."
You should be able to add new functionality without changing existing code. Achieve this with abstraction and polymorphism.
java// ❌ Bad — must modify existing code to add new shape
class AreaCalculator {
double calculate(Object shape) {
if (shape instanceof Circle) { ... }
else if (shape instanceof Rectangle) { ... }
// Add triangle? Must touch this class.
}
}
// ✅ Good — extend without modification
interface Shape {
double area();
}
class Circle implements Shape { public double area() { ... } }
class Rectangle implements Shape { public double area() { ... } }
class Triangle implements Shape { public double area() { ... } } // new, no change to calculator
class AreaCalculator {
double calculate(Shape shape) {
return shape.area();
}
}
Liskov Substitution Principle (LSP)#
"Objects of a superclass should be replaceable with objects of a subclass without breaking the application."
If class B extends class A, you must be able to use B wherever A is expected, with no surprises.
java// ❌ Bad — Square breaks Rectangle's contract
class Rectangle {
int width, height;
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
@Override
void setWidth(int w) { width = height = w; } // breaks contract!
@Override
void setHeight(int h) { width = height = h; }
}
// ✅ Good — model them separately
interface Shape { int area(); }
class Rectangle implements Shape { ... }
class Square implements Shape { ... }
Interface Segregation Principle (ISP)#
"No client should be forced to depend on methods it does not use."
Split large interfaces into smaller, more specific ones. Clients should only know about the methods that matter to them.
java// ❌ Bad — fat interface
interface Worker {
void work();
void eat();
void sleep();
}
// A Robot implements Worker but doesn't eat or sleep — forced to implement empty methods.
// ✅ Good — segregated interfaces
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }
class Human implements Workable, Eatable, Sleepable { ... }
class Robot implements Workable { ... } // only what it needs
Dependency Inversion Principle (DIP)#
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Depend on interfaces or abstract classes, not concrete implementations.
java// ❌ Bad — high-level depends on low-level concrete class
class NotificationService {
EmailSender emailSender = new EmailSender(); // tightly coupled
void notify(String msg) {
emailSender.send(msg);
}
}
// ✅ Good — depend on abstraction
interface MessageSender {
void send(String msg);
}
class EmailSender implements MessageSender { public void send(String msg) { ... } }
class SMSSender implements MessageSender { public void send(String msg) { ... } }
class NotificationService {
private MessageSender sender;
NotificationService(MessageSender sender) { // injected
this.sender = sender;
}
void notify(String msg) { sender.send(msg); }
}
UML Diagrams#
Unified Modeling Language (UML) is a standardized visual language for designing and documenting software systems. In LLD, UML is your primary tool for communicating design decisions.
Why UML?#
UML gives a common vocabulary to engineers, architects, and stakeholders. In interviews, sketching a class diagram communicates your design faster and more clearly than describing it in words.
Types of UML Diagrams#
| Type | Category | Purpose |
|---|---|---|
| Class Diagram | Structural | Classes, relationships, attributes, methods |
| Sequence Diagram | Behavioral | Object interaction over time |
| Use Case Diagram | Behavioral | Actor-system interaction |
| Activity Diagram | Behavioral | Workflow and control flow |
| State Machine | Behavioral | Object state transitions |
| Component Diagram | Structural | System components and dependencies |
Class Diagrams#
A class diagram shows the static structure of the system — classes, their attributes, methods, and relationships.
Class Notation#
code+---------------------------+ | ClassName | +---------------------------+ | - privateAttr: Type | | # protectedAttr: Type | | + publicAttr: Type | +---------------------------+ | + publicMethod(): Return | | - privateMethod(): void | +---------------------------+
Relationships#
| Relationship | Symbol | Meaning |
|---|---|---|
| Association | — | Class A uses Class B |
| Aggregation | ◇— | "has-a", B can exist without A |
| Composition | ◆— | "has-a", B cannot exist without A |
| Inheritance | △— | "is-a", subclass extends superclass |
| Realization | △- - | Class implements interface |
| Dependency | - -> | Class temporarily uses another |
Multiplicity#
code1 — exactly one 0..1 — zero or one * — zero or more 1..* — one or more m..n — between m and n
Sequence Diagrams#
A sequence diagram shows how objects interact in a specific sequence over time. It is perfect for modeling a use case flow or an API call chain.
Notation#
- Lifelines — vertical dashed lines representing objects.
- Activation boxes — rectangles on lifelines when an object is active.
- Messages — horizontal arrows between lifelines (solid = call, dashed = return).
- Alt/Loop frames — conditional or looping interactions.
Example: User Login Flow#
codeUser AuthController AuthService Database | | | | |--login(creds)-->| | | | |--authenticate()->| | | | |--findUser()--> | | | |<--User--------| | | |--verifyPass() | | |<--token----------| | |<--200 + token---| | |
Use Case Diagrams#
A use case diagram shows how actors (users, external systems) interact with the system to accomplish goals. It is a high-level view of functionality.
Notation#
- Actor — stick figure (user, admin, external system)
- Use Case — oval with a short action verb phrase
- System Boundary — rectangle wrapping use cases
- Relationships —
include(always),extend(conditionally)
Example: Library System#
code+------------------------------------+ | Library System | | | | (Search Book) (Borrow Book) | | (Return Book) (Pay Fine) | +------------------------------------+ ^ ^ Member Librarian
Activity Diagrams#
An activity diagram represents workflows and logic flows — similar to a flowchart but richer. It shows sequential and parallel activities.
Notation#
| Symbol | Meaning |
|---|---|
| Filled circle | Start |
| Filled circle with ring | End |
| Rectangle with rounded corners | Activity |
| Diamond | Decision/Branch |
| Thick horizontal bar | Fork/Join (parallel) |
| Arrow | Flow |
State Machine Diagrams#
A state machine diagram models how an object transitions between states based on events. It is useful for modeling entities like orders, tickets, or connections.
Notation#
- State — rounded rectangle
- Transition — arrow labeled with event / [guard] / action
- Initial state — filled circle
- Final state — filled circle with outer ring
Example: Order States#
code[●] --> (Placed) --payment received--> (Confirmed) | item shipped | (Shipped) --delivered--> (Delivered) | cancelled | (Cancelled)
Singleton Pattern#
Category: Creational
The Singleton pattern ensures that a class has only one instance and provides a global access point to it.
When to use#
- Database connection pools
- Logger instances
- Configuration managers
- Thread pools
Implementation (Thread-safe)#
javapublic class Singleton {
private static volatile Singleton instance;
private Singleton() {} // private constructor
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) { // double-checked locking
instance = new Singleton();
}
}
}
return instance;
}
}
Pros and Cons#
| Pros | Cons |
|---|---|
| Controlled access to single instance | Hard to unit test (global state) |
| Lazy initialization | Can hide dependencies |
| Resource efficient | Violates SRP if overused |
Factory Method Pattern#
Category: Creational
The Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate. It decouples object creation from object use.
javainterface Notification {
void send(String message);
}
class EmailNotification implements Notification {
public void send(String message) { System.out.println("Email: " + message); }
}
class SMSNotification implements Notification {
public void send(String message) { System.out.println("SMS: " + message); }
}
class NotificationFactory {
public static Notification create(String type) {
return switch (type) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SMSNotification();
default -> throw new IllegalArgumentException("Unknown type");
};
}
}
// Usage
Notification n = NotificationFactory.create("EMAIL");
n.send("Hello!");
Abstract Factory Pattern#
Category: Creational
The Abstract Factory provides an interface for creating families of related objects without specifying their concrete classes. Think of it as a factory of factories.
javainterface Button { void render(); }
interface Checkbox { void render(); }
class WindowsButton implements Button { public void render() { System.out.println("Windows Button"); } }
class MacButton implements Button { public void render() { System.out.println("Mac Button"); } }
class WindowsCheckbox implements Checkbox { public void render() { System.out.println("Windows Checkbox"); } }
class MacCheckbox implements Checkbox { public void render() { System.out.println("Mac Checkbox"); } }
interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
class WindowsFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
class MacFactory implements UIFactory {
public Button createButton() { return new MacButton(); }
public Checkbox createCheckbox() { return new MacCheckbox(); }
}
Builder Pattern#
Category: Creational
The Builder pattern constructs complex objects step-by-step. It separates object construction from its representation, making it easy to produce different representations.
When to use#
- Object has many optional parameters.
- Telescoping constructor anti-pattern (too many constructor overloads).
- Object construction requires multiple steps.
javaclass Pizza {
private String size;
private boolean cheese;
private boolean pepperoni;
private boolean mushrooms;
private Pizza(Builder builder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.mushrooms = builder.mushrooms;
}
public static class Builder {
private String size;
private boolean cheese = false;
private boolean pepperoni = false;
private boolean mushrooms = false;
public Builder(String size) { this.size = size; }
public Builder cheese() { this.cheese = true; return this; }
public Builder pepperoni() { this.pepperoni = true; return this; }
public Builder mushrooms() { this.mushrooms = true; return this; }
public Pizza build() { return new Pizza(this); }
}
}
// Usage
Pizza pizza = new Pizza.Builder("Large")
.cheese()
.pepperoni()
.build();
Prototype Pattern#
Category: Creational
The Prototype pattern creates new objects by cloning an existing object (the prototype), instead of creating from scratch.
javaabstract class Shape implements Cloneable {
String color;
abstract double area();
public Shape clone() {
try { return (Shape) super.clone(); }
catch (CloneNotSupportedException e) { return null; }
}
}
class Circle extends Shape {
double radius;
Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI * radius * radius; }
}
// Usage
Circle c1 = new Circle(5);
c1.color = "Red";
Circle c2 = (Circle) c1.clone(); // deep copy
c2.color = "Blue"; // doesn't affect c1
Adapter Pattern#
Category: Structural
The Adapter pattern allows incompatible interfaces to work together. It wraps an existing class with a new interface.
java// Old interface
class OldPaymentGateway {
void makePayment(int amountInPaisa) { ... }
}
// New interface the app expects
interface PaymentProcessor {
void pay(double amountInRupees);
}
// Adapter
class PaymentAdapter implements PaymentProcessor {
private OldPaymentGateway gateway;
PaymentAdapter(OldPaymentGateway gateway) {
this.gateway = gateway;
}
public void pay(double amountInRupees) {
int paisa = (int)(amountInRupees * 100);
gateway.makePayment(paisa);
}
}
Bridge Pattern#
Category: Structural
The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. Use it to avoid a permanent binding between abstraction and implementation.
javainterface Renderer {
void renderCircle(double radius);
}
class VectorRenderer implements Renderer {
public void renderCircle(double radius) {
System.out.println("Drawing circle with radius " + radius + " [Vector]");
}
}
class RasterRenderer implements Renderer {
public void renderCircle(double radius) {
System.out.println("Drawing circle with radius " + radius + " [Raster]");
}
}
abstract class Shape {
protected Renderer renderer;
Shape(Renderer renderer) { this.renderer = renderer; }
abstract void draw();
}
class Circle extends Shape {
double radius;
Circle(Renderer r, double radius) { super(r); this.radius = radius; }
public void draw() { renderer.renderCircle(radius); }
}
Composite Pattern#
Category: Structural
The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. Clients treat individual objects and compositions uniformly.
javainterface FileSystemItem {
void display(String indent);
}
class File implements FileSystemItem {
String name;
File(String name) { this.name = name; }
public void display(String indent) { System.out.println(indent + name); }
}
class Directory implements FileSystemItem {
String name;
List<FileSystemItem> children = new ArrayList<>();
Directory(String name) { this.name = name; }
void add(FileSystemItem item) { children.add(item); }
public void display(String indent) {
System.out.println(indent + name + "/");
for (FileSystemItem item : children) item.display(indent + " ");
}
}
Decorator Pattern#
Category: Structural
The Decorator pattern attaches additional responsibilities to an object dynamically. It wraps the original object, adding new behavior before or after delegating to it.
javainterface Coffee {
String getDescription();
double getCost();
}
class SimpleCoffee implements Coffee {
public String getDescription() { return "Coffee"; }
public double getCost() { return 50.0; }
}
class MilkDecorator implements Coffee {
private Coffee coffee;
MilkDecorator(Coffee c) { this.coffee = c; }
public String getDescription() { return coffee.getDescription() + ", Milk"; }
public double getCost() { return coffee.getCost() + 10.0; }
}
class SugarDecorator implements Coffee {
private Coffee coffee;
SugarDecorator(Coffee c) { this.coffee = c; }
public String getDescription() { return coffee.getDescription() + ", Sugar"; }
public double getCost() { return coffee.getCost() + 5.0; }
}
// Usage
Coffee c = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println(c.getDescription()); // Coffee, Milk, Sugar
System.out.println(c.getCost()); // 65.0
Facade Pattern#
Category: Structural
The Facade pattern provides a simplified interface to a complex subsystem. It doesn't add new functionality — it makes existing functionality easier to use.
javaclass CPU { void freeze() {...} void jump(long pos) {...} void execute() {...} }
class Memory { void load(long pos, byte[] data) {...} }
class HardDrive { byte[] read(long lba, int size) {...} }
// Facade
class ComputerFacade {
private CPU cpu = new CPU();
private Memory memory = new Memory();
private HardDrive hd = new HardDrive();
public void start() {
cpu.freeze();
memory.load(0, hd.read(0, 1024));
cpu.jump(0);
cpu.execute();
}
}
// Client only needs to know one thing
new ComputerFacade().start();
Flyweight Pattern#
Category: Structural
The Flyweight pattern uses sharing to efficiently support a large number of similar objects. It separates intrinsic state (shared) from extrinsic state (unique per object).
javaclass TreeType { // flyweight — shared
String name, color, texture;
TreeType(String name, String color, String texture) { ... }
void draw(int x, int y) { ... }
}
class TreeTypeFactory {
private static Map<String, TreeType> cache = new HashMap<>();
public static TreeType get(String name, String color, String texture) {
String key = name + color + texture;
return cache.computeIfAbsent(key, k -> new TreeType(name, color, texture));
}
}
class Tree { // unique per object
int x, y;
TreeType type; // shared
Tree(int x, int y, TreeType type) { this.x = x; this.y = y; this.type = type; }
}
Proxy Pattern#
Category: Structural
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Useful for lazy initialization, access control, logging, and caching.
javainterface Image {
void display();
}
class RealImage implements Image {
String file;
RealImage(String file) {
this.file = file;
loadFromDisk(); // expensive
}
private void loadFromDisk() { System.out.println("Loading " + file); }
public void display() { System.out.println("Displaying " + file); }
}
class ProxyImage implements Image {
private RealImage realImage;
private String file;
ProxyImage(String file) { this.file = file; }
public void display() {
if (realImage == null) realImage = new RealImage(file); // lazy load
realImage.display();
}
}
Chain of Responsibility#
Category: Behavioral
The Chain of Responsibility pattern passes a request along a chain of handlers. Each handler decides whether to process the request or pass it to the next handler.
javaabstract class SupportHandler {
protected SupportHandler next;
SupportHandler setNext(SupportHandler next) {
this.next = next;
return next;
}
abstract void handle(int level);
}
class Level1Support extends SupportHandler {
public void handle(int level) {
if (level <= 1) System.out.println("Level 1 handles it");
else if (next != null) next.handle(level);
}
}
class Level2Support extends SupportHandler {
public void handle(int level) {
if (level <= 2) System.out.println("Level 2 handles it");
else if (next != null) next.handle(level);
}
}
// Usage
SupportHandler l1 = new Level1Support();
SupportHandler l2 = new Level2Support();
l1.setNext(l2);
l1.handle(2); // Level 2 handles it
Command Pattern#
Category: Behavioral
The Command pattern encapsulates a request as an object, allowing you to parameterize methods, queue or log requests, and support undoable operations.
javainterface Command {
void execute();
void undo();
}
class Light {
void on() { System.out.println("Light ON"); }
void off() { System.out.println("Light OFF"); }
}
class TurnOnCommand implements Command {
private Light light;
TurnOnCommand(Light l) { this.light = l; }
public void execute() { light.on(); }
public void undo() { light.off(); }
}
class RemoteControl {
private Deque<Command> history = new ArrayDeque<>();
void press(Command cmd) {
cmd.execute();
history.push(cmd);
}
void undoLast() {
if (!history.isEmpty()) history.pop().undo();
}
}
Iterator Pattern#
Category: Behavioral
The Iterator pattern provides a way to sequentially access elements of a collection without exposing its underlying representation.
javainterface Iterator<T> {
boolean hasNext();
T next();
}
class NumberRange {
private int[] numbers;
NumberRange(int[] numbers) { this.numbers = numbers; }
Iterator<Integer> iterator() {
return new Iterator<>() {
int index = 0;
public boolean hasNext() { return index < numbers.length; }
public Integer next() { return numbers[index++]; }
};
}
}
Java's Iterable and Iterator interfaces follow this pattern natively.
Mediator Pattern#
Category: Behavioral
The Mediator pattern defines an object that encapsulates how a set of objects interact, promoting loose coupling by keeping objects from referring to each other explicitly.
javainterface ChatMediator {
void sendMessage(String msg, User sender);
void addUser(User user);
}
class ChatRoom implements ChatMediator {
private List<User> users = new ArrayList<>();
public void addUser(User user) { users.add(user); }
public void sendMessage(String msg, User sender) {
for (User u : users) {
if (u != sender) u.receive(msg);
}
}
}
class User {
String name;
ChatMediator mediator;
User(String name, ChatMediator m) { this.name = name; this.mediator = m; }
void send(String msg) { mediator.sendMessage(msg, this); }
void receive(String msg) { System.out.println(name + " received: " + msg); }
}
Memento Pattern#
Category: Behavioral
The Memento pattern captures and externalizes an object's internal state so it can be restored later, without violating encapsulation.
javaclass TextEditor {
private String content = "";
void write(String text) { content += text; }
String getContent() { return content; }
Memento save() { return new Memento(content); }
void restore(Memento m) { content = m.getState(); }
static class Memento {
private final String state;
Memento(String state) { this.state = state; }
String getState() { return state; }
}
}
class History {
private Deque<TextEditor.Memento> stack = new ArrayDeque<>();
void push(TextEditor.Memento m) { stack.push(m); }
TextEditor.Memento pop() { return stack.pop(); }
}
Observer Pattern#
Category: Behavioral
The Observer pattern defines a one-to-many dependency between objects. When one object (subject) changes state, all its dependents (observers) are notified automatically.
javainterface Observer {
void update(String event);
}
interface Subject {
void subscribe(Observer o);
void unsubscribe(Observer o);
void notify(String event);
}
class EventManager implements Subject {
private List<Observer> observers = new ArrayList<>();
public void subscribe(Observer o) { observers.add(o); }
public void unsubscribe(Observer o) { observers.remove(o); }
public void notify(String event) {
for (Observer o : observers) o.update(event);
}
}
class EmailAlert implements Observer {
public void update(String event) {
System.out.println("Email alert: " + event);
}
}
This is the foundation of event-driven systems, reactive frameworks (RxJava, Node.js EventEmitter), and UI state management.
State Pattern#
Category: Behavioral
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
javainterface State {
void handle(TrafficLight light);
}
class GreenState implements State {
public void handle(TrafficLight light) {
System.out.println("Green — Go!");
light.setState(new YellowState());
}
}
class YellowState implements State {
public void handle(TrafficLight light) {
System.out.println("Yellow — Slow down!");
light.setState(new RedState());
}
}
class RedState implements State {
public void handle(TrafficLight light) {
System.out.println("Red — Stop!");
light.setState(new GreenState());
}
}
class TrafficLight {
private State state = new GreenState();
void setState(State s) { this.state = s; }
void change() { state.handle(this); }
}
Strategy Pattern#
Category: Behavioral
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
javainterface SortStrategy {
void sort(int[] data);
}
class BubbleSort implements SortStrategy {
public void sort(int[] data) { /* bubble sort */ }
}
class QuickSort implements SortStrategy {
public void sort(int[] data) { /* quick sort */ }
}
class Sorter {
private SortStrategy strategy;
Sorter(SortStrategy strategy) { this.strategy = strategy; }
void setStrategy(SortStrategy strategy) { this.strategy = strategy; }
void sort(int[] data) { strategy.sort(data); }
}
// Usage
Sorter sorter = new Sorter(new QuickSort());
sorter.sort(new int[]{5, 2, 8, 1});
sorter.setStrategy(new BubbleSort()); // swap at runtime
Template Method Pattern#
Category: Behavioral
The Template Method pattern defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. Subclasses can override steps without changing the algorithm's structure.
javaabstract class DataMigration {
// Template method — fixed sequence
final void migrate() {
readData();
transformData();
writeData();
}
abstract void readData();
abstract void transformData();
void writeData() {
System.out.println("Writing data to DB..."); // default implementation
}
}
class CSVMigration extends DataMigration {
void readData() { System.out.println("Reading from CSV..."); }
void transformData() { System.out.println("Transforming CSV rows..."); }
}
class JSONMigration extends DataMigration {
void readData() { System.out.println("Reading from JSON..."); }
void transformData() { System.out.println("Transforming JSON objects..."); }
}
Visitor Pattern#
Category: Behavioral
The Visitor pattern lets you add further operations to objects without modifying them. It separates an algorithm from the object structure on which it operates.
javainterface Visitor {
void visit(Circle c);
void visit(Rectangle r);
}
interface Shape {
void accept(Visitor v);
}
class Circle implements Shape {
double radius;
Circle(double r) { this.radius = r; }
public void accept(Visitor v) { v.visit(this); }
}
class Rectangle implements Shape {
double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
public void accept(Visitor v) { v.visit(this); }
}
class AreaCalculator implements Visitor {
public void visit(Circle c) { System.out.println("Circle area: " + Math.PI * c.radius * c.radius); }
public void visit(Rectangle r) { System.out.println("Rectangle area: " + r.w * r.h); }
}
Cohesion and Coupling#
Cohesion#
Cohesion measures how closely related the responsibilities of a single class are. High cohesion = class does one thing well. Low cohesion = class does many unrelated things.
Aim for HIGH cohesion.
java// Low cohesion ❌ — unrelated responsibilities
class UtilityClass {
void sendEmail() { ... }
double calculateTax() { ... }
void renderHTML() { ... }
}
// High cohesion ✅
class EmailService { void send() { ... } }
class TaxCalculator { double calculate() { ... } }
class HTMLRenderer { void render() { ... } }
Coupling#
Coupling measures how dependent classes are on each other. Tight coupling = changes in one class ripple through many. Loose coupling = classes interact through abstractions.
Aim for LOW coupling.
| Type | Description |
|---|---|
| Tight coupling | Class A directly instantiates Class B |
| Loose coupling | Class A depends on an interface B implements |
Law of Demeter#
"Talk only to your immediate friends."
The Law of Demeter (LoD) is a design guideline that says a method should only call methods on:
- The object itself.
- Objects passed as arguments.
- Objects it creates.
- Direct component objects.
java// ❌ Violation — chain of calls
double price = order.getCustomer().getWallet().getBalance();
// ✅ Correct — delegate through the object
double price = order.getCustomerBalance();
Violating LoD creates brittle code that breaks whenever any intermediate class changes.
DRY, KISS, YAGNI#
These three principles are fundamental software design heuristics every engineer should internalize.
DRY — Don't Repeat Yourself#
Every piece of knowledge should have a single, unambiguous representation in the codebase.
java// ❌ Bad — duplicated validation logic across multiple places
// ✅ Good — centralized in a Validator class
KISS — Keep It Simple, Stupid#
Prefer simple solutions. Don't over-engineer. The simplest code that works correctly is usually the best code.
YAGNI — You Aren't Gonna Need It#
Don't add functionality until you actually need it. Anticipatory code increases complexity and maintenance burden without benefit.
| Principle | Meaning |
|---|---|
| DRY | Eliminate duplication |
| KISS | Prefer simplicity |
| YAGNI | Build for now, not for imagined futures |
Dependency Injection#
Dependency Injection (DI) is a technique where an object receives its dependencies from outside rather than creating them itself. It is a practical application of the Dependency Inversion Principle.
Types of Dependency Injection#
java// 1. Constructor Injection (preferred)
class OrderService {
private final PaymentService paymentService;
OrderService(PaymentService ps) { this.paymentService = ps; }
}
// 2. Setter Injection
class OrderService {
private PaymentService paymentService;
void setPaymentService(PaymentService ps) { this.paymentService = ps; }
}
// 3. Interface Injection
interface PaymentServiceAware {
void inject(PaymentService ps);
}
Benefits#
- Easier to unit test (inject mock dependencies).
- Promotes loose coupling.
- Makes dependencies explicit and visible.
DI frameworks: Spring (Java), Angular (TypeScript), Inversify (Node.js).
Inversion of Control (IoC)#
Inversion of Control (IoC) is a broader design principle where the control of object creation and lifecycle is transferred from the application code to a framework or container.
In traditional code, your code controls the flow:
javaPaymentService ps = new StripePaymentService();
OrderService os = new OrderService(ps);
With IoC (e.g., Spring):
java// Framework creates and wires objects based on configuration
@Autowired
private OrderService orderService; // framework injects it
IoC containers manage:
- Object creation (instantiation)
- Object lifecycle (singleton vs prototype)
- Dependency wiring
- Configuration management
Design by Contract#
Design by Contract (DbC) is a methodology where software components define formal, precise interfaces using:
- Preconditions — conditions that must be true before a method executes (caller's responsibility).
- Postconditions — conditions guaranteed to be true after a method executes (callee's responsibility).
- Invariants — conditions always true about an object's state.
javaclass BankAccount {
private double balance;
/**
* Precondition: amount > 0 && amount <= balance
* Postcondition: balance is reduced by amount
*/
void withdraw(double amount) {
assert amount > 0 : "Amount must be positive"; // precondition
assert amount <= balance : "Insufficient funds";
balance -= amount;
assert balance >= 0 : "Balance cannot be negative"; // postcondition
}
}
LLD Interview Approach#
Low Level Design interviews test your ability to model a system at the class level. Here is a structured approach to tackle them.
Step-by-Step Framework#
- Clarify requirements — Ask about scale, actors, and core features. Don't assume.
- Identify actors and use cases — Who uses the system? What can they do?
- Identify core entities — What are the nouns in the problem? (User, Booking, Ticket, etc.)
- Define relationships — How do entities relate? (has-a, is-a, uses)
- Design the class structure — Draw a class diagram. Show attributes and methods.
- Apply design patterns — Identify applicable patterns and justify your choices.
- Handle edge cases — Concurrency, invalid input, failure scenarios.
- Walk through a scenario — Trace one flow end-to-end using your design.
Common Mistakes to Avoid#
- Jumping to code without a class diagram.
- Making everything a Singleton.
- Ignoring interfaces — always design to abstractions.
- Ignoring concurrency when it matters (booking, inventory).
- Not applying SOLID — especially SRP and OCP.
Parking Lot#
Problem Statement#
Design a Parking Lot system that supports multiple floors, different vehicle types, and various spot types. The system should assign and free spots, calculate fees, and track availability.
Requirements#
- Support vehicles: Motorcycle, Car, Truck.
- Support spot types: Compact, Regular, Large.
- Multiple floors, each with multiple spots.
- Assign nearest available spot to a vehicle.
- Track entry/exit time for fee calculation.
- Handle concurrent entries.
Core Entities#
codeParkingLot └── Floor[] └── ParkingSpot[] ParkingSpot → SpotType (COMPACT, REGULAR, LARGE) Vehicle → VehicleType (MOTORCYCLE, CAR, TRUCK) Ticket → entryTime, exitTime, spot, vehicle FeeCalculator (Strategy Pattern)
Class Design#
javaenum VehicleType { MOTORCYCLE, CAR, TRUCK }
enum SpotType { COMPACT, REGULAR, LARGE }
abstract class Vehicle {
String licensePlate;
VehicleType type;
}
class Car extends Vehicle { Car(String lp) { type = VehicleType.CAR; licensePlate = lp; } }
class Motorcycle extends Vehicle { Motorcycle(String lp) { type = VehicleType.MOTORCYCLE; licensePlate = lp; } }
class Truck extends Vehicle { Truck(String lp) { type = VehicleType.TRUCK; licensePlate = lp; } }
class ParkingSpot {
int id;
SpotType type;
boolean isOccupied;
Vehicle parkedVehicle;
boolean canFit(Vehicle v) { ... }
void park(Vehicle v) { parkedVehicle = v; isOccupied = true; }
void vacate() { parkedVehicle = null; isOccupied = false; }
}
class Ticket {
Vehicle vehicle;
ParkingSpot spot;
LocalDateTime entryTime;
LocalDateTime exitTime;
double calculateFee(FeeCalculator calculator) {
return calculator.compute(this);
}
}
interface FeeCalculator {
double compute(Ticket ticket);
}
class HourlyFeeCalculator implements FeeCalculator {
public double compute(Ticket ticket) {
long hours = ChronoUnit.HOURS.between(ticket.entryTime, ticket.exitTime);
return hours * 30.0; // ₹30 per hour
}
}
class ParkingLot {
private List<Floor> floors;
private Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();
synchronized Ticket entry(Vehicle vehicle) {
ParkingSpot spot = findSpot(vehicle);
if (spot == null) throw new RuntimeException("Lot full");
spot.park(vehicle);
Ticket ticket = new Ticket(vehicle, spot, LocalDateTime.now());
activeTickets.put(vehicle.licensePlate, ticket);
return ticket;
}
double exit(String licensePlate, FeeCalculator calculator) {
Ticket ticket = activeTickets.remove(licensePlate);
ticket.exitTime = LocalDateTime.now();
ticket.spot.vacate();
return ticket.calculateFee(calculator);
}
private ParkingSpot findSpot(Vehicle vehicle) { ... }
}
Design Patterns Applied#
- Strategy —
FeeCalculatorallows swapping pricing algorithms. - Factory — Vehicle creation based on type.
- Singleton —
ParkingLotinstance. - Template Method — Vehicle spot compatibility check.
Library Management System#
Problem Statement#
Design a Library Management System that allows members to search, borrow, and return books. Librarians can manage the catalog.
Requirements#
- Members can search books by title, author, ISBN.
- Members can borrow up to 5 books for 14 days.
- Fine for late returns.
- Librarians can add/remove books.
- Track multiple copies of the same book.
Core Entities#
codeLibrary └── Catalog → BookItem[] BookItem (physical copy) → Book (metadata) Member → BorrowRecord[] Librarian BorrowRecord → borrowDate, dueDate, returnDate FineCalculator
Class Design#
javaclass Book {
String ISBN, title, author, subject;
}
enum BookStatus { AVAILABLE, BORROWED, RESERVED, LOST }
class BookItem {
String barcode;
Book book;
BookStatus status;
Rack rack;
}
class Member {
String memberId, name;
List<BorrowRecord> borrowedBooks = new ArrayList<>();
static final int MAX_BOOKS = 5;
boolean canBorrow() { return borrowedBooks.size() < MAX_BOOKS; }
}
class BorrowRecord {
Member member;
BookItem bookItem;
LocalDate borrowDate;
LocalDate dueDate;
LocalDate returnDate;
boolean isOverdue() { return returnDate == null && LocalDate.now().isAfter(dueDate); }
double calculateFine() {
if (!isOverdue()) return 0;
long days = ChronoUnit.DAYS.between(dueDate, LocalDate.now());
return days * 2.0; // ₹2 per day
}
}
class Catalog {
private Map<String, List<BookItem>> byISBN = new HashMap<>();
private Map<String, List<BookItem>> byTitle = new HashMap<>();
private Map<String, List<BookItem>> byAuthor = new HashMap<>();
List<BookItem> searchByISBN(String isbn) { return byISBN.getOrDefault(isbn, List.of()); }
List<BookItem> searchByTitle(String title) { return byTitle.getOrDefault(title, List.of()); }
}
class Library {
Catalog catalog;
Map<String, Member> members;
BorrowRecord borrowBook(Member member, BookItem item) {
if (!member.canBorrow()) throw new RuntimeException("Borrow limit reached");
if (item.status != BookStatus.AVAILABLE) throw new RuntimeException("Book not available");
item.status = BookStatus.BORROWED;
BorrowRecord record = new BorrowRecord(member, item, LocalDate.now(), LocalDate.now().plusDays(14));
member.borrowedBooks.add(record);
return record;
}
double returnBook(BorrowRecord record) {
record.returnDate = LocalDate.now();
record.bookItem.status = BookStatus.AVAILABLE;
double fine = record.calculateFine();
record.member.borrowedBooks.remove(record);
return fine;
}
}
Design Patterns Applied#
- Observer — Notify member when reserved book becomes available.
- Strategy — Fine calculation policy.
- Factory — Member/Librarian creation.
- Iterator — Searching through catalog.
BookMyShow (Movie Ticket Booking)#
Problem Statement#
Design an online movie ticket booking system like BookMyShow.
Requirements#
- Users can search movies by city, date, and cinema.
- Select show, choose seats, and book tickets.
- Prevent double booking of same seat.
- Support different seat types: Silver, Gold, Platinum.
- Payment integration.
- Booking confirmation and cancellation.
Core Entities#
codeMovie → Show[] → Cinema → CinemaHall → Seat[] Booking → Seat[], Payment, User
Class Design#
javaclass Movie { String title, language, genre; int durationMin; }
class City { String name; List<Cinema> cinemas; }
class Cinema { String name; City city; List<CinemaHall> halls; }
class CinemaHall { String name; List<Seat> seats; }
enum SeatType { SILVER, GOLD, PLATINUM }
enum SeatStatus { AVAILABLE, BOOKED, TEMPORARILY_LOCKED }
class Seat {
int row, col;
SeatType type;
SeatStatus status;
double price;
}
class Show {
Movie movie;
CinemaHall hall;
LocalDateTime startTime;
Map<Seat, SeatStatus> seatStatusMap;
synchronized boolean lockSeat(Seat seat) {
if (seatStatusMap.get(seat) == SeatStatus.AVAILABLE) {
seatStatusMap.put(seat, SeatStatus.TEMPORARILY_LOCKED);
return true;
}
return false;
}
}
enum BookingStatus { PENDING, CONFIRMED, CANCELLED }
class Booking {
String bookingId;
User user;
Show show;
List<Seat> seats;
BookingStatus status;
Payment payment;
LocalDateTime bookingTime;
}
interface PaymentProcessor {
boolean pay(double amount, String method);
}
class BookingService {
Booking createBooking(User user, Show show, List<Seat> seats) {
for (Seat seat : seats) {
if (!show.lockSeat(seat)) throw new RuntimeException("Seat " + seat + " unavailable");
}
double total = seats.stream().mapToDouble(s -> s.price).sum();
// process payment, then confirm booking
Booking booking = new Booking(user, show, seats);
booking.status = BookingStatus.CONFIRMED;
return booking;
}
}
Concurrency Handling#
Seat booking is a classic concurrency problem. Two users may try to book the same seat simultaneously. Solutions:
- Optimistic Locking — Check seat status before booking; roll back on conflict.
- Pessimistic Locking — Lock the seat row in DB with
SELECT FOR UPDATE. - Temporary Lock — Lock seat for 10 minutes during payment. Release if payment fails.
Design Patterns Applied#
- Strategy — Payment processor (Credit Card, UPI, Wallet).
- Observer — Notify user on confirmation.
- State — Booking status transitions (Pending → Confirmed → Cancelled).
- Facade —
BookingServicesimplifies seat lock + payment + confirmation.
Amazon (E-Commerce System)#
Problem Statement#
Design a core e-commerce platform covering product catalog, cart, orders, and payments.
Requirements#
- Customers browse products by category.
- Add/remove items from cart.
- Place orders with address and payment.
- Inventory check before order confirmation.
- Order tracking with status updates.
- Sellers can list and manage products.
Core Entities#
codeUser (Customer | Seller) Product → Category, Inventory Cart → CartItem[] Order → OrderItem[], Address, Payment, OrderStatus Shipment → TrackingInfo
Class Design#
javaclass Product {
String productId, name, description;
double price;
Category category;
Seller seller;
int inventoryCount;
}
class CartItem { Product product; int quantity; }
class Cart {
User user;
List<CartItem> items = new ArrayList<>();
void addItem(Product p, int qty) {
items.stream()
.filter(i -> i.product.equals(p))
.findFirst()
.ifPresentOrElse(
i -> i.quantity += qty,
() -> items.add(new CartItem(p, qty))
);
}
double total() {
return items.stream().mapToDouble(i -> i.product.price * i.quantity).sum();
}
}
enum OrderStatus { PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
class Order {
String orderId;
User customer;
List<OrderItem> items;
Address deliveryAddress;
Payment payment;
OrderStatus status;
LocalDateTime placedAt;
}
class OrderService {
InventoryService inventory;
PaymentProcessor paymentProcessor;
Order placeOrder(User user, Cart cart, Address address, String paymentMethod) {
// 1. Check inventory
for (CartItem item : cart.items) {
if (!inventory.isAvailable(item.product, item.quantity))
throw new RuntimeException("Out of stock: " + item.product.name);
}
// 2. Process payment
double total = cart.total();
if (!paymentProcessor.pay(total, paymentMethod))
throw new RuntimeException("Payment failed");
// 3. Deduct inventory
cart.items.forEach(item -> inventory.deduct(item.product, item.quantity));
// 4. Create order
Order order = new Order(user, cart.items, address);
order.status = OrderStatus.PLACED;
return order;
}
}
Design Patterns Applied#
- Observer — Order status change notifications.
- Strategy — Payment methods, shipping providers.
- Decorator — Product pricing with discounts, taxes, coupons.
- Iterator — Browse products by category.
- Command — Cancellation and return requests.
Chess Game#
Problem Statement#
Design a two-player Chess game.
Requirements#
- 8x8 board with standard pieces.
- Two players: White and Black.
- Valid move enforcement per piece type.
- Check and checkmate detection.
- Track move history.
Core Entities#
codeGame → Board → Cell[][] Piece (King, Queen, Rook, Bishop, Knight, Pawn) Player → Color Move → fromCell, toCell, piece, capturedPiece MoveValidator
Class Design#
javaenum Color { WHITE, BLACK }
enum PieceType { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN }
abstract class Piece {
Color color;
PieceType type;
abstract List<Cell> getValidMoves(Board board, Cell current);
}
class Rook extends Piece {
public List<Cell> getValidMoves(Board board, Cell current) {
// horizontal + vertical moves
List<Cell> moves = new ArrayList<>();
// ... logic
return moves;
}
}
class Cell {
int row, col;
Piece piece; // null if empty
boolean isEmpty() { return piece == null; }
}
class Board {
Cell[][] cells = new Cell[8][8];
void initialize() {
// Place pieces in starting positions
}
Cell getCell(int row, int col) { return cells[row][col]; }
boolean isInCheck(Color color) { ... }
boolean isCheckmate(Color color) { ... }
}
class Move {
Cell from, to;
Piece piece;
Piece captured;
}
class Game {
Board board;
Player whitePlayer, blackPlayer;
Player currentPlayer;
List<Move> moveHistory = new ArrayList<>();
boolean makeMove(Cell from, Cell to) {
Piece piece = from.piece;
if (piece == null || piece.color != currentPlayer.color) return false;
List<Cell> validMoves = piece.getValidMoves(board, from);
if (!validMoves.contains(to)) return false;
Move move = new Move(from, to, piece, to.piece);
to.piece = piece;
from.piece = null;
moveHistory.add(move);
if (board.isInCheck(currentPlayer.color)) {
// Undo move — puts self in check
from.piece = piece;
to.piece = move.captured;
return false;
}
currentPlayer = (currentPlayer == whitePlayer) ? blackPlayer : whitePlayer;
return true;
}
}
Design Patterns Applied#
- Strategy — Each piece has its own move validation strategy.
- Command — Moves are command objects (supports undo/redo).
- Observer — UI listens to board state changes.
- Memento — Save/restore game state.
- Factory — Piece creation during board setup.
Cohesion and Coupling#
This section is covered earlier in Chapter VII. Refer there for the complete content.
Next Steps#
Congratulations, you've finished the course!
Now that you understand Low Level Design from fundamentals to advanced patterns, here are some next steps:
- Practice LLD problems on GeeksForGeeks LLD and InterviewBit
- Study the Gang of Four book: Design Patterns: Elements of Reusable Object-Oriented Software
- Read Refactoring by Martin Fowler to understand code improvement techniques
- Explore Head First Design Patterns for a beginner-friendly deep dive
- Build real projects and apply the patterns — nothing beats hands-on practice
It is also recommended to actively follow these resources:
- Refactoring Guru — Excellent pattern explanations with diagrams
- Martin Fowler's Blog — Deep articles on software design
- GeeksForGeeks LLD Section
- Coursera — Design Patterns (University of Alberta)
- GitHub — Awesome Design Patterns
Practice LLD problems consistently. Start with simpler systems (Parking Lot, Vending Machine), then move to complex ones (BookMyShow, Splitwise, Uber). Draw class diagrams before writing a single line of code.
References#
Here are the resources referenced while creating this course.
- Design Patterns: Elements of Reusable Object-Oriented Software — GoF
- Clean Code — Robert C. Martin
- Head First Design Patterns — Eric Freeman
- Refactoring.Guru — Design Patterns
- Martin Fowler — Catalog of Patterns of Enterprise Application Architecture
- GeeksForGeeks — LLD Articles
- Baeldung — Java Design Patterns
- TutorialsPoint — Design Patterns
- InterviewBit — LLD Problems
- Wikipedia — SOLID Principles
- Wikipedia — Design Patterns
All code examples are written in Java for clarity and consistency, but the patterns apply to any OOP language including TypeScript, Python, C++, and Kotlin.