Loading content...
In the previous page, we explored the 30,000-foot view of High-Level Design—the world of services, databases, and data flows. Now we descend to ground level, where architectural blueprints become executable code. This is the realm of Low-Level Design (LLD).
If HLD answers "What components does this system have?", LLD answers "How is each component built internally?" It's the difference between knowing a building has floors, walls, and an elevator versus knowing the precise dimensions of each room, the material of each wall, and the mechanism of each door.
Low-Level Design is where software craftsmanship truly manifests. A well-designed class hierarchy, a thoughtfully abstracted interface, a method with exactly the right parameters—these are the marks of an engineer who understands not just what to build, but how to build it with elegance, maintainability, and extensibility.
By the end of this page, you will understand what Low-Level Design encompasses, the fundamental building blocks of LLD (classes, objects, methods, relationships), and how these elements combine to create well-structured, maintainable software components.
Low-Level Design (LLD) is the process of designing the internal structure of individual software components. While HLD defines what components exist and how they interact, LLD defines what's inside each component—the classes, interfaces, methods, and data structures that implement the component's functionality.
LLD is concerned with:
The Scope of LLD
LLD operates at the component level. If HLD produced an architecture with a "User Service" box, LLD designs everything inside that box:
If HLD is viewing a city from an airplane, LLD is walking the streets. You see individual buildings, their entrances, their floor plans. You understand how people move through spaces, where they wait, how doors connect rooms. This ground-level perspective is essential for actually constructing something that works.
Classes are the fundamental unit of organization in object-oriented design. A class defines a blueprint for creating objects—specifying what data they hold and what operations they can perform.
A class consists of:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
// A well-designed class with clear responsibilitiesclass BankAccount { // Attributes (state) private readonly accountNumber: string; private balance: number; private readonly owner: string; private transactionHistory: Transaction[]; // Constructor (initialization) constructor(accountNumber: string, owner: string, initialBalance: number = 0) { this.accountNumber = accountNumber; this.owner = owner; this.balance = initialBalance; this.transactionHistory = []; } // Methods (behavior) public deposit(amount: number): void { if (amount <= 0) { throw new Error("Deposit amount must be positive"); } this.balance += amount; this.recordTransaction("DEPOSIT", amount); } public withdraw(amount: number): void { if (amount <= 0) { throw new Error("Withdrawal amount must be positive"); } if (amount > this.balance) { throw new Error("Insufficient funds"); } this.balance -= amount; this.recordTransaction("WITHDRAWAL", amount); } public getBalance(): number { return this.balance; } // Private helper method private recordTransaction(type: string, amount: number): void { this.transactionHistory.push({ type, amount, timestamp: new Date(), balanceAfter: this.balance }); }}What Makes a Good Class?
Not all classes are created equal. Well-designed classes share common characteristics:
Objects are instances of classes—concrete manifestations of the abstract blueprint. While a class defines what a BankAccount is, an object is a specific bank account with a specific account number, owner, and balance.
Understanding objects requires grasping three fundamental concepts:
Object Lifecycle
Objects have a lifecycle—they're created, used, and eventually destroyed:
Not everything that holds data is an object in the OOP sense. Data Transfer Objects (DTOs) and Value Objects are 'data structures'—they expose data and have minimal behavior. True objects hide their data and expose behavior. Understanding this distinction is crucial for good LLD.
| Characteristic | Object (OOP) | Data Structure |
|---|---|---|
| Data Access | Encapsulated (private) | Exposed (public) |
| Behavior | Rich, domain-focused | Minimal or none |
| Purpose | Model domain concepts | Transfer or store data |
| Example | BankAccount, OrderProcessor | UserDTO, Point, Config |
| Changes | Add behavior easily, data changes hard | Add data easily, behavior changes hard |
Methods are where computation happens—the executable units that perform work. They're the verbs of your system, the actions that transform state, make decisions, and produce results.
A method has several components:
Method Categories
Methods serve different purposes in a class. Understanding these categories helps you design intentional APIs:
| Category | Purpose | Naming Patterns | Examples |
|---|---|---|---|
| Queries | Return information without side effects | get*, find*, is*, has*, can* | getBalance(), findByEmail(), isActive() |
| Commands | Modify state or cause side effects | create*, update*, delete*, set*, add* | createOrder(), updateProfile(), deleteUser() |
| Calculations | Compute and return a value (pure) | calculate*, compute*, determine* | calculateTax(), computeHash() |
| Conversions | Transform between representations | to*, as*, from* | toString(), asDTO(), fromEntity() |
| Validation | Check conditions, return boolean or throw | validate*, check*, verify* | validateInput(), checkPermissions() |
| Factories | Create and return new objects | create*, build*, make*, of* | createUser(), List.of() |
Good code reads like a newspaper: headlines (method names) at the top telling you what happens, details (implementation) as you read deeper. A developer should understand what a method does from its signature before reading the body.
Objects don't exist in isolation—they relate to other objects. These relationships are fundamental to LLD because they determine how objects collaborate, how changes propagate, and how the system can evolve.
Understanding relationship types is essential for designing flexible, maintainable systems.
| Relationship | Description | UML Notation | Example |
|---|---|---|---|
| Association | Objects that know about each other | Solid line | Student ↔ Course (enrolled in) |
| Dependency | One object uses another temporarily | Dashed arrow | OrderProcessor → EmailService (uses) |
| Aggregation | 'Has-a' with independent lifecycle | Empty diamond | Department ◇── Employee (contains) |
| Composition | 'Has-a' with dependent lifecycle | Filled diamond | House ◆── Room (made of) |
| Inheritance | 'Is-a' relationship | Empty triangle | Dog ▷── Animal (extends) |
| Realization | Implements an interface | Dashed triangle | PayPalProcessor ▷- - PaymentProcessor |
Dependency Direction: The Key to Maintainability
The direction of dependencies is crucial. Well-designed systems have dependencies that flow in one direction—typically from high-level policy toward low-level details. This is the Dependency Inversion Principle in action.
Consider: Should OrderService depend on MySQLDatabase, or should both depend on an abstract Repository interface?
The second option is better because:
OrderService doesn't know or care about MySQLOrderServiceIf A depends on B, and B depends on A, you have a circular dependency. This makes code harder to understand, test, and modify. Break cycles by introducing abstractions (interfaces) or rethinking responsibilities.
Interfaces define contracts—promises about what an object can do without specifying how it does it. They are perhaps the most powerful abstraction mechanism in object-oriented design.
An interface declares:
An interface does NOT specify:
1234567891011121314151617181920212223242526272829303132333435363738
// Interface defines WHAT, not HOWinterface PaymentProcessor { processPayment(amount: number, currency: string): Promise<PaymentResult>; refund(transactionId: string, amount?: number): Promise<RefundResult>; getTransactionStatus(transactionId: string): Promise<TransactionStatus>;} // Multiple implementations fulfill the same contractclass StripeProcessor implements PaymentProcessor { async processPayment(amount: number, currency: string): Promise<PaymentResult> { // Stripe-specific implementation const charge = await this.stripe.charges.create({ amount, currency }); return { success: true, transactionId: charge.id }; } // ... other methods} class PayPalProcessor implements PaymentProcessor { async processPayment(amount: number, currency: string): Promise<PaymentResult> { // PayPal-specific implementation const payment = await this.paypal.createPayment({ amount, currency }); return { success: true, transactionId: payment.id }; } // ... other methods} // Calling code works with ANY implementationclass CheckoutService { constructor(private paymentProcessor: PaymentProcessor) {} async checkout(cart: Cart): Promise<Order> { const result = await this.paymentProcessor.processPayment( cart.total, cart.currency ); // Works with Stripe, PayPal, or any future processor }}This is one of the most important principles in OOP. When you depend on a concrete class, you're locked to that specific implementation. When you depend on an interface, you're free to use any implementation that fulfills the contract. This flexibility is the foundation of extensible design.
Low-Level Design produces specific artifacts that guide implementation. These artifacts are more detailed than HLD diagrams and directly translate to code.
From Diagram to Code
Unlike HLD diagrams that remain somewhat abstract, LLD diagrams should be directly translatable to code:
The diagram should be accurate enough that a developer could implement from it without ambiguity. This tight coupling between diagram and code is what makes LLD 'low-level.'
Low-Level Design serves as the critical bridge between High-Level Design (architecture) and actual code. It translates architectural concepts into implementation blueprints.
| HLD Concept | LLD Translation | Implementation |
|---|---|---|
| Service | Package/Module of classes | UserService/, OrderService/ |
| API Endpoint | Controller class + methods | UserController.createUser() |
| Database Table | Entity class + Repository | User entity, UserRepository |
| Message Queue | Producer/Consumer classes | OrderEventPublisher, OrderEventHandler |
| Cache | Caching decorator or service | CachedUserRepository, CacheService |
| External System | Client/Adapter class | PaymentGatewayAdapter, EmailClient |
The Translation Process
When you receive an HLD and need to produce LLD, you:
LLD is a plan, not a contract. As you implement, you'll discover nuances the design missed. Good LLD is detailed enough to guide implementation but flexible enough to accommodate discoveries. Document deviations and update the design.
We've explored the fundamental building blocks 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 how LLD fits within the overall design process. The next page examines where LLD sits in the software development lifecycle, how it connects to requirements and architecture, and the iterative nature of design work.
You now understand Low-Level Design—the world of classes, objects, methods, and relationships where architectural blueprints become executable code. You know the fundamental building blocks and how they combine to create well-structured software. Next, we'll see how LLD fits into the broader design process.