On this page
- The Difference Between Low-Level Design and High-Level Design
- Example: An online shopping system
- What Are the Main Components of Low-Level Design?
- 1. Classes and objects
- 2. Attributes and methods
- 3. Interfaces and abstract classes
- 4. Relationships between classes
- 5. UML diagrams
- Which Design Principles Are Used in LLD?
- SOLID principles
- Encapsulation and abstraction
- Separation of concerns, cohesion, and coupling
- Common Design Patterns in Low-Level Design
- How to Approach a Low-Level Design Problem
- Low-Level Design Example: A Notification System
- Step 1: Define the requirements
- Step 2: Identify the main classes
- NotificationService
- NotificationChannel
- Step 3: Illustrative Python implementation
- Step 4: Understand why this design works
- Common Low-Level Design Mistakes
- 1. Creating a god class
- 2. Applying design patterns unnecessarily
- 3. Overusing inheritance
- 4. Ignoring error handling
- 5. Confusing diagrams with implementation quality
- 6. Designing for hypothetical future requirements
- 7. Ignoring testability
- Low-Level Design Best Practices
- Frequently Asked Questions
- What is Low-Level Design (LLD) in simple terms?
- Why is Low-Level Design important in software engineering?
- What is the difference between LLD and HLD?
- What are the main components of LLD?
- Is Low-Level Design the same as object-oriented design?
- Are UML diagrams required for every LLD problem?
- Which design patterns are useful in LLD interviews?
- How should beginners practise Low-Level Design?
- Which programming language is best for LLD interviews?
- Conclusion
Low-Level Design (LLD) is the process of defining how individual parts of a software system will work, including their classes, objects, methods, interfaces, and relationships. It translates high-level requirements into a more detailed design that developers can use to implement, test, and maintain software.
For example, when building a notification system, high-level design might identify a notification service and its connections to email, SMS, and push services. Low-level design determines which classes and interfaces are needed, what each component does, and how the system chooses the appropriate delivery method.
LLD helps developers organize code, manage complexity, and make software easier to test and extend. It commonly uses object-oriented design, UML diagrams, SOLID principles, and design patterns where they solve real problems.
In this guide, you’ll learn how LLD works, how it differs from High-Level Design (HLD), which principles and patterns matter, and how to approach a practical design problem.
The Difference Between Low-Level Design and High-Level Design
High-Level Design (HLD) describes the broader architecture of a software system, while Low-Level Design (LLD) describes the internal structure and behavior of its components. HLD helps establish the major building blocks and their connections; LLD helps developers determine how those building blocks should be implemented.
| Aspect | High-Level Design (HLD) | Low-Level Design (LLD) |
|---|---|---|
| Main focus | Overall system architecture | Individual components and their implementation |
| Typical elements | Services, databases, APIs, system boundaries | Classes, methods, interfaces, object relationships |
| Main questions | Which components are needed, and how do they communicate? | What responsibilities does each class have, and how does it behave? |
| Common artifacts | Architecture diagrams, component diagrams, data-flow diagrams | Class diagrams, sequence diagrams, method contracts |
| Primary audience | Architects, technical leads, developers | Developers, design reviewers, technical leads |
| Example decision | Use a notification service connected to a message queue | Define a notification interface and separate delivery implementations |
The distinction is about abstraction, not a rigid division of responsibility. A small application may need little formal HLD, while a large application may require extensive architectural planning before its components can be designed.
Example: An online shopping system
At the HLD level, the system might contain a product catalog, shopping cart, order service, payment integration, and database.
At the LLD level, the order component might define an Order class, an OrderItem class, an OrderService interface, and methods for calculating totals and updating order status.
Both levels are necessary when the system’s complexity warrants them. HLD establishes the broad structure; LLD fills in the details.
What Are the Main Components of Low-Level Design?
LLD is not limited to writing class definitions. It involves identifying responsibilities, defining contracts, modeling relationships, and considering how components interact.
1. Classes and objects
A class defines the data and behavior associated with a type. An object is an instance of that class.
For example, an Order class might contain an order ID, a collection of items, a status, and methods for calculating the total. Each individual purchase is represented by a separate Order object.
2. Attributes and methods
Attributes describe an object’s state, while methods define its behavior.
A shopping-cart class could have:
itemsto store the products in the cart.addItem()to add a product.removeItem()to remove a product.calculateTotal()to calculate the current total.
The exact design depends on requirements. For example, discounts, taxes, currency conversion, and shipping may deserve separate responsibilities rather than being placed in one large method.
3. Interfaces and abstract classes
An interface specifies a contract that implementations must follow. An abstract class can provide shared behavior while leaving certain operations to subclasses.
For example, a notification service could depend on a NotificationChannel interface rather than directly depending on an email implementation.
This separation allows developers to add SMS or push notifications without rewriting the service’s central workflow. Microsoft Learn explains how interfaces can reduce dependencies and support more resilient code.
4. Relationships between classes
The main relationships commonly considered in object-oriented design include:
- Association: one object knows about or works with another.
- Aggregation: one object groups other objects that can exist independently.
- Composition: an object owns constituent objects whose lifecycle is closely tied to it.
- Inheritance: a class specializes another class.
- Dependency: one component temporarily uses another component.
These distinctions help clarify ownership, responsibilities, and coupling. Their exact meaning should be represented consistently in the chosen modeling notation.
5. UML diagrams
The Unified Modeling Language (UML) provides standardized ways to visualize, specify, and document software and systems.
Two particularly useful diagram types are:
- Class diagrams: show classes, attributes, operations, and relationships.
- Sequence diagrams: show how objects or components interact over time.
A diagram is a communication tool, not a substitute for clear requirements or correct implementation. Use the level of detail that helps the team make decisions.
Which Design Principles Are Used in LLD?
Good low-level design aims to keep responsibilities clear and dependencies manageable. Several principles help achieve that goal.
SOLID principles
SOLID is a group of five object-oriented design principles.
| Principle | Meaning | Practical application |
|---|---|---|
| Single Responsibility | A class should have one coherent responsibility and one main reason to change | Separate notification delivery from message formatting |
| Open/Closed | Software entities should support appropriate extension without repeatedly modifying stable code | Add a new delivery implementation through an established interface |
| Liskov Substitution | Subtypes should remain usable wherever their base type is expected without breaking required behavior | Ensure a replacement implementation honors the interface contract |
| Interface Segregation | Clients should not depend on methods they do not need | Use focused interfaces instead of one oversized interface |
| Dependency Inversion | High-level policy should not depend directly on low-level implementation details | Make a notification service depend on an abstraction rather than a concrete email class |
These principles are guidelines, not rules that require the maximum number of classes or interfaces. A simple implementation may be the better choice when requirements are stable and the abstraction adds no practical benefit.
Encapsulation and abstraction
Encapsulation protects an object’s internal state by controlling how other components access or modify it.
Abstraction exposes the behavior that other components need while hiding unnecessary implementation details.
For example, a payment service might expose a processPayment() operation without requiring an order-processing component to understand the payment provider’s internal request format.
Separation of concerns, cohesion, and coupling
- Separation of concerns: keep unrelated responsibilities distinct.
- High cohesion: keep closely related behavior together.
- Low coupling: avoid unnecessary dependencies between components.
These ideas work together. If a class calculates prices, sends emails, stores database records, and processes refunds, changes to one responsibility can affect the others. Separating these concerns can make the system easier to test and maintain.
Common Design Patterns in Low-Level Design
Design patterns are reusable approaches to recurring design problems. They are not complete solutions that should be inserted into every project.
| Pattern | What it helps with | Example |
|---|---|---|
| Factory | Centralizes or simplifies object creation | Creating a notification channel based on configuration |
| Strategy | Makes interchangeable algorithms or behaviors easier to select | Choosing among delivery or pricing strategies |
| Observer | Notifies interested subscribers when an event occurs | Informing subscribers when an order changes status |
| Adapter | Allows incompatible interfaces to work together | Wrapping a third-party payment API behind an internal interface |
| Singleton | Restricts a class to a single shared instance | A genuinely process-wide resource where that constraint is appropriate |
The Strategy pattern, for example, separates interchangeable algorithms behind a common interface. The Observer pattern provides a subscription mechanism for notifying multiple interested objects about events.
Be careful with Singleton. Global shared state can make testing and dependency management harder. Dependency injection or an explicitly managed lifecycle may be preferable.
The right question is not “Which pattern can I use?” but “What design problem am I trying to solve?”
How to Approach a Low-Level Design Problem
A repeatable process is more useful than memorizing a collection of patterns.
Step 1: Clarify requirements
Identify the required behavior, constraints, edge cases, and expected inputs and outputs. Separate essential requirements from optional features.
Step 2: Identify entities and responsibilities
List the main concepts in the problem. Decide which objects own the data and which components should perform each operation.
Step 3: Define classes and interfaces
Give each component a clear contract. Decide which methods it exposes and which implementation details it should hide.
Step 4: Model relationships
Use a class diagram when it clarifies associations, ownership, inheritance, and dependencies.
Step 5: Walk through object interactions
Trace the main use cases from start to finish. A sequence diagram can help expose missing operations or unclear responsibilities.
Step 6: Check edge cases and testability
Consider invalid inputs, failures, repeated requests, concurrency where relevant, and how individual components can be tested.
Step 7: Refine the design
Remove unnecessary abstractions, reduce avoidable dependencies, and verify that the design satisfies the original requirements.
This process is iterative. New requirements or discovered constraints may require revisiting earlier decisions.
Low-Level Design Example: A Notification System
Consider an application that must send notifications through email, SMS, or push notifications. The system should allow a new delivery channel to be added without rewriting its core notification workflow.
Step 1: Define the requirements
The system must:
- Accept a notification message and recipient.
- Support multiple delivery channels.
- Select an appropriate channel for each request.
- Report delivery failures.
- Allow new channel implementations to be added later.
For this example, assume channel selection is supplied by the caller. A production system might use user preferences, delivery policies, retries, queues, and provider-specific rate limits.
Step 2: Identify the main classes
NotificationService
Coordinates notification delivery
send(message, recipient, channel)
Uses the common contract
NotificationChannel
Interface
send(message, recipient)
Implemented by
EmailChannel
SMSChannel
PushChannel
This is a simplified conceptual diagram, not a complete UML specification. The key design decision is that NotificationService relies on a common interface rather than being tightly coupled to each delivery provider.
Step 3: Illustrative Python implementation
The following code demonstrates the core abstraction. Real integrations would need provider-specific authentication, validation, error handling, observability, and security controls.
from typing import Protocolclass NotificationChannel(Protocol): def send(self, message: str, recipient: str) -> None: ...class EmailChannel: def send(self, message: str, recipient: str) -> None: print(f"Sending email to {recipient}: {message}")class SMSChannel: def send(self, message: str, recipient: str) -> None: print(f"Sending SMS to {recipient}: {message}")class NotificationService: def __init__(self, channel: NotificationChannel): self.channel = channel def send(self, message: str, recipient: str) -> None: self.channel.send(message, recipient)email_service = NotificationService(EmailChannel())email_service.send("Your order has shipped.", "customer@example.com")sms_service = NotificationService(SMSChannel())sms_service.send("Your order has shipped.", "+15551234567")
Step 4: Understand why this design works
- Separation of concerns: the service coordinates delivery, while each channel handles its own delivery mechanism.
- Dependency inversion: the service depends on a shared contract rather than a specific provider.
- Open/Closed principle: a new channel can generally be added without changing the service’s implementation.
- Testability: a test double can implement the interface and verify that the service delegates correctly.
- Extensibility: provider-specific logic can be isolated within each implementation.
The example also has limitations. Channel selection is manual, delivery results are not modeled, and provider failures are not handled. A real system may need a result type, retry policy, queue, idempotency controls, and monitoring.
The lesson is that good LLD does not mean creating the largest possible class hierarchy. It means choosing a structure that satisfies requirements while keeping change manageable.
Common Low-Level Design Mistakes
1. Creating a god class
A single class that handles every responsibility becomes difficult to understand, test, and modify. Separate responsibilities when doing so creates clearer boundaries.
2. Applying design patterns unnecessarily
A pattern should address a real design problem. Introducing several abstractions into a stable, simple workflow can increase complexity without delivering a benefit.
3. Overusing inheritance
Inheritance is useful when a genuine subtype relationship exists and the subtype can honor the base type’s contract. Composition or interfaces may be more suitable when behavior needs to be combined flexibly.
4. Ignoring error handling
A design that covers only the successful path may fail under invalid inputs, unavailable dependencies, or partial failures. Identify expected failure cases before implementation.
5. Confusing diagrams with implementation quality
A polished class diagram does not prove that the code is correct or secure. Validate the design against requirements, test behavior, and review implementation details.
6. Designing for hypothetical future requirements
Extensibility matters, but unnecessary flexibility creates extra work. Prefer a simple design that supports current requirements and has a reasonable path for likely changes.
7. Ignoring testability
If a component cannot be tested without starting every external service, its dependencies may be too tightly coupled. Interfaces, dependency injection, and clear boundaries can help.
Google’s published code-review guidance similarly emphasizes design, functionality, complexity, tests, naming, and documentation when reviewing code.
Low-Level Design Best Practices
Use this checklist when reviewing an LLD:
- Each class has a clear responsibility.
- Public methods have understandable contracts.
- Dependencies are explicit and manageable.
- Interfaces are focused and useful.
- Relationships represent actual ownership and behavior.
- Error cases and boundary conditions are considered.
- Important behavior can be tested independently.
- Design patterns are used only where justified.
- Diagrams clarify decisions rather than duplicate unnecessary detail.
- The design can accommodate plausible changes without excessive complexity.
The best design is not necessarily the most abstract or elaborate. It is the simplest design that meets the requirements and can evolve responsibly.
Frequently Asked Questions
What is Low-Level Design (LLD) in simple terms?
Low-Level Design is the detailed plan for how a software component will work. It defines the classes, objects, methods, interfaces, and relationships developers need to implement a feature. It bridges the gap between broad system requirements and working code.
Why is Low-Level Design important in software engineering?
LLD helps developers clarify responsibilities before implementation, reduce unnecessary dependencies, identify missing behavior, and plan testing. A well-considered design can make changes easier, although it cannot guarantee that software will be bug-free or performant.
What is the difference between LLD and HLD?
HLD describes the system’s overall architecture, including major services and their interactions. LLD focuses on the internal structure of individual components, such as classes, methods, interfaces, and object relationships. The two levels complement each other.
What are the main components of LLD?
Common components include classes, objects, attributes, methods, interfaces, class relationships, method contracts, and relevant UML diagrams. Depending on the system, the design may also address data models, validation, error handling, and concurrency.
Is Low-Level Design the same as object-oriented design?
No. Object-oriented design is an approach to structuring software around objects and their responsibilities. LLD is a broader implementation-level design activity that may use object-oriented design but can also use other paradigms and techniques.
Are UML diagrams required for every LLD problem?
No. UML diagrams are useful when they clarify structure or behavior, especially in complex systems and collaborative work. A small change may need only a concise explanation or sketch, while a more complex design may benefit from class and sequence diagrams.
Which design patterns are useful in LLD interviews?
Factory, Strategy, Observer, and Adapter are useful patterns to understand because they address common creation, behavior-selection, event-notification, and integration problems. Learn the underlying problem each pattern solves instead of memorizing names.
How should beginners practise Low-Level Design?
Start with a small problem, such as a notification system or library-management application. Write down requirements, identify entities, define responsibilities, draw a simple class diagram, walk through key interactions, and test edge cases. Refine the design before adding complexity.
Which programming language is best for LLD interviews?
There is no universally best language. Choose one you can use comfortably to express classes, interfaces or abstractions, object relationships, and clean method contracts. Follow the interview’s language requirements and focus on design reasoning rather than language-specific syntax.
Conclusion
Low-Level Design turns software requirements into a detailed, implementable structure. It helps developers decide how classes, interfaces, methods, and components should work together while keeping responsibilities clear and dependencies manageable.
To improve your LLD skills, practise taking one small requirement from start to finish: clarify the behavior, model the components, explain their interactions, and test the design against realistic changes. Learn SOLID principles and design patterns as tools for solving specific problems, not as rules to apply everywhere.
The goal is not to create a complicated design. It is to create software that is understandable, testable, and appropriate for the requirements.