Skip to content
LLD: Master the Building Blocks of Low-Level Design 54 min

LLD: Master the Building Blocks of Low-Level Design

ScaleDojo
ScaleDojo
Jul 14, 2026
54 min read
12,859 words
Cover Image.png

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, PaymentHandler

  • Methods: Ride.assignDriver(), Trip.calculateFare()

  • Design pattern: maybe a Strategy pattern for different fare calculation types (surge pricing, flat rate)

  • Database tables: rides, drivers, payments with exact columns

  • Sequence 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.

Class and Object in OOPs

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.

Constructor in OOPs

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.

Encapsulation in OOPs

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.

Abstraction in OOPs

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.

Inheritance in OOPs

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.

Polymorphism in OOPs

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.

Interfaces in OOPs

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.

Association in OOPs

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.

Aggregation

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.

Composition

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

UML and Object Modeling

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:
        pass

Visibility 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 ------ Order
  • Aggregation: A weak HAS-A relationship.

Department ◇──── Employee
  • Composition: A strong HAS-A relationship.

House ◆──── Room
  • Inheritance (Generalization): An IS-A relationship.

Dog --------▷ Animal
  • Realization: Used for Interfaces.

CreditCard - - -▷ Payment
  • Dependency:

OrderService ------> PaymentGateway

These 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:

  • Student and Course are separate classes.

  • A Student has data (id, name) and behavior (enroll(), dropCourse()).

  • A Course has its own attributes and methods.

  • One student can enroll in multiple courses (indicated by *).


Quick Visual Recap

Class Diagram

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

Student

Object Diagram

student1 : Student

student2 : Student

Notice the difference.

The Object Diagram uses instances.


Components of an Object Diagram

1. Object: An object is an instance of a class.

Notation

student1 : Student

Meaning

Object Name

student1

Class

Student

2. 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 -------- course1

Meaning

Student 1 is enrolled in Course 1. Links represent runtime relationships.


Example : Student Management System

Suppose we have:

Classes

Student

Course

Objects

student1

course1

Object 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

Object Diagram

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

👤 User

The actor always initiates the interaction.


2. Objects (Participants): Objects represent the classes or services participating in the interaction.

User

LoginController
AuthService
UserRepository
Database

These 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 ---------> Service

Example:

service.login();

The controller cannot continue until login() returns.


  • Asynchronous Message: The sender does not wait.

NotificationService -----> EmailQueue

Example:

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 error

2. 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

Sequence Diagram

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
     /|\
     / \
 Customer

2. 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)                             |
|                                          |
+------------------------------------------+

Customer

This 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      Admin

Both 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

Use Case Diagram

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
    ↓
Delivered

Sometimes things don't go as planned:

Created
    ↓
Cancelled

or

Shipped
    ↓
Returned

A 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
Created

Every 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 --------> Approved

A transition occurs when an event happens.


4. Event: An Event triggers a transition.

Example:

Pending ----approve()----> Approved

Here, 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]--> Confirmed

The transition happens only when payment succeeds.

Python equivalent:

if paymentSuccessful:
    state = CONFIRMED

6. Action: A transition may execute an action.

Example:

Pending
    |
approve()
/sendEmail()
    |
    ▼
Approved

When 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

State Diagram

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
   ↓
End

Each 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
Login

Every 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 User

It 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 Error

5. Merge Node: A Merge Node combines multiple alternative paths back into a single flow.

Example:

Success ----\
             \
              ◇
             /
Failure ----/
      |
      v
Generate Report

Unlike 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 Inventory

Both activities start at the same time.


7. Join Node: A Join Node synchronizes parallel activities.

Example:

Send Email -------\
                   \
                    ==========
                   /
Update Inventory--/
        |
        v
Complete Order

The 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

Activity Diagram

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):
        pass

This class handles:

  • Registration

  • Email

  • Reporting

  • Database

Too many responsibilities.


Good Design

UserService
      |
      |---- UserRepository
      |
      |---- EmailService
      |
      |---- ReportService

Each 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

Single Responsibility Principle (SRP)

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

Open/Closed Principle

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 fly

Why 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):
    pass

Penguin:

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
Swimming

Now 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

Liskov Substitution Principle (LSP)

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):
        pass

Now 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 supported

Better Design

Split the large interface into smaller interfaces.

Print Interface

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print_document(self):
        pass

Scanner Interface

from abc import ABC, abstractmethod

class Scanner(ABC):
    @abstractmethod
    def scan_document(self):
        pass

Fax Interface

from abc import ABC, abstractmethod

class Fax(ABC):
    @abstractmethod
    def fax_document(self):
        pass

Basic 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

Interface Segregation Principle (ISP)

5. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) is the fifth and final principle of the SOLID principles.

Definition (Robert C. Martin):

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.

  2. 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        Lenovo

The 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
      │
      ▼
 EmailService

Now 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):
        pass

Step 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:

MessageService

Quick Visual Recap

Dependency Inversion Principle (DIP)

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 Logic

Use this:

          TaxService
              │
     ┌────────┼────────┐
     │        │        │
 Checkout Invoice Refund

Now 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.10

The 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.10

Now 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

DRY Pricniple

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 False

Better Design

def is_eligible(age):
    return age >= 18

KISS 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

KISS Principle

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):
        pass

When the business later requires SMS support, add it then.


Quick Visual Recap

YAGNI Principle

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

The Law of Demeter (LoD)

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

Separation of Concerns (SoC)

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):
        pass

This 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):
        pass

All methods are related to users.


Quick Visual Recap

Cohesion

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

Coupling

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 Vehicle

Composition → Has-A

Car HAS-A Engine

Computer HAS-A Keyboard

Order HAS-A Payment

Composition 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 sent

Why is this better?

NotificationService doesn't inherit from EmailService or Logger. Instead, it has an EmailService and a Logger.

NotificationService
      │
      ├── EmailService
      │
      └── Logger

Quick Visual Recap

Composing Objects Principle

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

Design Pattern

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
PayPal

The 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

Credential Design Pattern

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

Notification

The 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-C

But the device provides:

USB-A

An adapter bridges the gap.

Example

Laptop
↓
USB Adapter
↓
Old USB Device

Real-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
BlueSquare

As more shapes and colors are added, the number of classes grows rapidly.

With Bridge:

Shape
  |
Color

Any 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

      └── File

Whether 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 Cream

Instead of creating:

MilkCoffee

SugarCoffee

MilkSugarCoffee

MilkSugarCreamCoffee

You 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 System

Instead 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 Trees

Instead 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
↓
Database

The proxy may:

  • Check permissions

  • Cache results

  • Delay object creation

  • Log requests

Real-world Examples

  • Authentication

  • Lazy loading

  • Caching

  • Remote services


Quick Visual Recap

Structural Design Pattern

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 Pattern

The 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

└── PayPalPayment

The 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   Inventory

Each 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
↓
Television

The 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
↓
Delivered

The 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
↓
Manager

Each 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 B

Planes 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 Song

The 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
↓
Serve

Tea 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
↓
Furniture

Each 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 State

XI. Interpreter Pattern

Purpose: Defines a grammar and interprets expressions according to that grammar.

Example

A calculator interpreting:

10 + 20 * 5

or a search engine interpreting:

(status = "Active") AND (age > 18)

Quick Visual Recap

Behavioral Design Pattern

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:

  1. Check "is A1 available?" → Yes (for both)

  2. Both proceed to book it

  3. 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., seat_available, a bank balance, an inventory count)

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:

  1. Read counter into a temporary register

  2. Add 1 to it

  3. 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.Lock at 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 what threading.Condition gives 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):

  1. Mutual exclusion, resources can't be shared

  2. Hold and wait, a thread holds one resource while waiting for another

  3. No preemption, a resource can't be forcibly taken away

  4. 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:

  • threading in 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 of threading

Workload Type

Best Tool in Python

I/O-bound (API calls, DB, file I/O)

threading or asyncio

CPU-bound (heavy computation)

multiprocessing

Interview line that shows depth: "I'd use threading here 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)

  1. What's the difference between concurrency and parallelism?

  2. Why isn't x += 1 atomic in Python?

  3. Why do we check if instance is None twice in a thread-safe Singleton?

  4. What are the four conditions required for a deadlock, and how do you prevent it?

  5. When would you choose multiprocessing over threading in Python?

  6. 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 Account class with a balance and a withdraw(amount) method

  • Spin 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

  1. Underline the nouns in your requirements.

  2. Classify each as an Entity (has state, is part of the system) or an Actor (external, triggers actions on the system).

  3. 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 PaymentGateway class, just a Fee value or FeeCalculator.


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

Driver uses Ticket

Aggregation ("has-a", weak)

Whole-part, but part can exist independently

ParkingFloor has ParkingSpots, but spots aren't destroyed if floor object goes away conceptually

Composition ("has-a", strong)

Whole-part, part's lifecycle is tied to whole

ParkingLot is composed of ParkingFloors, floors don't exist without the lot

Inheritance ("is-a")

Specialization

Car, Bike, Truck are-a Vehicle

Multiplicity

How many of each side

One ParkingLot → many ParkingFloor → many ParkingSpot

Example: Parking Lot relationships

  • ParkingLot 1 → many ParkingFloor (composition)

  • ParkingFloor 1 → many ParkingSpot (composition)

  • Vehicle is a base class; Car, Bike, Truck inherit from it

  • Ticket associates a Vehicle with a ParkingSpot and holds timestamps

  • ParkingSpot 1 → 0..1 Vehicle at 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

ParkingLot itself (only one lot instance managing state)

Factory

Object creation logic varies by type and should be centralized

SpotFactory or VehicleFactory to create the right spot/vehicle type

Strategy

An algorithm/behavior needs to vary and swap at runtime

FeeCalculationStrategy (hourly vs flat-rate vs weekend pricing)

Observer

Multiple parts need to be notified when state changes

Notify DisplayBoard when spot availability changes

Decorator

Add responsibilities to objects dynamically

Adding surge pricing on top of base fee

State

An object's behavior changes based on internal state

ParkingSpot behaving differently when FREE vs OCCUPIED vs RESERVED

Command

Encapsulate a request as an object (undo/redo, queuing)

Entry/Exit operations as commands for logging

Example: Parking Lot decisions

  • SingletonParkingLot (only one lot in the system).

  • StrategyFeeStrategy, because the requirement said fee calc should be flexible (hourly now, maybe flat-rate later).

  • FactoryVehicleFactory, 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 (FeeStrategy doesn't manage spots)

  • Open/Closed, FeeStrategy is open for extension (new strategies) but closed for modification

  • Liskov Substitution, any Vehicle subtype should work wherever Vehicle is expected

  • Interface Segregation, don't force ParkingSpot to implement unrelated methods

  • Dependency Inversion, ParkingLot depends on the abstract FeeStrategy, 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

ParkingFloor holds spots by SpotType; multiple floors supported in ParkingLot.floors

Assign spot on entry, free on exit

park_vehicle / unpark_vehicle

Fee based on duration/type

FeeStrategy.calculate_fee

Thread-safe allocation

Lock inside assign_vehicle, but find_available_spot + assign_vehicle together aren't atomic, race condition! Worth calling out and fixing (e.g., lock at floor level, or use an atomic "find-and-reserve").

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 and SpotType.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 a SurgeFeeStrategy and inject it, ParkingLot code 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 ParkingLotManager that holds multiple ParkingLot instances, 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: ParkingSpot or ParkingFloor notifies subscribed DisplayBoard objects 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)

  1. Requirements, Functional vs Non-Functional, ask questions, state assumptions.

  2. Entities/Actors, Nouns → classes; distinguish system entities from external actors.

  3. Relationships, Association / Aggregation / Composition / Inheritance + multiplicity; sketch it.

  4. Patterns, Pick patterns because a requirement demands them; justify each choice.

  5. Responsibilities/Interfaces, One responsibility per class (SRP), define method signatures before logic, apply SOLID.

  6. Core Flow Code, Implement the happy path cleanly; mention edge cases verbally instead of coding all of them.

  7. Validate, Walk requirements against the design; proactively surface gaps/race conditions.

  8. 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).

  • VehicleCar, Bike, Truck (is-a, via interface/abstract class)

  • ParkingLot has many ParkingSpots (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 Manager or Helper god-class

  • Forgetting 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! 🚀

Enjoyed this article?

Share it with your network to help others level up their system design skills.

Discussion0

Join the Discussion

Sign in to leave comments, reply to others, or like insights.

Sign In to ScaleDojo

No comments yet. Be the first to start the thread!

Related Articles

Enjoyed this content?

Your support keeps us creating free resources

We put a lot of hours into researching and writing these guides. If it helped you, consider buying us a coffee. Every bit goes toward keeping ScaleDojo's content free and growing.

$

One-time payment via Stripe. ScaleDojo account required.