When preparing for Software Engineering interviews, you'll often encounter two common design rounds: High-Level Design (HLD) and Low-Level Design (LLD).
Although both evaluate your design skills, they focus on different levels of abstraction.
High-Level Design (HLD) focuses on the overall architecture of a system. It covers the major components, their responsibilities, how they communicate, scalability, reliability, and other system-wide design decisions.
Low-Level Design (LLD) focuses on the implementation details of individual components. It deals with classes, objects, interfaces, design patterns, relationships, business logic, and the internal structure of the code.
A simple way to understand the difference is:
HLD answers: "What components does the system need, and how do they interact?"
LLD answers: "How should each component be designed and implemented?"
What is LLD: Low Level Design
Low-Level Design (LLD) is the phase of system design that focuses on how individual components of a system are built internally as opposed to High-Level Design (HLD), which focuses on the overall architecture and what components exist and how they interact at a broad level.
Example
If HLD says "we need a Ride-Booking Service" for something like Uber, LLD would define:
Classes:
Ride,Driver,Rider,Trip,PaymentHandlerMethods:
Ride.assignDriver(),Trip.calculateFare()Design pattern: maybe a Strategy pattern for different fare calculation types (surge pricing, flat rate)
Database tables:
rides,drivers,paymentswith exact columnsSequence diagram: showing the exact call order when a rider requests a ride
PHASE 1 : OOP Foundations
OOP (Object-Oriented Programming) is a programming paradigm that organizes code around objects bundles of data (attributes) and behavior (methods) rather than around functions and logic operating on raw data separately (as in procedural programming).
Core Elements
1. Classes & Objects
Before diving into the pillars, it helps to understand the foundational components of OOP:
Class: A blueprint or template that defines the properties (data) and behaviors (methods) an object will have. (Think of this as the architectural plan for a house.)
Object: A specific instance of a class that takes up actual memory. (Think of this as the actual house built from the plan.

2. Constructors
A constructor is a special method that is automatically invoked when an object is created. Its primary purpose is to initialize the object's state by assigning values to its fields or performing any required setup. Unlike regular methods, a constructor has the same name as the class and does not have a return type.
Why do we need constructors?
Initialize object fields.
Ensure every object starts in a valid state.
Reduce repetitive initialization code.
Example: When creating a Car object, the constructor can initialize its brand, model, and color.

3. Encapsulation
Encapsulation is the process of combining data (fields) and the methods that operate on that data into a single unit (class) while restricting direct access to the data using access modifiers. Instead of allowing other classes to modify data directly, encapsulation provides controlled access through methods such as getters and setters.
Benefits:
Protects data from unauthorized access.
Improves maintainability.
Makes the code easier to modify without affecting other components.
Enforces business rules before updating data.
Example: A BankAccount class keeps its balance private and only allows updates through deposit() and withdraw() methods.

4. Abstraction
Abstraction is the process of hiding complex implementation details and exposing only the essential functionality required by the user. It allows users to interact with an object without knowing how it works internally. In Java, abstraction is achieved using abstract classes and interfaces.
Benefits:
Reduces complexity.
Improves security by hiding implementation details.
Makes code easier to maintain and extend.
Example: You can drive a car using the steering wheel and pedals without understanding how the engine works internally.

5. Inheritance
Inheritance allows a class (child class) to inherit the properties and behaviors of another class (parent class). It promotes code reuse and establishes an "is-a" relationship. The child class can reuse existing functionality and add new features or override existing behavior.
Benefits:
Reduces code duplication.
Improves reusability.
Makes applications easier to extend.
Example: A Car class inherits common properties such as brand and speed from a Vehicle class.

6. Polymorphism
Polymorphism means "many forms." It allows the same method or interface to behave differently depending on the object that invokes it. There are two types:
Compile-time Polymorphism (Method Overloading)
Runtime Polymorphism (Method Overriding)
Benefits:
Improves flexibility.
Reduces conditional logic.
Makes code easier to extend.
Example: A draw() method behaves differently for Circle, Rectangle, and Triangle.

7. Interfaces
An interface defines a contract that specifies what a class must do without specifying how it should do it. Any class implementing an interface must provide implementations for all of its methods. Interfaces enable multiple unrelated classes to follow the same contract while having different implementations.
Benefits:
Supports abstraction.
Encourages loose coupling.
Makes systems easier to test and extend.
Example: A Payment interface can be implemented by CreditCardPayment, UPIPayment, and PayPalPayment, where each class processes payments differently.

8. Association
Association represents a relationship between two independent objects where each object has its own lifecycle. Neither object owns the other; they simply interact or collaborate. Association can be:
One-to-One
One-to-Many
Many-to-One
Many-to-Many
Example: A Teacher teaches multiple Students, but both can exist independently.

9. Aggregation
Aggregation is a specialized form of association that represents a weak "has-a" relationship. One object contains another object, but the contained object can exist independently even if the parent object is destroyed.
Benefits:
Promotes object reuse.
Maintains independent object lifecycles.
Example: A Department has multiple Employees. If the department is deleted, the employees continue to exist.

10. Composition
Composition is a strong "has-a" relationship where the child object's lifecycle depends entirely on the parent object. If the parent object is destroyed, the child objects are destroyed as well. Composition represents ownership and is stronger than aggregation.
Benefits:
Provides strong ownership.
Improves encapsulation.
Models real-world part-of relationships effectively.
Example: A House consists of multiple Rooms. If the house is demolished, the rooms no longer exist independently.

Understand Through Interactive Explorer
Object-Oriented Programming can feel abstract when you're just reading definitions, so here's a hands-on way to explore it instead. The interactive explorer below walks through all nine core OOP concepts from constructors to composition, using simple Java examples and everyday analogies for each one. Click through the concepts at your own pace, then test what you've learned with the built-in quiz at the end.
PHASE 2: UML and Object Modeling
What is Object Modeling?
Object Modeling is the process of representing a real-world problem as software objects.Instead of thinking about functions or database tables first, we think about:
What objects exist?
What information do they have?
What actions can they perform?
How do they interact?
In other words,
Object Modeling is the blueprint of an object-oriented software system.
What is UML?
UML (Unified Modeling Language) is a standard visual language used to model, design, and document software systems. It is not a programming language. You cannot execute UML diagrams. They are used to communicate and visualize a system's design.
Think of UML as the language of software blueprints, just as architects use architectural drawings before constructing a building.
Why is UML Needed?
Suppose a team has 20 developers, 5 testers, 3 architects, 2 product managers. Instead of explaining the system verbally, everyone can understand the same UML diagrams.
UML helps teams:
Visualize the system
Reduce misunderstandings
Plan before coding
Document architecture
Quick Visual Recap

Types of UML Diagrams
1. Class Diagram
A Class Diagram is a UML Structural Diagram that represents the static structure of a software system. It shows:
Classes
Attributes (data)
Methods (behavior)
Relationships between classes
It is called a static diagram because it describes what the system looks like, not how it behaves over time.
Definition: A Class Diagram is a blueprint of your application's classes and the relationships among them before you start coding.
Structure of a Class
+--------------------------------+
| User | ----> Class Name
+--------------------------------+
| - id : int |
| - name : String | ----> Attributes
| - email : String |
+--------------------------------+
| + login() : boolean |
| + logout() : void | ----> Methods
| + updateProfile() : void |
+--------------------------------+
-----------Equivalent Python----------
class User:
def __init__(self, user_id: int, name: str, email: str):
self.id = user_id
self.name = name
self.email = email
def login(self) -> bool:
return True
def logout(self) -> None:
pass
def update_profile(self) -> None:
passVisibility Symbols
Symbol | Meaning | Java Equivalent |
|---|---|---|
+ | Public | public |
- | Private | private |
# | Protected | protected |
~ | Package | default |
Relationships in Class Diagrams
The six major relationships are:
Association: Two objects know each other.
Customer ------ OrderAggregation: A weak HAS-A relationship.
Department ◇──── EmployeeComposition: A strong HAS-A relationship.
House ◆──── RoomInheritance (Generalization): An IS-A relationship.
Dog --------▷ AnimalRealization: Used for Interfaces.
CreditCard - - -▷ PaymentDependency:
OrderService ------> PaymentGatewayThese relationships help model the real-world interactions between objects.
Example Class Diagram
+----------------------+
| Student |
+----------------------+
| - id |
| - name |
+----------------------+
| + enroll() |
| + dropCourse() |
+----------------------+
|
| enrolls
|
*
+----------------------+
| Course |
+----------------------+
| - code |
| - title |
+----------------------+
| + addStudent() |
+----------------------+This diagram tells us:
StudentandCourseare separate classes.A
Studenthas data (id,name) and behavior (enroll(),dropCourse()).A
Coursehas its own attributes and methods.One student can enroll in multiple courses (indicated by
*).
Quick Visual Recap

2. Object Diagram
An Object Diagram is a UML Structural Diagram that represents the instances (objects) of classes and the relationships between those objects at a particular point in time.
Unlike a Class Diagram, which defines the structure of classes, an Object Diagram shows:
Real objects
Current attribute values
Links between objects
Definition: An Object Diagram is a snapshot of a system that illustrates specific objects, their attribute values, and their relationships at a given moment.
Why Do We Need an Object Diagram?
Suppose you've designed a Library Management System.
Your Class Diagram defines:
Book
Member
Librarian
However, it doesn't tell you:
Which member borrowed which book?
What are the actual values of object attributes?
How are the objects connected at runtime?
An Object Diagram answers these questions.
Why is it Called an Object Diagram?
Because it represents objects, not classes.
Example:
Class Diagram
StudentObject Diagram
student1 : Student
student2 : StudentNotice the difference.
The Object Diagram uses instances.
Components of an Object Diagram
1. Object: An object is an instance of a class.
Notation
student1 : StudentMeaning
Object Name
student1Class
Student2. Attribute Values: Unlike Class Diagrams, object Diagrams store actual values.
Example
+------------------------+
| student1 : Student |
+------------------------+
| id = 101 |
| name = "Ritesh" |
+------------------------+Notice that the datatype is replaced by actual data.
3. Links: Links connect objects.
Example
student1 -------- course1Meaning
Student 1 is enrolled in Course 1. Links represent runtime relationships.
Example : Student Management System
Suppose we have:
Classes
Student
CourseObjects
student1
course1Object Diagram
+-----------------------+
| student1 : Student |
+-----------------------+
| id = 101 |
| name = "Ritesh" |
+-----------------------+
|
| enrolledIn
|
+-----------------------+
| course1 : Course |
+-----------------------+
| code = CS101 |
| title = Java |
+-----------------------+This diagram tells us:
Student 101 exists.
His name is Ritesh.
He is enrolled in Java.
A Class Diagram cannot show these actual values.
Quick Visual Recap

3. Sequence Diagram
A Sequence Diagram is a UML Behavioral Diagram that illustrates the order of interactions (messages) exchanged between objects or components over time to accomplish a particular use case. It answers questions like:
What happens when a user logs in?
Which object calls which method?
In what order are methods executed?
What is the flow of control?
What response is returned at each step?
Definition: A Sequence Diagram models the chronological sequence of messages exchanged between objects during the execution of a scenario.
Why Do We Need a Sequence Diagram?
Suppose you're building an e-commerce application. A user clicks "Place Order." Many things happen behind the scenes:
User is authenticated.
Cart items are fetched.
Inventory is checked.
Payment is processed.
Order is created.
Notification is sent.
A Class Diagram tells you these classes exist, but it doesn't explain which one talks first. A Sequence Diagram shows the complete interaction.
When Should You Use a Sequence Diagram?
Use a Sequence Diagram when you want to model:
User Login
Registration
Checkout
Payment Flow
File Upload
Password Reset
Order Placement
OTP Verification
API Request Flow
Microservice Communication
In general, any workflow involving multiple objects is a good candidate.
Components of a Sequence Diagram:
1. Actor: An Actor represents an external entity that interacts with the system.
Examples:
Customer
Admin
User
Payment Gateway
Delivery Partner
👤 UserThe actor always initiates the interaction.
2. Objects (Participants): Objects represent the classes or services participating in the interaction.
User
LoginController
AuthService
UserRepository
DatabaseThese participants are arranged left to right across the top of the diagram.
3. Lifeline: A Lifeline shows the existence of an object during the interaction. It is represented by a vertical dashed line.
User LoginController
| |
| |
| |
| |Time flows from top to bottom.
4. Activation Bar: The Activation Bar (also called the execution specification) indicates the period during which an object is actively executing an operation.
Controller
|
|█████
|█████
|It shows that the object is processing a request.
5. Messages: Messages represent communication between participants.
Synchronous Message: The sender waits for the receiver to finish.
Controller ---------> ServiceExample:
service.login();The controller cannot continue until login() returns.
Asynchronous Message: The sender does not wait.
NotificationService -----> EmailQueueExample:
sendEmailAsync();The application continues processing while the email is sent in the background.
Combined Fragments
Sequence Diagrams can represent conditions, loops, and parallel execution using combined fragments.
1. alt (Alternative)
Represents an if-else condition.
+-----------------------------+
| alt |
|-----------------------------|
| Valid Password |
| Generate Token |
|-----------------------------|
| Invalid Password |
| Return Error |
+-----------------------------+
---------------Equivalent Python code-------------
if password_correct:
generate_token()
else:
return error2. opt (Optional)
Represents an optional operation.
+-----------------------------+
| opt Remember Me |
|-----------------------------|
| Store Cookie |
+-----------------------------+
---------------Equivalent Python code-------------
if remember_me:
store_cookie()3. loop(Iterations)
Represents repeated execution.
+---------------------------+
| loop For Each Product |
|---------------------------|
| Check Inventory |
+---------------------------+
---------------Equivalent Python code-------------
for product in cart:
check_inventory(product)4. par
Represents parallel execution.
+------------------------------+
| par |
|------------------------------|
| Send Email |
|------------------------------|
| Send SMS |
+------------------------------+Both tasks execute simultaneously.
Example: User Login Flow
User LoginController AuthService UserRepository Database
| | | | |
|----login()------>| | | |
| |----authenticate()-> | |
| | |----findUser()--->| |
| | | |----SELECT---->|
| | | |<---User-------|
| | |<---User----------| |
| |<---JWT Token-----| | |
|<---Success-------| | | |Quick Visual Recap

4. Use Case Diagram
A Use Case Diagram is a UML Behavioral Diagram that illustrates the functional requirements of a system by showing:
Actors (who interacts with the system)
Use Cases (what the system does)
Relationships between actors and use cases
The boundary of the system
It focuses on user goals rather than classes, methods, or algorithms.
Definition: A Use Case Diagram visually represents the interactions between external actors and the system to achieve specific business goals.
Why Do We Need a Use Case Diagram?
Suppose you're building an Online Shopping System. Before designing classes or writing code, you need to understand:
Who will use the system?
What actions can they perform?
Which features should the system provide?
A Use Case Diagram answers these questions. Without one, developers may misunderstand the requirements, resulting in missing or unnecessary features.
When Should You Use a Use Case Diagram?
Use a Use Case Diagram during:
Requirements gathering
Business analysis
Software planning
Client discussions
Feature identification
Project documentation
It is usually one of the first UML diagrams created because it helps define what the system should do before deciding how it will do it.
Components of a Use Case Diagram
1. Actor
An Actor is any external entity that interacts with the system. An actor is not part of the system.
Actors can be:
Human users
External systems
Hardware devices
Third-party services
O
/|\
/ \
Customer2. Use Case
A Use Case represents a function or service provided by the system.
Examples:
Login
Register
Search Products
Add to Cart
Place Order
Make Payment
Track Order
Notation:
( Place Order )Each use case represents a complete user goal.
3. System Boundary
The System Boundary defines the scope of the system. Everything inside the boundary belongs to the system. Everything outside represents external actors.
Example:
+------------------------------------------+
| Online Shopping System |
| |
| (Login) |
| (Search Products) |
| (Add to Cart) |
| (Checkout) |
| |
+------------------------------------------+
CustomerThis clearly separates the system from its environment.
4. Association
Association represents communication between an actor and a use case.
Example:
Customer -------- (Place Order)Meaning:
The customer can place an order.
Association is represented using a simple solid line.
5. <<include>> Relationship
Sometimes one use case always requires another use case. Instead of repeating the same functionality, UML uses <<include>>.
Example:
When placing an order:
Validate Cart
Calculate Total
Process Payment
These steps are mandatory.
Diagram:
(Place Order)
|
<<include>>
|
(Process Payment)Meaning:
Every time "Place Order" executes, "Process Payment" must also execute.
6. <<extend>> Relationship
Sometimes additional functionality is optional. In that case, UML uses <<extend>>.
Example:
After placing an order, the customer may purchase gift wrapping.
Diagram:
(Gift Wrap)
^
|
<<extend>>
|
(Place Order)Gift wrapping is optional. The order can still be placed without it.
7. Generalization
Generalization represents inheritance between actors or use cases.
Example:
User
▲
┌─────┴─────┐
│ │
Customer AdminBoth Customer and Admin inherit common behaviors from User.
Example: Food Delivery System
Customer
|
------------------------------------
| | | |
| | | |
(Login) (Browse Menu) (Place Order) (Track Order)
|
<<include>>
|
(Process Payment)
|
<<include>>
|
(Generate Invoice)
^
|
<<extend>>
|
(Apply Coupon)Quick Visual Recap

5. State Diagram
A State Diagram is a UML Behavioral Diagram that models the different states of an object and the transitions between those states based on events.
It answers questions like:
What happens after an order is placed?
When does a payment become successful?
How does a ticket move from "Open" to "Closed"?
What are all the possible states of a user account?
Definition: A State Diagram represents the lifecycle of an object by showing its states, the events that trigger state changes, and the transitions between those states.
Why Do We Need a State Diagram?
Suppose you're building an E-Commerce System. An order doesn't remain the same forever. It moves through several stages:
Created
↓
Confirmed
↓
Packed
↓
Shipped
↓
DeliveredSometimes things don't go as planned:
Created
↓
Cancelledor
Shipped
↓
ReturnedA State Diagram helps developers understand every possible state and transition, ensuring that invalid transitions (such as shipping a cancelled order) are prevented.
When Should You Use a State Diagram?
State Diagrams are useful whenever an object has a lifecycle.
Common examples include:
Order Processing
User Account Status
ATM Machine
Elevator System
Traffic Light
Online Payment
Flight Booking
Support Ticket
Food Delivery
Document Approval Workflow
If an object changes state over time, a State Diagram is often the right choice.
Components of a State Diagram
1. Initial State: The Initial State represents where the object's lifecycle begins.
Notation:
●Example:
●
|
v
CreatedEvery State Diagram typically has one initial state.
2. State: A State represents a condition or situation during the object's lifetime.
Examples:
Created
Pending
Approved
Rejected
Active
Suspended
Notation:
+------------------+
| Pending |
+------------------+An object performs certain behavior while it remains in that state.
3. Transition: A Transition represents movement from one state to another.
Notation:
Pending --------> ApprovedA transition occurs when an event happens.
4. Event: An Event triggers a transition.
Example:
Pending ----approve()----> ApprovedHere, approve() is the event.
Other examples:
pay()
cancel()
ship()
deliver()
login()
5. Guard Condition: Sometimes a transition occurs only if a condition is true.
Notation:
Pending --[Payment Successful]--> ConfirmedThe transition happens only when payment succeeds.
Python equivalent:
if paymentSuccessful:
state = CONFIRMED6. Action: A transition may execute an action.
Example:
Pending
|
approve()
/sendEmail()
|
▼
ApprovedWhen approval occurs, an email is sent.
7. Final State: The Final State indicates the end of the object's lifecycle.
Notation:
◎Example:
Delivered
|
▼
◎Once the object reaches the final state, no further transitions occur.
Example: User Account Lifecycle
●
|
▼
+--------------+
| Registered |
+--------------+
|
verifyEmail()
|
▼
+--------------+
| Active |
+--------------+
/ \
suspend() deactivate()
| |
▼ ▼
+-------------+ +-------------+
| Suspended | | Deactivated |
+-------------+ +-------------+
|
▼
◎Quick Visual Recap

6. Activity Diagram
An Activity Diagram is a UML Behavioral Diagram that represents the flow of activities in a system, showing how a process starts, executes, makes decisions, runs tasks in parallel, and eventually ends.
It answers questions like:
What steps are involved in placing an order?
How does a loan approval process work?
What happens after a user logs in?
Which activities can run simultaneously?
Where are the decision points?
Definition: An Activity Diagram models the workflow of a business process or system by showing the sequence of activities, control flow, decisions, loops, and parallel execution.
Why Do We Need an Activity Diagram?
Suppose you're designing an Online Shopping System.
When a customer places an order, the system performs several tasks:
Validate the cart
Calculate the total amount
Process payment
Update inventory
Generate invoice
Send confirmation email
Some tasks happen one after another, while others can happen simultaneously. An Activity Diagram provides a clear visualization of this entire workflow.
Real-World Analogy
Imagine making a cup of coffee.
Start
↓
Boil Water
↓
Add Coffee Powder
↓
Add Sugar
↓
Pour Milk
↓
Stir
↓
Serve
↓
EndEach step is an activity, and together they form a complete workflow. An Activity Diagram models business processes in exactly the same way.
When Should You Use an Activity Diagram?
Use an Activity Diagram whenever you need to model a workflow.
Common examples include:
User Registration
Login Process
Checkout Workflow
Food Delivery
ATM Withdrawal
Loan Approval
Hospital Patient Admission
Employee Leave Approval
Online Ticket Booking
CI/CD Deployment Pipeline
If you're modeling a sequence of business activities, an Activity Diagram is usually the best choice.
Components of an Activity Diagram
1. Initial Node: The Initial Node represents the starting point of the workflow.
Notation:
●Example:
●
|
v
LoginEvery Activity Diagram typically has one initial node.
2. Activity: An Activity represents a task or operation.
Examples:
Login
Validate Payment
Search Products
Generate Invoice
Deliver Package
Notation:
+----------------------+
| Process Payment |
+----------------------+Activities are the building blocks of the workflow.
3. Control Flow: A Control Flow arrow connects one activity to the next.
Example:
Login
|
v
Validate UserIt indicates the order in which activities are executed.
4. Decision Node: A Decision Node represents a branching point based on a condition.
Notation:
◇Example:
Validate Payment
|
v
◇
/ \
Success Failure
| |
Ship Order Show Error5. Merge Node: A Merge Node combines multiple alternative paths back into a single flow.
Example:
Success ----\
\
◇
/
Failure ----/
|
v
Generate ReportUnlike a Join Node, a Merge Node does not synchronize parallel tasks, it simply reunites alternative branches.
6. Fork Node: A Fork Node starts multiple activities in parallel.
Notation:
==========Example:
Process Payment
|
v
==========
| |
v v
Send Email
Update InventoryBoth activities start at the same time.
7. Join Node: A Join Node synchronizes parallel activities.
Example:
Send Email -------\
\
==========
/
Update Inventory--/
|
v
Complete OrderThe workflow continues only after both parallel activities have finished.
8. Final Node: The Final Node indicates the end of the workflow.
Notation:
◎Example:
Deliver Order
|
v
◎9. Swimlanes (Optional): Swimlanes divide the workflow based on responsibility.
Example:
+-----------+-------------------+------------------+
| Customer | Order Service | Payment Gateway |
+-----------+-------------------+------------------+
| Login | | |
| Place | Create Order | |
| Order |------------------>| Process Payment |
| |<------------------| Payment Success |
+-----------+-------------------+------------------+Swimlanes help identify who is responsible for each activity.
Example: Online Shopping Workflow
●
|
v
Browse Products
|
v
Add to Cart
|
v
Checkout
|
v
Process Payment
|
v
◇
/ \
Payment Success Payment Failed
| |
v v
Generate Invoice Show Error
|
v
==========
| |
v v
Send Email
Update Inventory
| |
==========
|
v
Ship Order
|
v
◎Workflow Explanation
1. Customer browses products.
2. Adds items to the cart.
3. Checks out.
4. Payment is processed.
5. If payment fails, an error is displayed.
6. If payment succeeds:
a. Invoice is generated.
b. Email is sent.
c. Inventory is updated.
7. Order is shipped.
8. Process ends.Quick Visual Recap

PHASE 3: SOLID Principles
The Foundation of Object-Oriented Design
When software projects grow, code often becomes difficult to maintain. Classes become too large, adding new features breaks existing functionality, and small changes require modifications in multiple places.
To solve these problems, software engineers follow the SOLID Principles, a set of five object-oriented design principles introduced by Robert C. Martin (Uncle Bob). These principles help developers write code that is maintainable, scalable, flexible, and easy to test.
If you're preparing for Low-Level Design (LLD) interviews, mastering SOLID is essential because it forms the foundation of clean architecture and design patterns.
What are SOLID Principles?
SOLID is an acronym representing five design principles:
Letter | Principle |
|---|---|
S | Single Responsibility Principle (SRP) |
O | Open/Closed Principle (OCP) |
L | Liskov Substitution Principle (LSP) |
I | Interface Segregation Principle (ISP) |
D | Dependency Inversion Principle (DIP) |
Together, these principles help create software that is easier to understand, extend, and maintain.
Why Do We Need SOLID Principles?
Imagine you're building an E-Commerce Application. Initially, your application has only:
User Management
Product Catalog
Order Management
A few months later, new requirements arrive:
Online Payments
Coupons
Notifications
Multiple Payment Methods
International Shipping
If the code wasn't designed well, every new feature requires modifying existing classes, increasing the risk of bugs. SOLID Principles encourage designs where new functionality can be added with minimal changes to existing code.
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) is the first principle of the SOLID principles in Object-Oriented Design.
Definition:
A class should have only one reason to change.
In simple words:
A class should do one job, and do it well.
If a class performs multiple unrelated tasks, it becomes difficult to maintain, test, and extend.
Real-World Analozy
A restaurant has:
Chef → Cooks food
Cashier → Handles payments
Waiter → Serves customers
One person shouldn't perform all these jobs.
Bad Example
class UserService:
def register_user(self):
pass
def send_email(self):
pass
def generate_report(self):
pass
def save_to_database(self):
passThis class handles:
Registration
Email
Reporting
Database
Too many responsibilities.
Good Design
UserService
|
|---- UserRepository
|
|---- EmailService
|
|---- ReportServiceEach class has one responsibility.
Benefits of SRP
Benefit | Description |
|---|---|
Easier maintenance | Change one responsibility without affecting others |
Easier testing | Unit tests become smaller and more focused |
Better readability | Small, focused classes are easier to understand |
Better reusability | Reuse components independently |
Lower coupling | Changes in one class are less likely to impact others |
Easier debugging | Problems are isolated to a single responsibility |
Better scalability | New features can be added with minimal impact |
Quick Visual Recap

2. Open/Closed Principle (OCP)
The Open/Closed Principle (OCP) is the second principle of the SOLID principles in Object-Oriented Design.
Definition (Bertrand Meyer):
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
In simple words:
You should be able to add new behavior without changing existing, tested code.
Instead of modifying an existing class whenever a new requirement appears, extend it by creating new classes or implementations.
Real-World Analogy
Imagine a smartphone. The phone comes with a system that supports apps. If you want a new feature like:
WhatsApp
Spotify
Instagram
Google Maps
You install a new app. You don't modify the operating system every time you need new functionality. The operating system is:
Closed for modification (stable)
Open for extension (new apps)
Software should work the same way.
Bad Example
if (paymentType.equals("CreditCard")) {
// Process Credit Card
} else if (paymentType.equals("UPI")) {
// Process UPI
} else if (paymentType.equals("PayPal")) {
// Process PayPal
}Every new payment method requires modifying this code.
Good Design
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self):
pass
class CreditCardPayment(Payment):
def pay(self):
print("Processing Credit Card payment")
class UpiPayment(Payment):
def pay(self):
print("Processing UPI payment")
# Usage
payment = CreditCardPayment()
payment.pay()
payment = UpiPayment()
payment.pay()Quick Visual Recap

3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) is the third principle of the SOLID principles in Object-Oriented Design. It was introduced by Barbara Liskov in 1987.
Definition:
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
In simple words:
A child class should be able to replace its parent class without breaking the application's behavior.
If replacing the parent with the child causes errors, unexpected behavior, or requires special handling, the inheritance hierarchy is incorrect.
Real-World Analogy
Imagine you have a USB charger. Your phone expects:
5V power
USB connector
Whether you plug in:
Samsung charger
Google charger
OnePlus charger
your phone should work normally. If one charger suddenly supplies 20V and damages the phone, it is not a valid substitute. Similarly, every subclass should behave in a way that the parent class promises.
Understanding LSP
Suppose your code works with this type:
Animal animal = new Dog();Later, you replace it with:
Animal animal = new Cat();The rest of the code should continue working without any modifications. That is LSP.
Bad Example (Violates LSP)
Now let's create a Penguin.
class Bird:
def fly(self):
print("Bird is flying")
class Sparrow(Bird):
pass
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly")Client code:
def make_bird_fly(bird):
bird.fly()
sparrow = Sparrow()
penguin = Penguin()
make_bird_fly(sparrow)
make_bird_fly(penguin)Output
Bird is flying
Exception:
Penguins can't flyWhy is this wrong?
The function
make_bird_fly(bird)expects every Bird to fly. But Penguin breaks that expectation. So Penguin cannot replace Bird.
This violates the Liskov Substitution Principle.
Better Design
Instead of forcing every bird to fly, separate the behaviors.
class Bird:
def eat(self):
print("Bird is eating")Flying birds:
class FlyingBird(Bird):
def fly(self):
print("Flying")Now,
class Sparrow(FlyingBird):
passPenguin:
class Penguin(Bird):
def swim(self):
print("Swimming")Usage:
sparrow = Sparrow()
sparrow.eat()
sparrow.fly()
print()
penguin = Penguin()
penguin.eat()
penguin.swim()Output
Bird is eating
Flying
Bird is eating
SwimmingNow everything follows LSP.
LSP Rules
A subclass should not:
Throw unexpected exceptions for valid parent operations.
Remove functionality promised by the parent.
Strengthen method preconditions.
Weaken expected postconditions.
Break the behavior that client code relies on.
Quick Visual Recap

4. Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) is the fourth principle of the SOLID principles.
Definition (Robert C. Martin):
Clients should not be forced to depend on interfaces they do not use.
In simple words:
Don't create one large interface with many unrelated methods. Instead, create multiple small, focused interfaces.
A class should implement only the methods it actually needs.
Real-World Analogy
Imagine you buy a multifunction printer.
It can:
Print
Scan
Fax
Now imagine you buy a basic printer. It only prints.
Should the basic printer be forced to implement Scan and Fax?
No.
It should only implement Print. That's exactly what ISP says.
Bad Example (Violates ISP)
Suppose we have one large interface.
from abc import ABC, abstractmethod
class Machine(ABC):
@abstractmethod
def print_document(self):
pass
@abstractmethod
def scan_document(self):
pass
@abstractmethod
def fax_document(self):
passNow create a simple printer.
class BasicPrinter(Machine):
def print_document(self):
print("Printing...")
def scan_document(self):
raise NotImplementedError("Scan not supported")
def fax_document(self):
raise NotImplementedError("Fax not supported")Usage:
printer = BasicPrinter()
printer.print_document()
printer.scan_document()Output
Printing...
NotImplementedError:
Scan not supportedBetter Design
Split the large interface into smaller interfaces.
Print Interface
from abc import ABC, abstractmethod
class Printer(ABC):
@abstractmethod
def print_document(self):
passScanner Interface
from abc import ABC, abstractmethod
class Scanner(ABC):
@abstractmethod
def scan_document(self):
passFax Interface
from abc import ABC, abstractmethod
class Fax(ABC):
@abstractmethod
def fax_document(self):
passBasic Printer
class BasicPrinter(Printer):
def print_document(self):
print("Printing...")All-in-One Printer
class AllInOnePrinter(Printer, Scanner, Fax):
def print_document(self):
print("Printing...")
def scan_document(self):
print("Scanning...")
def fax_document(self):
print("Faxing...")Usage
basic = BasicPrinter()
basic.print_document()
print()
office = AllInOnePrinter()
office.print_document()
office.scan_document()
office.fax_document()Output
Printing...
Printing...
Scanning...
Faxing...Now each class implements only the interfaces it needs.
ISP and LSP
These two principles often work together.
Suppose you have:
class Bird:
def fly(self):
...A Penguin cannot fly, so it shouldn't inherit a fly() method. One solution is to introduce a separate capability:
class FlyingBird:
def fly(self):
...Now only birds that can actually fly implement that behavior. This design satisfies both LSP (correct substitution) and ISP (no unnecessary methods).
Quick Visual Recap

5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) is the fifth and final principle of the SOLID principles.
Definition (Robert C. Martin):
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
In simple words:
Don't make your business logic depend on concrete classes. Instead, depend on interfaces or abstract classes.
This makes your code flexible, testable, and easy to extend.
Real-World Analogy
Imagine you charge your laptop. The laptop doesn't care whether the charger is from:
Dell
HP
Lenovo
Apple
It only cares that the charger fits the charging port (the standard interface).
Laptop
│
Charging Port (Interface)
│
┌──┼──────────────┐
│ │ │
Dell HP LenovoThe laptop depends on the charging standard, not on a specific charger. That's DIP.
Bad Example (Violates DIP)
Suppose we have an email service.
class EmailService:
def send(self, message):
print(f"Sending Email: {message}")Notification service:
class Notification:
def __init__(self):
self.email = EmailService()
def notify(self, message):
self.email.send(message)Usage:
notification = Notification()
notification.notify("Welcome!")Output
Sending Email: Welcome!What's Wrong?
The Notification class is directly dependent on EmailService.
Notification
│
▼
EmailServiceNow suppose the business says:
"We want to send WhatsApp messages instead."
You must modify the Notification class.
Tomorrow:
SMS
Push Notification
Slack
Telegram
Again, modify the same class. This violates both OCP and DIP.
Better Design Using Abstraction
Step 1: Create an Abstract Class
from abc import ABC, abstractmethod
class MessageService(ABC):
@abstractmethod
def send(self, message):
passStep 2: Email Implementation
class EmailService(MessageService):
def send(self, message):
print(f"Email: {message}")Step 3: SMS Implementation
class SMSService(MessageService):
def send(self, message):
print(f"SMS: {message}")Step 4: WhatsApp Implementation
class WhatsAppService(MessageService):
def send(self, message):
print(f"WhatsApp: {message}")Step 5: Notification Depends on the Abstraction
class Notification:
def __init__(self, service: MessageService):
self.service = service
def notify(self, message):
self.service.send(message)Notice: Notification doesn't know whether it's using:
Email
SMS
WhatsApp
It only knows:
MessageServiceQuick Visual Recap

PHASE 4: Design Principles
A Design Principle is a best practice or guideline that helps developers design software that is:
Easy to maintain
Easy to extend
Easy to test
Easy to understand
Easy to scale
These principles reduce complexity and make software resilient to future changes.
Definition: Design Principles are high-level guidelines that help developers create clean, maintainable, reusable, and scalable software systems.
1. Dry Pricniple
DRY (Don't Repeat Yourself) is a software design principle that states:
"Every piece of knowledge should have a single, unambiguous, authoritative representation within a system."
, Andrew Hunt & David Thomas, The Pragmatic Programmer
This means that every business rule, calculation, validation, or algorithm should exist only once in your application.
Why Do We Need DRY?
Imagine you're building an E-Commerce Application.
The GST tax rate is 18%.
You write the same calculation in five different places:
Checkout
Invoice
Refund
Order Summary
Admin Dashboard
double tax = amount * 0.18;Everything works initially. Six months later, the tax changes from 18% to 20%. Now you must remember to update every copy of that calculation.
The Core Idea
Instead of this:
Checkout
│
Tax Logic
Invoice
│
Tax Logic
Refund
│
Tax LogicUse this:
TaxService
│
┌────────┼────────┐
│ │ │
Checkout Invoice RefundNow every module shares the same implementation.
Example: Duplicate Code (Bad Design)
Suppose you're calculating discounts.
def calculate_order_discount(price):
return price * 0.10
def calculate_invoice_discount(price):
return price * 0.10
def calculate_refund_discount(price):
return price * 0.10The same formula appears three times.
Problems
Hard to maintain
Easy to introduce inconsistencies
Higher chance of bugs
Improved Design (DRY)
class DiscountService:
def calculate_discount(self, price):
return price * 0.10Now every module uses the same method.
discount_service = DiscountService()
discount = discount_service.calculate_discount(price)If the discount changes from 10% to 15%, only one method needs updating.
Quick visual Recap

2. KISS (Keep It Simple, Stupid)
KISS (Keep It Simple, Stupid) is a software design principle that states:
Design your code and systems to be as simple as possible. Avoid unnecessary complexity.
The idea is not to write "basic" code, it is to write code that is easy to understand, easy to maintain, and easy to extend.
One-Line Interview Answer
KISS means writing the simplest solution that correctly solves the problem without adding unnecessary complexity.
Bad Example
Instead of using a simple condition, someone writes unnecessary logic.
def is_eligible(age):
if (age >= 18) == True:
return True
else:
return FalseBetter Design
def is_eligible(age):
return age >= 18KISS vs Oversimplification
KISS does not mean writing the shortest possible code.
It means writing the clearest solution that satisfies the requirements.
For example:
# Hard to understand
result = list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, numbers)))A clearer version is:
result = []
for number in numbers:
if number % 2 == 0:
result.append(number * 2)The second version is longer but easier for most developers to read and maintain.
Quick Visual Recap

3. YAGNI (You Aren't Gonna Need It)
YAGNI (You Aren't Gonna Need It) is a software development principle that states:
Don't implement a feature until it is actually needed.
Instead of building functionality based on assumptions about future requirements, implement only what the current requirements demand.
One-Line Interview Answer:
YAGNI means implementing only the features required today and avoiding unnecessary code for possible future requirements.
Why is YAGNI Important?
Developers often think:
"Maybe we'll need this feature later."
"Let's build it now just in case."
This leads to:
Unused code
More bugs
Higher maintenance costs
Increased development time
More complex systems
YAGNI encourages you to build only what provides value now.
Bad Example
Suppose your application currently supports only email notifications.
❌ Overengineering
class NotificationService:
def send_email(self):
pass
def send_sms(self):
pass
def send_whatsapp(self):
pass
def send_slack(self):
pass
def send_telegram(self):
passOnly email is required, but several unused methods have been added.Improved Example
class NotificationService:
def send_email(self):
passWhen the business later requires SMS support, add it then.
Quick Visual Recap

4. The Law of Demeter (LoD)
The Law of Demeter (LoD), also known as the Principle of Least Knowledge, states:
A class should only communicate with its immediate friends and should not know the internal details of other objects.
This principle helps reduce coupling between classes, making the code easier to maintain and modify.
One-Line Interview Answer
The Law of Demeter states that an object should interact only with its direct dependencies and should not access the internal objects of other classes.
Why is LoD Important?
Without LoD:
Classes become tightly coupled.
Internal implementation details leak outside.
Small changes can break many parts of the application.
Code becomes difficult to maintain.
With LoD:
Low coupling
Better encapsulation
Easier maintenance
Better readability
More reusable code
Bad Example
Suppose we have these classes:
class Engine:
def start(self):
print("Engine Started")
class Car:
def __init__(self):
self.engine = Engine()Now someone writes:
❌ Bad
car = Car()
car.engine.start()Problem - The outside code knows:
Car has an Engine
Engine has a
start()method
If tomorrow the internal implementation changes, every place using car.engine.start() must also change. This violates the Law of Demeter.
Good Example
Let the Car manage its own engine.
class Engine:
def start(self):
print("Engine Started")
class Car:
def __init__(self):
self._engine = Engine()
def start(self):
self._engine.start()Usage:
car = Car()
car.start()Now the outside code only knows about the Car. It doesn't know anything about the Engine. This follows the Law of Demeter.
Qick Visual Recap

5. Separation of Concerns (SoC)
Separation of Concerns (SoC) is a software design principle that states:
A software system should be divided into separate sections, where each section handles a specific responsibility or concern.
In simple words:
Each part of the system should focus on one specific job instead of doing everything together.
A concern means a specific responsibility or functionality of an application.
Examples of concerns:
Database operations
Business logic
User interface
Logging
Payment processing
One-Line Interview Answer
Separation of Concerns is a design principle that separates different responsibilities of a system into independent modules, making code easier to understand, maintain, test, and modify.
Bad Design
class UserService:
def register_user(self, user):
# Validate user
print("Validating user")
# Save to database
print("Saving user")
# Send email
print("Sending email")
# Generate token
print("Generating token")The class is doing too many things.
Good Design
class UserValidator:
def validate(self, user):
print("Validating user")
class UserRepository:
def save(self, user):
print("Saving user")
class EmailService:
def send(self, user):
print("Sending email")
class AuthService:
def generate_token(self):
print("Generating token")Each class handles one concern.
Quick Visual Recap

6. Cohesion
Cohesion is a software design principle that describes how closely related the responsibilities and functionalities inside a single module or class are. In simple words:
A class should contain things that belong together and should focus on one specific purpose.
A class with high cohesion does one well-defined job. A class with low cohesion tries to do many unrelated jobs.
One-Line Interview Answer
Cohesion measures how strongly the responsibilities of a class or module are related. Good design aims for high cohesion, where a class focuses on a single, clear responsibility.
Why is Cohesion Important?
High cohesion provides:
Easier understanding
Easier maintenance
Better reusability
Easier testing
Fewer bugs
Better scalability
Low cohesion creates:
Large classes
Confusing code
Difficult modifications
More dependencies
Low Cohesion
class ApplicationManager:
def create_user(self):
pass
def send_email(self):
pass
def generate_invoice(self):
pass
def calculate_tax(self):
passThis class is doing too many unrelated things.
High Cohesion
class UserService:
def create_user(self):
pass
def update_user(self):
pass
def delete_user(self):
passAll methods are related to users.
Quick Visual Recap

7. Coupling
Coupling is a software design principle that describes
How much one class, module, or component depends on another class or component.
In simple words:
Coupling tells us how tightly connected different parts of a system are.
A good software design aims for: Low Coupling + High Cohesion
One-Line Interview Answer
Coupling measures the level of dependency between software components. Good design minimizes coupling so that changes in one component have minimal impact on others.
High Coupling
class MySQLDatabase:
def save(self):
print("Saving data in MySQL")
class UserService:
def __init__(self):
self.database = MySQLDatabase()
def create_user(self):
self.database.save()Problem: UserService only works with MySQL. Changing to MongoDB requires modifying the class.
Low Coupling
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def save(self):
pass
class MySQLDatabase(Database):
def save(self):
print("Saving data in MySQL")
class MongoDatabase(Database):
def save(self):
print("Saving data in MongoDB")
class UserService:
def __init__(self, database):
self.database = database
def create_user(self):
self.database.save()Usage:
db = MySQLDatabase()
user_service = UserService(db)
user_service.create_user()Now UserService does not care which database is used.
Quick Visual Recap

7. Composing Objects Principle
Composition Over Inheritance is a software design principle that states:
Favor object composition over class inheritance.
Instead of inheriting behavior from a parent class, build objects by combining smaller, reusable objects.
In simple words:
Rather than saying "is-a", prefer "has-a".
One-Line Interview Answer
Composition Over Inheritance means building complex behavior by combining objects instead of inheriting from base classes. It provides greater flexibility, lower coupling, and better maintainability.
Why is Composition Important?
Inheritance creates a strong relationship between classes.
Problems with excessive inheritance:
Tight coupling
Fragile hierarchies
Difficult to modify
Limited flexibility
Changes in the parent class affect all child classes
Composition solves these problems by delegating work to other objects.
Benefits:
More flexible
Easier to test
Better code reuse
Lower coupling
Easier maintenanc
"Is-A" vs "Has-A"
Inheritance → Is-A
Dog IS-A Animal
Car IS-A VehicleComposition → Has-A
Car HAS-A Engine
Computer HAS-A Keyboard
Order HAS-A PaymentComposition models real-world relationships more naturally.
Bad Example (Using Inheritance)
Suppose you're building a notification system.
class Notification:
def send(self):
print("Sending notification")
class EmailNotification(Notification):
def send(self):
print("Sending Email")
class SMSNotification(Notification):
def send(self):
print("Sending SMS")
class WhatsAppNotification(Notification):
def send(self):
print("Sending WhatsApp")
email = EmailNotification()
email.send()Problem: As the project grows, you'll need:
Email + Logging
Email + Retry
Email + Encryption
SMS + Retry
SMS + Logging
WhatsApp + Retry
Soon you'll end up creating many subclasses like:
EmailWithRetry
EmailWithLogging
SMSWithRetry
SMSWithLogging
WhatsAppWithRetry
...This becomes difficult to maintain.
Good Example (Composition)
Instead of inheriting everything, compose the object from smaller reusable components.
class EmailService:
def send(self, message):
print(f"Sending Email: {message}")
class Logger:
def log(self, message):
print(f"LOG: {message}")
class NotificationService:
def __init__(self, email_service, logger):
self.email_service = email_service
self.logger = logger
def send_notification(self, message):
self.logger.log("Preparing notification")
self.email_service.send(message)
self.logger.log("Notification sent")
email_service = EmailService()
logger = Logger()
notification = NotificationService(email_service, logger)
notification.send_notification("Welcome to our platform!")Output
LOG: Preparing notification
Sending Email: Welcome to our platform!
LOG: Notification sentWhy is this better?
NotificationService doesn't inherit from EmailService or Logger. Instead, it has an EmailService and a Logger.
NotificationService
│
├── EmailService
│
└── LoggerQuick Visual Recap

PHASE 5: Design Pattern in System Design
You have probably encountered this situation: you are building a feature, and partway through, you realize you have written similar code before. The structure feels familiar. Maybe it was handling different payment methods, or notifying multiple components when something changed, or creating objects without hardcoding their exact types.
You solved it then, but now you cannot quite remember how, and you end up reinventing a solution from scratch.
Design patterns are named solutions to these recurring problems. They give you a vocabulary to describe common structures, a toolkit of proven approaches, and a way to communicate design decisions with other developers without explaining everything from first principles.
What is Design Pattern
A Design Pattern is a proven, reusable solution to a commonly occurring software design problem. It is not a ready-made piece of code. Instead, it is a template or blueprint that guides you in solving recurring design challenges.
Think of design patterns as best practices that experienced software engineers use to build maintainable, scalable, and flexible applications.
One-Line Interview Answer
A design pattern is a reusable solution to a common software design problem that improves code reusability, maintainability, and scalability.
Why Do We Need Design Patterns?
Imagine you're building different applications:
E-commerce
Banking
Food Delivery
Ride Sharing
Social Media
Although these systems are different, many problems repeat:
Only one database connection should exist.
Different payment methods should be interchangeable.
Objects should be created without exposing creation logic.
Multiple users should receive notifications.
Different algorithms should be interchangeable.
Instead of reinventing the solution every time, developers use design patterns.
Real-World Analogy
Imagine you're building a house.
You don't invent a new design for:
Doors
Windows
Staircases
Roofs
Architects use proven blueprints.
Similarly, software engineers use design patterns as proven blueprints for solving recurring software problems.
Benefits of Design Patterns
Reusable solutions
Easier maintenance
Better code organization
Low coupling
High cohesion
Easier testing
Better scalability
Improved communication among developers
Quick Explainer

1. Creational Design Patterns
Creational Design Patterns are a category of design patterns that focus on how objects are created.
Instead of creating objects directly using new (or directly calling constructors), creational patterns provide flexible, reusable, and controlled ways to create objects.
Their main goal is to make object creation simple, flexible, and independent of the concrete classes being instantiated.
One-Line Interview Answer
Creational Design Patterns provide reusable solutions for object creation, making a system more flexible, maintainable, and loosely coupled by separating object creation from object usage.
Why Do We Need Creational Patterns?
Creating objects directly seems simple.
For example:
payment = CreditCardPayment()This works well initially. But imagine your application later supports:
Credit Card
UPI
PayPal
Apple Pay
Google Pay
Cryptocurrency
Now you'll have object creation spread across hundreds of files.
Problems:
Tight coupling
Difficult to extend
Difficult to test
Code duplication
Hard to maintain
Creational patterns solve these issues by centralizing or abstracting object creation.
Real-World Analogy
Imagine buying a car.
You don't build:
Engine
Wheels
Steering
Brakes
yourself.
Instead, you ask the car manufacturer to build the complete car.
Similarly, instead of manually constructing complex objects, you let a design pattern manage the creation process.
Without Creational Pattern
payment = CreditCardPayment()
payment.pay()Suppose tomorrow the payment method changes. Now you must modify every place where CreditCardPayment is created. This creates tight coupling.
With Creational Pattern
payment = PaymentFactory.create_payment("CreditCard")
payment.pay()Now only the factory knows which class to instantiate.
The rest of the application works with the Payment interface.
This creates low coupling.
Characteristics of Creational Patterns
Hide object creation logic
Reduce coupling
Increase flexibility
Improve maintainability
Simplify object creation
Promote code reuse
Creational Patterns in System Design
Imagine an e-commerce application.
Client
|
Order Service
|
Factory
|
-------------------------
Credit Card
UPI
PayPalThe OrderService doesn't know which payment class is created. The factory handles object creation.
When Should You Use Creational Patterns?
Use them when:
Object creation is complex.
You need loose coupling.
Multiple implementations exist.
You want to hide creation details.
Objects should be reused.
Construction involves many optional parameters.
Only one instance should exist.
Types of Creational Design Patterns
The Gang of Four (GoF) defined five Creational Design Patterns.
I. Singleton Pattern
Purpose: Ensure that only one instance of a class exists.
Real-world examples
Database connection
Logger
Configuration manager
Cache manager
Example:
database = Database.get_instance()Instead of:
Database()
Database()
Database()Only one object is shared throughout the application.
II. Factory Method Pattern
Purpose: Create objects without exposing the object creation logic.
Instead of:
payment = UpiPayment()Use:
payment = PaymentFactory.create_payment("UPI")Benefits:
Low coupling
Easy to add new payment methods
Follows the Open/Closed Principle (OCP)
III. Abstract Factory Pattern
Purpose: Create families of related objects.
Example: Suppose you're building a cross-platform UI library.
Windows
Windows Button
Windows Checkbox
Windows Textbox
macOS
Mac Button
Mac Checkbox
Mac Textbox
Instead of creating each object manually, an abstract factory creates all matching UI components for the selected platform.
IV. Builder Pattern
Purpose: Build complex objects step by step. Suppose you're creating a Computer.
Some users want:
CPU
RAM
SSD
Others want:
CPU
RAM
SSD
GPU
RGB Keyboard
Liquid Cooling
Instead of writing many constructors, use a builder.
Example:
computer = (
ComputerBuilder()
.set_cpu("Intel i9")
.set_ram(32)
.set_ssd(1000)
.build()
)The Builder pattern makes object construction more readable and flexible.
V. Prototype Pattern
Purpose: Create new objects by copying existing objects.
Instead of:
employee = Employee()Use:
copy = employee.clone()Useful when object creation is expensive or complex.
Example:
Game characters
Documents
Graphic objects
Templates
Quick Visual Recap

2. Structural Design Patterns
Structural Design Patterns are a category of design patterns that focus on how classes and objects are organized, connected, and composed to form larger structures. While Creational Patterns answer the question:
"How should objects be created?"
Structural Patterns answer:
"How should objects be connected and organized so they can work together efficiently?"
Their goal is to create flexible, reusable, and maintainable relationships between classes and objects.
One-Line Interview Answer
Structural Design Patterns define how classes and objects are composed to create larger, flexible, and efficient software structures while reducing coupling and improving maintainability.
Why Do We Need Structural Patterns?
Imagine building an e-commerce application.
You have:
Payment Service
Inventory Service
Notification Service
Order Service
Shipping Service
These components need to work together.
If every class directly communicates with every other class, the system becomes:
Tightly coupled
Hard to understand
Difficult to modify
Difficult to test
Structural patterns help organize these relationships cleanly.
Real-World Analogy
Think of building a house.
Individual components like:
Bricks
Doors
Windows
Pipes
Electrical wiring
are assembled into a complete house.
The materials already exist, the challenge is how to connect them together.
Structural Design Patterns solve a similar problem in software.
Characteristics of Structural Patterns
Organize classes and objects
Reduce coupling
Improve code reuse
Simplify complex systems
Hide implementation details
Increase flexibility
Make systems easier to maintain
Structural Patterns in System Design
Imagine an online shopping platform.
Customer
|
Order Service
|
-------------------------
|
Facade
|
-------------------------
Payment
Inventory
Shipping
NotificationThe Facade provides a simple interface while internally coordinating multiple services.
When Should You Use Structural Patterns?
Use them when:
Multiple classes need to work together.
Existing classes have incompatible interfaces.
You want to simplify a complex subsystem.
Features should be added dynamically.
Memory optimization is important.
Access to an object needs to be controlled.
Types of Structural Design Patterns
I. Adapter Pattern
Purpose: Allows two incompatible interfaces to work together.
Imagine: Your application expects:
USB-CBut the device provides:
USB-AAn adapter bridges the gap.
Example
Laptop
↓
USB Adapter
↓
Old USB DeviceReal-world Examples
USB adapters
Power plug converters
Legacy API integration
Database drivers
II. Bridge Pattern
Purpose: Separate abstraction from implementation so both can evolve independently.
Example: Suppose you have:
Shapes
Circle
Square
Colors
Red
Blue
Without Bridge:
RedCircle
BlueCircle
RedSquare
BlueSquareAs more shapes and colors are added, the number of classes grows rapidly.
With Bridge:
Shape
|
ColorAny shape can use any color without creating every possible combination.
III. Composite Pattern
Purpose: Treat individual objects and groups of objects the same way.
Example: A file system.
Folder
├── File
├── File
└── Folder
├── File
└── FileWhether you open a file or a folder, the interaction is similar.
Real-world Examples
File systems
Organization charts
Menu structures
HTML DOM tree
IV. Decorator Pattern
Purpose: Add new functionality to an object without modifying its existing code.
Example:
Coffee
↓
Add Milk
↓
Add Sugar
↓
Add Whipped CreamInstead of creating:
MilkCoffee
SugarCoffee
MilkSugarCoffee
MilkSugarCreamCoffeeYou decorate the coffee with additional features.
Real-world Examples
Java I/O streams
Logging
Encryption
Compression
V. Facade Pattern
Purpose: Provide a simple interface to a complex subsystem.
Suppose starting a computer requires:
Start CPU
Initialize Memory
Start Hard Disk
Load Operating SystemInstead of exposing all these steps:
computer.start();The Computer class hides the complexity.
Real-world Examples
Spring Boot starters
Payment SDKs
Database libraries
Home theater systems
VI. Flyweight Pattern
Purpose: Reduce memory usage by sharing common objects.
Example: A game has:
100,000 TreesInstead of storing:
Color
Texture
Shape
for every tree, all trees share common data. Only unique information like position is stored separately.
Real-world Examples
Text editors (characters)
Game engines
Icons
Browser rendering
VII. Proxy Pattern
Purpose: Control access to another object. The proxy sits between the client and the real object.
Example:
User
↓
Proxy
↓
DatabaseThe proxy may:
Check permissions
Cache results
Delay object creation
Log requests
Real-world Examples
Authentication
Lazy loading
Caching
Remote services
Quick Visual Recap

3. Behavioral Design Patterns
Behavioral Design Patterns are a category of design patterns that focus on how objects communicate, interact, and collaborate to accomplish a task.
While:
Creational Patterns focus on object creation.
Structural Patterns focus on object composition and relationships.
Behavioral Patterns focus on:
"How should objects communicate and share responsibilities?"
They define how objects exchange information, delegate tasks, and coordinate behavior while keeping the system flexible and loosely coupled.
One-Line Interview Answer
Behavioral Design Patterns define how objects communicate and collaborate with each other, making systems more flexible, maintainable, and loosely coupled.
Why Do We Need Behavioral Patterns?
Imagine you're building an e-commerce application.
When a customer places an order:
Inventory should be updated.
Payment should be processed.
A confirmation email should be sent.
A notification should be pushed to the mobile app.
Analytics should be updated.
If OrderService directly performs all these tasks, it becomes:
Huge
Tightly coupled
Difficult to maintain
Hard to test
Behavioral patterns help distribute responsibilities among different objects and define how they interact.
Characteristics of Behavioral Patterns
Define communication between objects
Reduce coupling
Improve flexibility
Clearly separate responsibilities
Simplify complex workflows
Promote reusable behaviors
Make systems easier to extend
Behavioral Patterns in System Design
Imagine an online shopping platform.
Customer
|
Order Service
|
------------------------------
| | | |
Payment Inventory Notification Analytics
|
Observer PatternThe OrderService doesn't perform every action itself. Instead, it coordinates with other components using behavioral patterns.
When Should You Use Behavioral Patterns?
Use them when:
Multiple objects need to communicate.
Different algorithms should be interchangeable.
Requests need to be processed in stages.
Objects need to change behavior based on state.
You want to implement event-driven systems.
You need Undo/Redo functionality.
Complex workflows require clear coordination.
Types of Behavioral Design Patterns
The Gang of Four (GoF) defined 11 Behavioral Design Patterns.
I. Strategy Pattern
Purpose: Allows you to switch algorithms at runtime.
Example
A payment system supports:
Credit Card
UPI
PayPal
Instead of using a long if-else statement, each payment method implements the same interface.
Payment
├── CreditCardPayment
├── UpiPayment
└── PayPalPaymentThe application selects the appropriate strategy when needed.
II. Observer Pattern
Purpose: Notifies multiple objects automatically when one object changes.
Example
A customer places an order. The following services are notified:
Order Placed
|
----------------
| | |
Email SMS InventoryEach service reacts independently.
III. Command Pattern
Purpose: Encapsulates a request as an object.
Instead of executing an action directly, the action is wrapped inside a command object.
Example
A remote control.
Button
↓
TurnOnCommand
↓
TelevisionThe remote doesn't know how the TV works; it simply executes the command.
IV. State Pattern
Purpose: Allows an object to change its behavior when its internal state changes.
Example
An online order.
Created
↓
Paid
↓
Shipped
↓
DeliveredThe available operations depend on the current state.
V. Chain of Responsibility Pattern
Purpose: Passes a request through a chain of handlers until one processes it.
Example
Customer support.
Customer
↓
Chatbot
↓
Support Agent
↓
ManagerEach handler decides whether to handle the request or pass it along.
VI. Mediator Pattern
Purpose: Centralizes communication between multiple objects.
Instead of every object talking to every other object, all communication goes through a mediator.
Example
An air traffic control tower.
Plane A
|
Control Tower
|
Plane BPlanes don't communicate directly; they communicate through the control tower.
VII. Iterator Pattern
Purpose: Provides a standard way to traverse a collection without exposing its internal implementation.
Example
Playlist
↓
Next Song
↓
Previous SongThe user doesn't need to know whether songs are stored in an array, linked list, or database.
VIII. Template Method Pattern
Purpose: Defines the overall structure of an algorithm while allowing subclasses to customize specific steps.
Example
Making a beverage.
Boil Water
↓
Add Ingredient
↓
Pour Into Cup
↓
ServeTea and coffee follow the same process but differ in one or two steps.
IX. Visitor Pattern
Purpose: Adds new operations to existing objects without modifying their classes.
Example
A tax calculator visits different item types:
Tax Visitor
↓
Book
↓
Electronics
↓
FurnitureEach object accepts the visitor, which performs the appropriate calculation.
X. Memento Pattern
Purpose: Saves an object's state so it can be restored later.
Example
A text editor.
Type Text
↓
Save State
↓
Undo
↓
Restore Previous StateXI. Interpreter Pattern
Purpose: Defines a grammar and interprets expressions according to that grammar.
Example
A calculator interpreting:
10 + 20 * 5or a search engine interpreting:
(status = "Active") AND (age > 18)Quick Visual Recap

Phase 6: Concurrency & Thread Safety, Beginner-Friendly Deep Dive
This is the topic most LLD candidates skip, and the one that separates "I know patterns" from "I can build something real." We'll build every concept from the ground up with runnable Python examples, then connect it to interview questions.
1. Why This Even Matters
Imagine a Movie Ticket Booking System. Two users, at the exact same millisecond, try to book the last seat (Seat A1).
Without any protection, both requests could:
Check "is A1 available?" → Yes (for both)
Both proceed to book it
Both succeed → Seat A1 sold twice
This is the core problem concurrency & thread safety solves: multiple things happening "at the same time" corrupting shared data.
2. Core Vocabulary (learn these cold, interviewers use this language)
Term | Meaning |
|---|---|
Process | An independent running program, with its own memory |
Thread | A lightweight unit of execution inside a process; threads in the same process share memory |
Concurrency | Multiple tasks making progress in overlapping time periods (not necessarily simultaneously) |
Parallelism | Multiple tasks running literally at the same instant (needs multiple CPU cores) |
Race Condition | Bug where the outcome depends on the unpredictable timing/order of threads |
Critical Section | The part of code that touches shared data and must not be run by two threads at once |
Shared Resource | Any variable/object/data that multiple threads can read or write (e.g., |
Lock (Mutex) | A tool that lets only one thread at a time enter a critical section |
Deadlock | Two or more threads stuck forever, each waiting on a resource the other holds |
Starvation | A thread never gets a chance to run because others keep "winning" |
Atomic Operation | An operation that completes in a single, uninterruptible step, no other thread can see it "half-done" |
Concurrency vs Parallelism, the classic interview one-liner: Concurrency is about dealing with lots of things at once (structure). Parallelism is about doing lots of things at once (execution). A single-core CPU can be concurrent but never truly parallel.
3. Seeing a Race Condition Happen (the "aha" moment)
Let's simulate 1000 people trying to increment a shared counter (like a ticket counter or inventory count) using multiple threads, without any protection.
import threading
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1 # looks like one step, but it ISN'T
threads = []
for _ in range(5):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
for t in threads:
t.join()
print("Final counter:", counter)
# Expected: 500000
# Actual: something LESS, and different every run (e.g., 487213)
Why does this happen?
counter += 1 looks like one line, but under the hood the CPU does three steps:
Read
counterinto a temporary registerAdd 1 to it
Write it back to
counter
If Thread A reads counter = 5, and before it writes back, Thread B also reads counter = 5, then both threads write back 6, one increment is lost.
This is a race condition, and counter += 1 is not atomic in Python (despite being one line of code).
Interview gold nugget: "One line of code" does NOT mean "one atomic operation." This is one of the most common interview gotchas.
4. Fixing It: Locks (Mutex)
A Lock ensures only one thread can execute the critical section at a time, everyone else waits their turn.
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # acquire lock, run block, auto-release
counter += 1
threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print("Final counter:", counter) # Always 500000, every time
with lock: is shorthand for:
lock.acquire()
try:
counter += 1
finally:
lock.release()
Key interview point: always release locks in a finally (or use with), otherwise an exception mid-critical-section leaves the lock held forever → deadlock.
5. Applying This to a Real LLD Problem: Seat Booking
This is the direct, interview-realistic version of the movie ticket problem.
import threading
class Seat:
def __init__(self, seat_id):
self.seat_id = seat_id
self.is_booked = False
self.lock = threading.Lock() # one lock PER seat, not one global lock
def book(self, user):
with self.lock:
if self.is_booked:
print(f"{user}: Seat {self.seat_id} already booked. Try another seat.")
return False
# Simulate some processing time (payment, DB write, etc.)
self.is_booked = True
print(f"{user}: Successfully booked seat {self.seat_id}!")
return True
seat = Seat("A1")
def try_booking(user):
seat.book(user)
t1 = threading.Thread(target=try_booking, args=("Alice",))
t2 = threading.Thread(target=try_booking, args=("Bob",))
t1.start()
t2.start()
t1.join()
t2.join()
# Guaranteed: only ONE of Alice/Bob gets the seat, no matter the timing
Design decision worth saying out loud in an interview:
"I put one lock per seat, not one global lock for the whole theater, so booking Seat A1 doesn't block someone booking Seat B5 at the same time. A single global lock would be correct but kills performance."
This is exactly the kind of trade-off interviewers want to hear.
6. Thread-Safe Singleton (very commonly asked)
Problem: A Singleton must return the same instance to everyone. But if two threads call get_instance() for the first time simultaneously, both might see "no instance yet" and create two instances.
import threading
class ConfigManager:
_instance = None
_lock = threading.Lock()
def __init__(self):
if ConfigManager._instance is not None:
raise Exception("Use get_instance() instead of direct instantiation!")
self.settings = {}
@classmethod
def get_instance(cls):
# First check (no lock), fast path once instance exists
if cls._instance is None:
with cls._lock:
# Second check (inside lock), the "double-checked locking" pattern
if cls._instance is None:
cls._instance = ConfigManager()
return cls._instance
Why check twice (double-checked locking)?
The first check (no lock) avoids acquiring a lock every single time, locks are expensive if called constantly.
Once the instance exists, thousands of threads can read it lock-free.
The lock is only ever needed for the first creation race.
The second check inside the lock is needed because multiple threads could have passed the first check before any of them acquired the lock.
Note: In real Python, module-level singletons or
threading.Lockat import time are simpler and more idiomatic, but double-checked locking is what interviewers expect you to explain, regardless of language.
7. Producer-Consumer Pattern (appears in Rate Limiters, Order Queues, Ticket Systems)
Scenario: One or more threads produce work (e.g., incoming orders), one or more threads consume it (e.g., processing orders). You need a thread-safe queue between them.
import threading
import queue
import time
import random
order_queue = queue.Queue(maxsize=5) # Queue is thread-safe out of the box
def producer():
for i in range(10):
order = f"Order-{i}"
order_queue.put(order) # blocks if queue is full
print(f"Produced {order}")
time.sleep(random.uniform(0.1, 0.3))
def consumer():
while True:
order = order_queue.get() # blocks if queue is empty
print(f" Consumed {order}")
order_queue.task_done()
time.sleep(random.uniform(0.2, 0.4))
p = threading.Thread(target=producer)
c = threading.Thread(target=consumer, daemon=True)
p.start()
c.start()
p.join()
order_queue.join() # wait until all items are processed
Why this matters for LLD: Python's queue.Queue is already thread-safe internally (it uses locks/conditions for you). This is the pattern behind:
Rate limiters (requests queued, workers consume at a fixed rate)
Order processing systems
Logging frameworks (log calls queued, a background thread writes to disk)
Interview tip: If asked to design a Producer-Consumer system from scratch (not using
queue.Queue), know that it's built from a Lock + a "wait until condition is true" mechanism, which is exactly whatthreading.Conditiongives you.
8. Deadlock (know how to spot and explain it)
Deadlock: two threads each hold a lock the other needs, and neither will let go.
import threading
import time
lock_a = threading.Lock()
lock_b = threading.Lock()
def task_1():
with lock_a:
print("Task 1 acquired Lock A")
time.sleep(0.5)
print("Task 1 waiting for Lock B")
with lock_b:
print("Task 1 acquired Lock B")
def task_2():
with lock_b:
print("Task 2 acquired Lock B")
time.sleep(0.5)
print("Task 2 waiting for Lock A")
with lock_a:
print("Task 2 acquired Lock A")
t1 = threading.Thread(target=task_1)
t2 = threading.Thread(target=task_2)
t1.start(); t2.start()
# t1.join(); t2.join() # this would hang forever, classic deadlock
The four conditions for deadlock (say these in an interview):
Mutual exclusion, resources can't be shared
Hold and wait, a thread holds one resource while waiting for another
No preemption, a resource can't be forcibly taken away
Circular wait, Thread A waits on B, B waits on A (a cycle)
The standard fix: always acquire locks in the same global order everywhere in your code (e.g., always Lock A before Lock B). This breaks the circular wait condition.
# Fixed version, both tasks acquire in the SAME order: A then B
def task_1_fixed():
with lock_a:
with lock_b:
print("Task 1 done safely")
def task_2_fixed():
with lock_a: # same order as task_1, not lock_b first
with lock_b:
print("Task 2 done safely")
9. Python-Specific Gotcha: The GIL (you WILL be asked about this if you say "Python")
The Global Interpreter Lock (GIL) means only one thread executes Python bytecode at a time, even on a multi-core machine.
What this means practically:
threadingin Python gives you concurrency, not true parallelism, for CPU-bound work (e.g., heavy math loops)For I/O-bound work (network calls, file reads, DB queries, sleeping), threads still help a lot, because the GIL is released while waiting on I/O
For CPU-bound work needing real parallel speed, use
multiprocessing(separate processes, separate memory, no GIL sharing) instead ofthreading
Workload Type | Best Tool in Python |
|---|---|
I/O-bound (API calls, DB, file I/O) |
|
CPU-bound (heavy computation) |
|
Interview line that shows depth: "I'd use
threadinghere because seat booking is I/O-bound (mostly waiting on a database write), not CPU-bound, the GIL isn't really a bottleneck in that case."
10. Where This Shows Up in Classic LLD Problems
Problem | Concurrency Concern |
|---|---|
Parking Lot | Two cars claiming the same spot at once → lock per spot |
Movie/Ticket Booking | Double booking the same seat → lock per seat |
ATM / Bank Account | Two withdrawals draining more than the balance → lock per account |
Rate Limiter | Multiple requests updating the token count → atomic counter / lock |
Inventory System (e-commerce) | Overselling the last item in stock → lock per SKU |
Singleton services (Logger, ConfigManager) | Multiple threads creating duplicate instances → double-checked locking |
Logging Framework | Many threads writing to one file → Producer-Consumer queue |
11. A Quick Self-Test (say these out loud, no notes)
What's the difference between concurrency and parallelism?
Why isn't
x += 1atomic in Python?Why do we check
if instance is Nonetwice in a thread-safe Singleton?What are the four conditions required for a deadlock, and how do you prevent it?
When would you choose
multiprocessingoverthreadingin Python?In the seat-booking example, why one lock per seat instead of one global lock?
If you can answer all six confidently without looking back, you're in good shape for this topic in an interview.
12. Practice Exercise (do this yourself before moving on)
Extend the Seat booking example into a small ATM withdrawal simulator:
An
Accountclass with abalanceand awithdraw(amount)methodSpin up 10 threads all trying to withdraw money from the same account simultaneously
First without a lock (watch the balance go negative, a real bug class in banking systems)
Then with a lock, and verify the balance never goes below zero
This exercise directly mirrors a real interview follow-up: "How would you prevent a double-spend in this account?"
Recap Through Visual Explainer
Phase 7: Low-Level Design (LLD), The Design Process
A beginner-friendly, interview-focused guide (with Python examples)
This guide walks through the exact process interviewers expect you to follow when asked to "design a Parking Lot", "design a Splitwise", "design an Elevator System", etc. Each stage below is something you should visibly narrate in an interview, interviewers grade the process, not just the final code.
We'll use "Design a Parking Lot System" as a running example throughout, so you see how each stage connects to the next.
1. Requirement Gathering (Functional + Non-Functional)
Why it matters: Jumping straight into code is the #1 reason candidates fail LLD interviews. The interviewer wants to see you scope the problem before solving it, real systems are never fully specified upfront.
How to do it
Ask clarifying questions, then explicitly separate requirements into two buckets:
Functional Requirements, what the system should do (features/behavior)
What are the core use cases?
Who are the users and what actions can they take?
What are the inputs and outputs?
Non-Functional Requirements, how well the system should do it (qualities/constraints)
Concurrency (multiple threads/users accessing simultaneously?)
Extensibility (will new types be added later?)
Performance constraints
Consistency/availability trade-offs (less relevant for LLD, more for HLD)
Example: Parking Lot
Functional Requirements
The system should support multiple floors and multiple spot types (Compact, Large, Handicapped, Motorcycle).
A vehicle should be assigned an available spot on entry and the spot freed on exit.
The system should calculate a parking fee based on duration and vehicle type.
The system should track available spot count per type per floor.
Non-Functional Requirements
Should support thread-safe spot allocation (two cars shouldn't get the same spot).
Should be easily extensible to add new vehicle types or pricing strategies without rewriting core logic.
Spot lookup should be reasonably fast (not O(n) scan every time, ideally).
💡 Interview tip: Say your assumptions out loud. "I'll assume a single parking lot with multiple floors, and I won't worry about payment gateways, just fee calculation." This shows you can scope work like a real engineer.
2. Identifying Entities / Actors
Why it matters: This is where you translate the English requirements into nouns (entities) and actors (external users/systems that interact with the system). This is essentially informal domain modeling, the seed of your classes.
How to do it
Underline the nouns in your requirements.
Classify each as an Entity (has state, is part of the system) or an Actor (external, triggers actions on the system).
Drop generic/irrelevant nouns; keep only what's relevant to the scope you defined in Step 1.
Example: Parking Lot
Noun | Type | Notes |
|---|---|---|
Vehicle | Entity | Car, Bike, Truck, has a type, license plate |
ParkingSpot | Entity | Has size, status (free/occupied) |
ParkingFloor | Entity | Contains multiple spots |
ParkingLot | Entity | Contains multiple floors (the "system" itself) |
Ticket | Entity | Created on entry, holds entry time, spot, vehicle |
Payment / FeeCalculator | Entity | Computes cost |
Driver / Attendant | Actor | Triggers entry/exit |
Admin | Actor | Configures the lot (adds floors/spots) |
💡 Interview tip: Don't over-model. If "Payment gateway" isn't in scope, don't create a
PaymentGatewayclass, just aFeevalue orFeeCalculator.
3. Defining Relationships
Why it matters: Once you know your entities, you need to define how they connect, this determines your class structure (composition vs aggregation vs inheritance) before you write a single line of code.
Key relationship types (know these cold)
Relationship | Meaning | Example |
|---|---|---|
Association | Two classes use each other, independent lifecycles |
|
Aggregation ("has-a", weak) | Whole-part, but part can exist independently |
|
Composition ("has-a", strong) | Whole-part, part's lifecycle is tied to whole |
|
Inheritance ("is-a") | Specialization |
|
Multiplicity | How many of each side | One |
Example: Parking Lot relationships
ParkingLot1 → manyParkingFloor(composition)ParkingFloor1 → manyParkingSpot(composition)Vehicleis a base class;Car,Bike,Truckinherit from itTicketassociates aVehiclewith aParkingSpotand holds timestampsParkingSpot1 → 0..1Vehicleat a time (association)
💡 Interview tip: Draw this as a quick UML-style box diagram (even in text/ASCII), interviewers love seeing you sketch relationships before coding.
ParkingLot ──composes──▶ ParkingFloor ──composes──▶ ParkingSpot
│
associates
▼
Vehicle ◀──inherits── Car, Bike, Truck
4. Selecting Design Patterns
Why it matters: Patterns aren't decoration, each one exists to solve a specific recurring problem. Interviewers want to see you pick a pattern because the requirement demands it, not to show off vocabulary.
Common patterns mapped to LLD problems
Pattern | Use it when... | Parking Lot example |
|---|---|---|
Singleton | Exactly one instance should exist system-wide |
|
Factory | Object creation logic varies by type and should be centralized |
|
Strategy | An algorithm/behavior needs to vary and swap at runtime |
|
Observer | Multiple parts need to be notified when state changes | Notify |
Decorator | Add responsibilities to objects dynamically | Adding surge pricing on top of base fee |
State | An object's behavior changes based on internal state |
|
Command | Encapsulate a request as an object (undo/redo, queuing) | Entry/Exit operations as commands for logging |
Example: Parking Lot decisions
Singleton →
ParkingLot(only one lot in the system).Strategy →
FeeStrategy, because the requirement said fee calc should be flexible (hourly now, maybe flat-rate later).Factory →
VehicleFactory, because vehicle creation logic (validating type, plate) shouldn't live inside client code.
💡 Interview tip: State the pattern and the specific requirement it satisfies: "I'll use Strategy for fee calculation because the non-functional requirement said pricing rules must be swappable without touching core logic." This ties Step 4 directly back to Step 1, a strong signal of structured thinking.
5. Writing Class Responsibilities / Interfaces
Why it matters: Before writing full implementations, define what each class is responsible for (Single Responsibility Principle) and its public interface (method signatures). This is where SOLID principles get applied concretely.
How to do it
For each entity from Step 2, write:
One sentence describing its single responsibility
Its public methods (interface), not the implementation yet
Example: Parking Lot (interfaces only)
from abc import ABC, abstractmethod
from enum import Enum
from datetime import datetime
class VehicleType(Enum):
CAR = "CAR"
BIKE = "BIKE"
TRUCK = "TRUCK"
class SpotType(Enum):
COMPACT = "COMPACT"
LARGE = "LARGE"
MOTORCYCLE = "MOTORCYCLE"
class Vehicle(ABC):
"""Responsibility: represent a vehicle and its identifying info."""
def __init__(self, license_plate: str, vehicle_type: VehicleType):
self.license_plate = license_plate
self.vehicle_type = vehicle_type
class ParkingSpot(ABC):
"""Responsibility: represent a single spot's state (free/occupied)."""
def is_available(self) -> bool: ...
def assign_vehicle(self, vehicle: Vehicle) -> None: ...
def remove_vehicle(self) -> None: ...
class FeeStrategy(ABC):
"""Responsibility: calculate fee given duration/vehicle, swappable algorithm."""
@abstractmethod
def calculate_fee(self, entry_time: datetime, exit_time: datetime, vehicle: Vehicle) -> float:
...
class ParkingFloor:
"""Responsibility: manage spots on one floor, find an available spot."""
def find_available_spot(self, spot_type: SpotType) -> "ParkingSpot | None": ...
class ParkingLot:
"""Responsibility: orchestrate entry/exit across all floors (Singleton)."""
def park_vehicle(self, vehicle: Vehicle) -> "Ticket": ...
def unpark_vehicle(self, ticket: "Ticket") -> float: ...
💡 Interview tip: This step is where interviewers check for SOLID principles:
Single Responsibility, each class does one thing (
FeeStrategydoesn't manage spots)Open/Closed,
FeeStrategyis open for extension (new strategies) but closed for modificationLiskov Substitution, any
Vehiclesubtype should work whereverVehicleis expectedInterface Segregation, don't force
ParkingSpotto implement unrelated methodsDependency Inversion,
ParkingLotdepends on the abstractFeeStrategy, not a concrete class
6. Coding the Core Flow
Why it matters: Now you implement, but only the core/happy-path flow, not every edge case. Interviewers want to see clean, working code for the primary use case within time constraints.
Example: Parking Lot (core flow implemented)
import threading
import uuid
class Car(Vehicle):
def __init__(self, license_plate: str):
super().__init__(license_plate, VehicleType.CAR)
class CompactSpot(ParkingSpot):
def __init__(self, spot_id: str):
self.spot_id = spot_id
self.spot_type = SpotType.COMPACT
self.vehicle: Vehicle | None = None
self._lock = threading.Lock()
def is_available(self) -> bool:
return self.vehicle is None
def assign_vehicle(self, vehicle: Vehicle) -> None:
with self._lock: # thread-safety, from our NFR!
if not self.is_available():
raise ValueError("Spot already occupied")
self.vehicle = vehicle
def remove_vehicle(self) -> None:
with self._lock:
self.vehicle = None
class HourlyFeeStrategy(FeeStrategy):
RATE_PER_HOUR = 20.0
def calculate_fee(self, entry_time, exit_time, vehicle) -> float:
hours = max(1, (exit_time - entry_time).seconds // 3600)
return hours * self.RATE_PER_HOUR
class Ticket:
def __init__(self, vehicle: Vehicle, spot: ParkingSpot):
self.ticket_id = str(uuid.uuid4())
self.vehicle = vehicle
self.spot = spot
self.entry_time = datetime.now()
class ParkingFloorImpl(ParkingFloor):
def __init__(self, floor_id: int, spots: list[ParkingSpot]):
self.floor_id = floor_id
self.spots = spots
def find_available_spot(self, spot_type: SpotType):
for spot in self.spots:
if spot.spot_type == spot_type and spot.is_available():
return spot
return None
class ParkingLotImpl(ParkingLot):
_instance = None # Singleton pattern
def __init__(self, floors: list[ParkingFloor], fee_strategy: FeeStrategy):
self.floors = floors
self.fee_strategy = fee_strategy
self.active_tickets: dict[str, Ticket] = {}
@classmethod
def get_instance(cls, floors=None, fee_strategy=None):
if cls._instance is None:
cls._instance = cls(floors, fee_strategy)
return cls._instance
def park_vehicle(self, vehicle: Vehicle) -> Ticket:
spot_type = SpotType.COMPACT if vehicle.vehicle_type == VehicleType.CAR else SpotType.LARGE
for floor in self.floors:
spot = floor.find_available_spot(spot_type)
if spot:
spot.assign_vehicle(vehicle)
ticket = Ticket(vehicle, spot)
self.active_tickets[ticket.ticket_id] = ticket
return ticket
raise Exception("Parking Lot Full")
def unpark_vehicle(self, ticket: Ticket) -> float:
exit_time = datetime.now()
fee = self.fee_strategy.calculate_fee(ticket.entry_time, exit_time, ticket.vehicle)
ticket.spot.remove_vehicle()
del self.active_tickets[ticket.ticket_id]
return fee
# ---- Wiring it together (core flow demo) ----
spots = [CompactSpot("F1-C1"), CompactSpot("F1-C2")]
floor1 = ParkingFloorImpl(1, spots)
lot = ParkingLotImpl.get_instance([floor1], HourlyFeeStrategy())
my_car = Car("KA-01-HH-1234")
ticket = lot.park_vehicle(my_car) # entry
print(f"Parked. Ticket: {ticket.ticket_id}")
fee = lot.unpark_vehicle(ticket) # exit
print(f"Fee charged: ₹{fee}")
💡 Interview tip: Write the "happy path" first (car enters → gets a spot → exits → pays), get it working conceptually, then mention edge cases verbally ("I'd also handle 'lot full' and 'invalid ticket' exceptions here") rather than spending your limited time coding every branch.
7. Validating with Use Cases
Why it matters: After coding, trace through your original requirements against your design out loud. This catches gaps before the interviewer has to point them out, a huge signal of maturity.
How to do it
Go back to Step 1's functional requirements one by one and verify:
Requirement | Does the design satisfy it? |
|---|---|
Multiple floors/spot types |
|
Assign spot on entry, free on exit |
|
Fee based on duration/type |
|
Thread-safe allocation | Lock inside |
Track available spot counts | Not implemented, could add a counter updated on assign/remove, or compute on demand. Flag as a known gap. |
Interview tip: Actively finding a flaw (like the race condition above) and proposing a fix in front of the interviewer is often scored higher than a "perfect" design presented silently. It shows critical thinking.
8. Discussing Extensibility
Why it matters: The final (and often most-weighted) part of an LLD interview: "What if we add X requirement?" Interviewers probe here to see if your design actually respects Open/Closed Principle, or if it would require a rewrite.
How to prepare
Have 2–3 ready extension scenarios and explain why your design handles them cleanly (or what minimal change is needed).
Example: Parking Lot extensibility
"What if we add Electric Vehicle spots with charging?"
Add a new
ElectricSpot(ParkingSpot)subclass andSpotType.ELECTRIC. No existing class needs modification, this is exactly what the Factory + inheritance structure was built for (Open/Closed Principle in action).
"What if pricing changes to surge pricing on weekends?"
Because we used the Strategy pattern for
FeeStrategy, we just add aSurgeFeeStrategyand inject it,ParkingLotcode doesn't change at all.
"What if we need to support multiple parking lots (a chain)?"
This is where Singleton becomes a liability, worth acknowledging! We'd refactor to a
ParkingLotManagerthat holds multipleParkingLotinstances, keyed by location. Good candidates flag when a chosen pattern has limits.
"What if we want real-time display boards showing free spots?"
Add the Observer pattern:
ParkingSpotorParkingFloornotifies subscribedDisplayBoardobjects on state change, no core logic touched.
💡 Interview tip: This step is your chance to show off pattern choices from Step 4 paying off. Explicitly say: "Because I used Strategy here, this extension is a 5-line addition, not a rewrite." That sentence alone signals strong design instinct.
Quick-Reference Summary (use this as interview checklist)
Requirements, Functional vs Non-Functional, ask questions, state assumptions.
Entities/Actors, Nouns → classes; distinguish system entities from external actors.
Relationships, Association / Aggregation / Composition / Inheritance + multiplicity; sketch it.
Patterns, Pick patterns because a requirement demands them; justify each choice.
Responsibilities/Interfaces, One responsibility per class (SRP), define method signatures before logic, apply SOLID.
Core Flow Code, Implement the happy path cleanly; mention edge cases verbally instead of coding all of them.
Validate, Walk requirements against the design; proactively surface gaps/race conditions.
Extensibility, Pre-think 2–3 "what if" scenarios; show how your pattern choices make them cheap.
Bonus: Other classic LLD problems to practice this process on
Design a Library Management System
Design a Splitwise / Expense Sharing App
Design an Elevator System
Design a Tic-Tac-Toe / Chess Game
Design a Ride-Sharing System (Uber-lite)
Design a Movie Ticket Booking System (BookMyShow-lite)
Design a Logging Framework
Design a Vending Machine (great for practicing the State pattern)
Run through all 8 steps on at least 2–3 of these, the process becomes muscle memory faster than memorizing any single solution.
PHASE 8: Low-Level Design (LLD) Interview Strategy
LLD interviews test whether you can turn a vague requirement into clean, extensible, object-oriented code, usually in 45-60 minutes. Here's how to approach them systematically.
1. Clarify Requirements (5-7 min)
Don't jump into coding. Ask questions to scope the problem:
Functional requirements: What features must the system support? (e.g., for a Parking Lot: multiple vehicle types, payment, spot allocation)
Non-functional/constraints: Concurrency? Scale? Persistence needed, or in-memory is fine?
Explicitly exclude things out of scope ("I'll skip authentication, focus on booking logic")
This shows maturity and prevents you from over-engineering or missing the point.
2. Identify Core Entities & Actors (5 min)
List the nouns in the problem, these become your classes. List the verbs, these become methods.
Example (Parking Lot): ParkingLot, ParkingSpot, Vehicle, Ticket, Payment, EntryGate, ExitGate.
3. Define Relationships (5 min)
Decide: is-a (inheritance) vs has-a (composition), and cardinality (1:1, 1:N, M:N).
Vehicle→Car,Bike,Truck(is-a, via interface/abstract class)ParkingLothas manyParkingSpots (has-a, composition)
Sketch this on a whiteboard/doc as a quick UML-lite diagram, boxes and arrows, not full UML syntax.
4. Apply Design Patterns, Only Where They Fit
Don't force patterns. Common natural fits:
Strategy: pricing/payment algorithms, spot-allocation algorithms
Factory: creating vehicle or spot objects
Singleton: a central manager (use cautiously, testability concerns)
Observer: notifications (e.g., spot becomes available)
Decorator: adding features to a base object (e.g., pizza toppings, discounts)
State: object behavior changes with status (e.g.,
Order: Placed → Shipped → Delivered)
Interviewers want to see you reach for a pattern because it solves a real problem, not to show off vocabulary.
5. Apply SOLID Principles As You Code
Single Responsibility, each class does one thing
Open/Closed, new vehicle type shouldn't require editing existing code
Liskov Substitution, subtypes must be substitutable for base types
Interface Segregation, don't force classes to implement methods they don't need
Dependency Inversion, depend on abstractions, not concrete classes
Narrate this out loud: "I'm making PaymentStrategy an interface so we can add new payment methods without touching Ticket."
6. Write the Code (20-25 min)
Start with interfaces/abstract classes, then concrete implementations
Use enums for fixed sets (VehicleType, Status)
Keep method signatures clean; stub out obvious getters/setters
Talk while typing, silence is worse than a wrong turn you catch yourself
7. Handle Edge Cases & Extensibility
Interviewer will often probe: "What if two cars enter at the same time?" (concurrency), "How would you add electric vehicle charging?" (extensibility). Show your design absorbs new requirements with minimal changes, that's the actual point of LLD.
8. Common Mistakes to Avoid
Jumping to code before clarifying scope
Over-engineering with patterns nobody asked for
Ignoring edge cases until prompted
Making everything a
ManagerorHelpergod-classForgetting to explain why, not just what
Practice Problems Worth Doing
Parking Lot, Elevator System, Library Management, Splitwise/Expense Sharing, Movie Ticket Booking (BookMyShow), Chess/Tic-Tac-Toe, Rate Limiter, LRU Cache, Vending Machine, Food Delivery (Swiggy/Zomato-style).
Wrapping Up
Low Level Design is where good architecture actually comes alive. High level design tells you what the system should do, but LLD is where you decide how, the classes, the interfaces, the relationships, and the design patterns that make your code maintainable, extensible, and easy to reason about six months from now.
Throughout this blog, we walked through the core principles, design patterns and all the LLD concepts with examples and diagrams. The goal was never to memorize patterns, but to build the instinct to ask the right questions: What's likely to change? What should be decoupled? What's the simplest design that still solves the problem well?
LLD isn't something you master by reading, it's a skill built through practice. The best way forward from here is to pick up classic problems (parking lot, elevator system, library management, rate limiter, etc.), design them on your own first, and then compare your approach against standard solutions. You'll learn far more from your own mistakes and trade-off decisions than from any perfect textbook answer.
If you found this useful, I'd love to hear your thoughts, feel free to drop a comment, share your own approach to any of the problems discussed, or reach out with questions. And if you're gearing up for interviews, LLD rounds reward clarity of thought over cleverness, so keep practicing the fundamentals.
Thanks for reading, and happy designing! 🚀
