What is Low-Level Design?
2026-03-22javalldfundamentalsoop
What is Low-Level Design?
Low-Level Design is the practice of structuring code at the class, interface, and method level so that it is readable, extensible, testable, and maintainable. It's the bridge between a vague requirement ("build a parking lot system") and actual working code.
Every line of code you write is an LLD decision. Choosing between inheritance and composition, deciding which class owns which responsibility, figuring out how objects collaborate — all of this is LLD.
graph LR
REQ["Requirements<br/>'Build a parking lot'"]
HLD["High-Level Design<br/>Services, APIs,<br/>databases, queues"]
LLD["Low-Level Design<br/>Classes, interfaces,<br/>methods, patterns"]
CODE["Code<br/>Actual Java<br/>implementation"]
REQ --> HLD --> LLD --> CODE
style REQ fill:#f1f5f9,stroke:#94a3b8
style HLD fill:#dbeafe,stroke:#3b82f6
style LLD fill:#dcfce7,stroke:#22c55e
style CODE fill:#fef3c7,stroke:#f59e0bLLD vs HLD — Understanding the Scope
This is the most common confusion. Both are "design," but they operate at completely different zoom levels.
graph TB
subgraph HLD["HLD: Satellite View"]
direction LR
CLIENT["Client App"]
LB["Load Balancer"]
API["API Server"]
CACHE["Redis Cache"]
DB["PostgreSQL"]
QUEUE["Message Queue"]
WORKER["Worker Service"]
CLIENT --> LB --> API
API --> CACHE
API --> DB
API --> QUEUE
QUEUE --> WORKER
end
subgraph LLD["LLD: Zooming into one component"]
direction TB
CTRL["ParkingController"]
SVC["ParkingService"]
STRAT["ParkingStrategy<br/>«interface»"]
NEAR["NearestFirstStrategy"]
SPREAD["SpreadOutStrategy"]
LOT["ParkingLot"]
FLOOR["ParkingFloor"]
SPOT["ParkingSpot"]
VEHICLE["Vehicle<br/>«abstract»"]
CAR["Car"]
TRUCK["Truck"]
BIKE["Bike"]
CTRL --> SVC
SVC --> STRAT
STRAT -.-> NEAR
STRAT -.-> SPREAD
SVC --> LOT
LOT --> FLOOR
FLOOR --> SPOT
SPOT --> VEHICLE
VEHICLE -.-> CAR
VEHICLE -.-> TRUCK
VEHICLE -.-> BIKE
end
HLD -.->|"Zoom into<br/>API Server"| LLD
style HLD fill:#dbeafe,stroke:#3b82f6
style LLD fill:#dcfce7,stroke:#22c55e| Aspect | HLD | LLD |
|---|---|---|
| Question it answers | What components do we need? How do they communicate? | How do we implement one component internally? |
| Artifacts | Architecture diagrams, service maps, data flow diagrams | Class diagrams, sequence diagrams, code |
| Decisions | Which database? REST or gRPC? Monolith or microservices? | Which design pattern? Inheritance or composition? Who owns this responsibility? |
| Scale | System-wide (multiple services) | Single service or module |
| Interview format | 45 min whiteboard: "Design Instagram" | 45 min coding: "Design a parking lot in Java" |
Why LLD Matters — The Real Cost of Bad Design
Bad LLD isn't just ugly code. It has concrete, measurable consequences:
graph TD
subgraph BAD["Bad LLD"]
B1["New feature request"]
B2["Read 5 classes to<br/>understand the flow"]
B3["Modify 3 existing<br/>classes to add feature"]
B4["Break 2 existing<br/>tests"]
B5["Fix tests, introduce<br/>new bug"]
B6["Hotfix in production"]
B1 --> B2 --> B3 --> B4 --> B5 --> B6
end
subgraph GOOD["Good LLD"]
G1["New feature request"]
G2["Read 1 interface to<br/>understand the contract"]
G3["Create 1 new class<br/>implementing the interface"]
G4["All existing tests<br/>still pass"]
G5["Ship to production"]
G1 --> G2 --> G3 --> G4 --> G5
end
style BAD fill:#fee2e2,stroke:#ef4444
style GOOD fill:#dcfce7,stroke:#22c55eIn interviews, LLD questions test exactly this: can you structure code so that adding a new feature is creating a new class, not modifying ten existing ones?
The Four Pillars of Good LLD
Every good low-level design exhibits four properties. These aren't abstract ideals — they're practical qualities you can test for.
1. Single Responsibility
Each class does one thing. If you can't describe what a class does in one sentence without using "and," it's doing too much.
// BAD: This class does everything
public class OrderService {
public Order createOrder(Cart cart) { /* ... */ }
public void sendConfirmationEmail(Order order) { /* ... */ }
public void updateInventory(Order order) { /* ... */ }
public void processPayment(Order order) { /* ... */ }
public PDF generateInvoice(Order order) { /* ... */ }
}// GOOD: Each class has one job
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
private final NotificationService notificationService;
private final InvoiceService invoiceService;
public Order createOrder(Cart cart) {
Order order = Order.from(cart);
paymentService.process(order);
inventoryService.reserve(order);
notificationService.sendConfirmation(order);
return order;
}
}2. Open for Extension, Closed for Modification
You should be able to add new behavior without changing existing code. This is the backbone of extensible design.
classDiagram
class PaymentProcessor {
<<interface>>
+process(order: Order): PaymentResult
}
class CreditCardProcessor {
+process(order: Order): PaymentResult
}
class UPIProcessor {
+process(order: Order): PaymentResult
}
class WalletProcessor {
+process(order: Order): PaymentResult
}
class NetBankingProcessor {
+process(order: Order): PaymentResult
}
PaymentProcessor <|.. CreditCardProcessor
PaymentProcessor <|.. UPIProcessor
PaymentProcessor <|.. WalletProcessor
PaymentProcessor <|.. NetBankingProcessor : "NEW — no existing code changed"
note for NetBankingProcessor "Adding this required ZERO\nchanges to existing classes"3. Dependency Inversion
High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).
// BAD: High-level class directly depends on low-level implementation
public class NotificationService {
private final SmtpEmailSender emailSender = new SmtpEmailSender();
public void notify(User user, String message) {
emailSender.send(user.getEmail(), message);
}
}// GOOD: Both depend on an abstraction
public interface MessageSender {
void send(String destination, String message);
}
public class SmtpEmailSender implements MessageSender {
public void send(String destination, String message) {
// SMTP implementation
}
}
public class TwilioSmsSender implements MessageSender {
public void send(String destination, String message) {
// Twilio SMS implementation
}
}
public class NotificationService {
private final MessageSender sender;
public NotificationService(MessageSender sender) {
this.sender = sender; // injected — easy to swap, easy to test
}
public void notify(User user, String message) {
sender.send(user.getContactInfo(), message);
}
}4. Composition Over Inheritance
Inheritance creates tight coupling. Composition gives you flexibility to mix and match behaviors.
graph TB
subgraph Inheritance["❌ Inheritance — rigid hierarchy"]
AN["Animal"]
FLY_AN["FlyingAnimal"]
SWIM_AN["SwimmingAnimal"]
DUCK_BAD["Duck extends ???<br/>(it flies AND swims)"]
AN --> FLY_AN
AN --> SWIM_AN
FLY_AN -.->|"Can't extend both!"| DUCK_BAD
SWIM_AN -.->|"Can't extend both!"| DUCK_BAD
end
subgraph Composition["✅ Composition — flexible behaviors"]
ANIMAL["Animal"]
FLY_B["FlyBehavior<br/>«interface»"]
SWIM_B["SwimBehavior<br/>«interface»"]
DUCK["Duck<br/>has FlyBehavior<br/>has SwimBehavior"]
PENGUIN["Penguin<br/>has SwimBehavior<br/>no FlyBehavior"]
ANIMAL --> DUCK
ANIMAL --> PENGUIN
FLY_B -.-> DUCK
SWIM_B -.-> DUCK
SWIM_B -.-> PENGUIN
end
style Inheritance fill:#fee2e2,stroke:#ef4444
style Composition fill:#dcfce7,stroke:#22c55eA Complete Example: Notification System
Let's walk through designing a notification system from scratch — the way you'd do it in an LLD interview.
Requirements
- Send notifications via Email, SMS, and Push
- Support adding new channels without modifying existing code
- Support notification priorities (urgent, normal, low)
- Retry failed notifications
- Log all notification attempts
Step 1: Identify the Core Abstractions
classDiagram
class NotificationChannel {
<<interface>>
+send(notification: Notification): boolean
+supports(type: ChannelType): boolean
}
class EmailChannel {
-smtpClient: SmtpClient
+send(notification: Notification): boolean
+supports(type: ChannelType): boolean
}
class SmsChannel {
-twilioClient: TwilioClient
+send(notification: Notification): boolean
+supports(type: ChannelType): boolean
}
class PushChannel {
-fcmClient: FcmClient
+send(notification: Notification): boolean
+supports(type: ChannelType): boolean
}
class NotificationService {
-channels: List~NotificationChannel~
-retryPolicy: RetryPolicy
-logger: NotificationLogger
+notify(user: User, notification: Notification): void
}
class Notification {
-id: String
-message: String
-priority: Priority
-channelType: ChannelType
-createdAt: Instant
}
class RetryPolicy {
-maxRetries: int
-backoffMs: long
+shouldRetry(attempt: int, error: Exception): boolean
+getDelay(attempt: int): long
}
class NotificationLogger {
+logSuccess(notification: Notification, channel: String): void
+logFailure(notification: Notification, channel: String, error: Exception): void
}
NotificationChannel <|.. EmailChannel
NotificationChannel <|.. SmsChannel
NotificationChannel <|.. PushChannel
NotificationService --> NotificationChannel : uses
NotificationService --> RetryPolicy : uses
NotificationService --> NotificationLogger : uses
NotificationService --> Notification : processesStep 2: Implement the Core Code
public enum Priority { URGENT, NORMAL, LOW }
public enum ChannelType { EMAIL, SMS, PUSH }
public record Notification(
String id,
String message,
Priority priority,
ChannelType channelType,
Instant createdAt
) {
public Notification(String message, Priority priority, ChannelType channelType) {
this(UUID.randomUUID().toString(), message, priority, channelType, Instant.now());
}
}public interface NotificationChannel {
boolean send(Notification notification, User user);
boolean supports(ChannelType type);
}
public class EmailChannel implements NotificationChannel {
private final SmtpClient smtpClient;
public EmailChannel(SmtpClient smtpClient) {
this.smtpClient = smtpClient;
}
@Override
public boolean send(Notification notification, User user) {
return smtpClient.send(user.getEmail(), notification.message());
}
@Override
public boolean supports(ChannelType type) {
return type == ChannelType.EMAIL;
}
}public class NotificationService {
private final List<NotificationChannel> channels;
private final RetryPolicy retryPolicy;
private final NotificationLogger logger;
public NotificationService(
List<NotificationChannel> channels,
RetryPolicy retryPolicy,
NotificationLogger logger) {
this.channels = channels;
this.retryPolicy = retryPolicy;
this.logger = logger;
}
public void notify(User user, Notification notification) {
NotificationChannel channel = channels.stream()
.filter(c -> c.supports(notification.channelType()))
.findFirst()
.orElseThrow(() -> new UnsupportedChannelException(notification.channelType()));
int attempt = 0;
while (true) {
try {
boolean sent = channel.send(notification, user);
if (sent) {
logger.logSuccess(notification, channel.getClass().getSimpleName());
return;
}
} catch (Exception e) {
if (!retryPolicy.shouldRetry(attempt, e)) {
logger.logFailure(notification, channel.getClass().getSimpleName(), e);
throw new NotificationFailedException(notification, e);
}
}
attempt++;
sleep(retryPolicy.getDelay(attempt));
}
}
}Step 3: Trace the Flow
sequenceDiagram
participant Client
participant Service as NotificationService
participant Channel as EmailChannel
participant Retry as RetryPolicy
participant Log as NotificationLogger
participant SMTP as SmtpClient
Client->>Service: notify(user, notification)
Service->>Channel: supports(EMAIL)?
Channel-->>Service: true
Service->>Channel: send(notification, user)
Channel->>SMTP: send(email, message)
SMTP-->>Channel: ❌ ConnectionTimeout
Channel-->>Service: throws Exception
Service->>Retry: shouldRetry(attempt=0, error)?
Retry-->>Service: true (delay: 1000ms)
Note over Service: Wait 1 second
Service->>Channel: send(notification, user) [retry 1]
Channel->>SMTP: send(email, message)
SMTP-->>Channel: ✅ success
Channel-->>Service: true
Service->>Log: logSuccess(notification, "EmailChannel")Why This Design Is Good
| Principle | How It's Applied |
|---|---|
| Single Responsibility | EmailChannel only knows how to send email. RetryPolicy only handles retry logic. NotificationLogger only logs. |
| Open-Closed | Adding WhatsApp? Create WhatsAppChannel implements NotificationChannel. Zero changes to existing classes. |
| Dependency Inversion | NotificationService depends on the NotificationChannel interface, not concrete classes. Swap implementations freely. |
| Composition | NotificationService is composed of channels, a retry policy, and a logger — all injected, all replaceable. |
What You'll Learn in This Series
This series takes you from fundamentals to solving real LLD interview problems. Everything is in Java.
graph LR
C0["Class 0<br/>What is LLD<br/>(you are here)"]
C1["Class 1<br/>SOLID Principles"]
C2["Class 2<br/>UML Class Diagrams"]
C3["Class 3-8<br/>Design Patterns<br/>(Creational, Structural,<br/>Behavioral)"]
C4["Class 9+<br/>LLD Problems<br/>(Parking Lot, Elevator,<br/>BookMyShow, Chess)"]
C0 --> C1 --> C2 --> C3 --> C4
style C0 fill:#3b82f6,stroke:#2563eb,color:#fff
style C4 fill:#22c55e,stroke:#16a34a,color:#fffSOLID Principles (Class 1)
The five rules that form the backbone of every good LLD decision. Each principle explained with a Java "before and after" — the wrong way, why it's wrong, and the fix.
UML Class Diagrams (Class 2)
How to read and draw class diagrams. Association, aggregation, composition, inheritance, dependency — each relationship type with Java code mapping. Interviewers expect you to sketch these.
Design Patterns (Classes 3–8)
Creational (Singleton, Factory, Builder, Prototype), Structural (Adapter, Decorator, Proxy, Facade, Composite), and Behavioral (Strategy, Observer, Command, State, Template Method, Chain of Responsibility). Each pattern with a real-world Java use case and class diagram.
LLD Interview Problems (Classes 9+)
Parking Lot, Elevator System, BookMyShow, Splitwise, Chess, Tic-Tac-Toe, Library Management, Vending Machine — full class-level designs with runnable Java code. The kind of problems you'll face at Amazon, Google, Flipkart, and Razorpay.
How to Approach an LLD Interview
graph TD
subgraph Phase1["Minutes 0-5: Clarify"]
A1["Ask about scope"]
A2["Ask about scale"]
A3["List core use cases"]
end
subgraph Phase2["Minutes 5-15: Core Design"]
B1["Identify key entities"]
B2["Define relationships"]
B3["Draw class diagram"]
end
subgraph Phase3["Minutes 15-35: Implementation"]
C1["Write key interfaces"]
C2["Implement core classes"]
C3["Show design patterns used"]
end
subgraph Phase4["Minutes 35-45: Extend"]
D1["Walk through a use case"]
D2["Show how to add a new feature"]
D3["Discuss trade-offs"]
end
Phase1 --> Phase2 --> Phase3 --> Phase4
style Phase1 fill:#dbeafe,stroke:#3b82f6
style Phase2 fill:#dcfce7,stroke:#22c55e
style Phase3 fill:#fef3c7,stroke:#f59e0b
style Phase4 fill:#f3e8ff,stroke:#a855f7What's Next
In the next class, we'll dive deep into the SOLID Principles — the five rules that form the backbone of every good LLD decision. Each principle will have a Java "before and after" example showing exactly what goes wrong when you violate it and how to fix it.