Loading content...
If High-Level Design is the architect's blueprint, Low-Level Design (LLD) is the structural engineer's detailed plans—the precise specifications that construction workers follow to build the actual structure.
Where HLD answers "What components does this system need?", LLD answers "How do we build each component?" It's the bridge between architectural vision and executable code, the layer where abstract boxes in diagrams become concrete classes, interfaces, and methods.
Low-Level Design is where software engineering craftsmanship truly shines. A well-designed class hierarchy, a clean interface, a method with exactly the right signature—these are the marks of an engineer who understands not just what to build, but how to build it elegantly.
By the end of this page, you will understand the scope and importance of Low-Level Design, how to think in terms of classes, interfaces, and methods, and the key principles that guide implementation-level design decisions. You'll see how LLD brings architectural decisions to life.
Low-Level Design (LLD) is the process of designing the internal structure of individual components. While HLD defines what components exist and how they interact, LLD defines how each component works internally.
LLD is concerned with:
The Scope of LLD
LLD operates at the level of a single service or component. If HLD produced a box labeled "User Service," LLD designs everything inside that box:
If HLD is the 30,000-foot view of a city, LLD is walking the streets. You see individual buildings, their entrances, their internal layouts. You understand how people move through spaces, where they wait, where they go. This ground-level perspective is essential for actually building something that works.
In object-oriented design, classes are the fundamental units of organization. A class defines:
Good class design is about finding the right abstractions—grouping related state and behavior in ways that make the system understandable, maintainable, and extensible.
Principles of Good Class Design
Several time-tested principles guide effective class design:
SOLID principles are guidelines, not laws. Blindly applying them can lead to over-engineering—dozens of tiny classes and interfaces that are harder to understand than a single, slightly-larger class. Use judgment. The goal is maintainability, not adherence to dogma.
Class Relationships
Classes don't exist in isolation. Understanding how classes relate to each other is crucial:
Interfaces define contracts—promises about what a class can do without specifying how it does it. They are the cornerstone of abstraction in LLD.
An interface says: "I guarantee these methods exist with these signatures." It doesn't say anything about implementation. This separation of what from how is powerful:
FileLogger, DatabaseLogger, CloudLogger all implement Logger)1234567891011121314151617181920212223242526272829303132333435363738394041424344
// Interface defines the contractinterface PaymentProcessor { processPayment(amount: number, currency: string): PaymentResult; refund(transactionId: string): RefundResult; getTransactionStatus(transactionId: string): TransactionStatus;} // Stripe implementationclass StripePaymentProcessor implements PaymentProcessor { private stripe: StripeClient; constructor(apiKey: string) { this.stripe = new StripeClient(apiKey); } processPayment(amount: number, currency: string): PaymentResult { // Stripe-specific implementation const charge = this.stripe.charges.create({ amount, currency }); return { success: true, transactionId: charge.id }; } // ... other methods} // PayPal implementationclass PayPalPaymentProcessor implements PaymentProcessor { private paypal: PayPalClient; processPayment(amount: number, currency: string): PaymentResult { // PayPal-specific implementation const payment = this.paypal.createPayment({ amount, currency }); return { success: true, transactionId: payment.id }; } // ... other methods} // Calling code works with any implementationfunction checkout(processor: PaymentProcessor, amount: number) { const result = processor.processPayment(amount, 'USD'); if (result.success) { console.log(`Payment successful: ${result.transactionId}`); }}Interface Design Principles
Well-designed interfaces are:
EmailSender sends emails; it doesn't also validate email addresses.Avoid 'god interfaces' with dozens of methods. If an interface is hard to implement because it has too many methods, it's violating the Interface Segregation Principle. Split it. If an interface has no implementations, question whether it's needed at all.
Methods (or functions) are where computation actually happens. They're the verbs of your system—the actions that transform state, make decisions, and produce results.
Method design is a micro-level craft, but its importance is immense. A system is only as good as its methods.
Method Signature Design
A method signature communicates intent through:
calculateTotal, sendEmail, validateUser)null when you can return Optional or an empty collection| Purpose | Naming Pattern | Example | Notes |
|---|---|---|---|
| Query (read) | get*, find*, is*, has*, can* | getUser(), findByEmail(), isActive() | Should have no side effects |
| Command (write) | create*, update*, delete*, set*, add* | createOrder(), updateProfile(), deleteAccount() | May modify state |
| Calculation | calculate*, compute*, determine* | calculateTotal(), computeHash() | Pure function, no side effects |
| Conversion | to*, as*, from* | toString(), asDTO(), fromEntity() | Transforms one type to another |
| Validation | validate*, check*, verify* | validateInput(), checkPermissions() | Returns boolean or throws |
| Factory | create*, build*, make*, of* | List.of(), StringBuilder.build() | Creates and returns new objects |
processOrder) with low-level details (formatStreetAddress).getNextItem() that also removes it is confusing.validate, it shouldn't also modify the input. Side effects should be obvious from the name.Good code reads like a newspaper: important high-level concepts at the top (method names that tell you what happens), details as you go deeper (implementation). A developer should understand what a method does from its signature before reading the body.
Design patterns are reusable solutions to common problems in software design. They're not code you copy-paste—they're templates that guide your design decisions.
In LLD, design patterns help you:
| Pattern | Category | Purpose | When to Use |
|---|---|---|---|
| Factory | Creational | Create objects without specifying exact class | When creation logic is complex or varies |
| Builder | Creational | Construct complex objects step by step | Objects with many optional parameters |
| Singleton | Creational | Ensure only one instance exists | Shared resources; use sparingly |
| Strategy | Behavioral | Define family of interchangeable algorithms | When behavior varies based on context |
| Observer | Behavioral | Notify multiple objects of state changes | Event systems, reactive updates |
| Decorator | Structural | Add behavior to objects dynamically | Adding features without subclassing |
| Adapter | Structural | Convert interface to expected interface | Integrating with external libraries |
| Repository | Architectural | Mediate between domain and data mapping | Clean separation of persistence logic |
A common junior developer mistake is seeing patterns everywhere and applying them unnecessarily. Patterns add complexity. Use them when you genuinely need the flexibility they provide, not because you learned about them and want to practice. Simplicity beats cleverness.
Patterns as Vocabulary
Beyond their direct utility, patterns provide a shared vocabulary. When you say 'This class is a Decorator over the base service,' experienced developers immediately understand the structure without seeing the code. This communication efficiency is invaluable in team settings.
LLD is where data structure and algorithm choices become concrete. While HLD might say 'we need a cache,' LLD decides what data structure backs that cache and how it handles eviction.
Choosing Data Structures
Every data structure has trade-offs:
| Data Structure | Access | Search | Insert | Delete | When to Use |
|---|---|---|---|---|---|
| Array/List | O(1) | O(n) | O(n) | O(n) | Fixed-size, index access |
| Linked List | O(n) | O(n) | O(1) | O(1) | Frequent insert/delete |
| Hash Map | O(1)* | O(1)* | O(1)* | O(1)* | Key-value lookup |
| Tree Set | O(log n) | O(log n) | O(log n) | O(log n) | Sorted, range queries |
| Heap | O(1) top | O(n) | O(log n) | O(log n) | Priority access |
*Average case, assuming good hash distribution
Algorithm Selection
Similarly, algorithm choice impacts performance:
Asymptotic complexity matters, but constant factors matter too. An O(n) algorithm with huge constants can be slower than O(n log n) for practical input sizes. Also consider cache locality, memory usage, and parallelizability. Profile before optimizing.
LLD produces specific artifacts that guide implementation:
From Diagram to Code
Unlike HLD diagrams that remain abstract, LLD diagrams should be directly translatable to code:
This tight coupling between diagram and code is what makes LLD 'low-level'—it's close to the final implementation.
We've explored the fundamentals of Low-Level Design. Let's consolidate the key takeaways:
What's Next:
Now that we understand both HLD and LLD independently, we'll explore when to focus on each. In the next page, we'll discuss the contexts, scenarios, and criteria that determine whether you should be thinking architecturally or diving into implementation details.
You now understand Low-Level Design—the world of classes, interfaces, and methods where architectural decisions become executable code. Next, we'll explore when to focus on HLD versus LLD and how to switch between these complementary perspectives.