Loading content...
We've defined what Low-Level Design is and how it bridges requirements to code. Now we must understand precisely what falls within LLD's scope. What elements must you design? What decisions must you make? What can you leave to implementation?
This page provides an exhaustive exploration of LLD's four fundamental elements: classes, interfaces, methods, and interactions. By the end, you'll have a comprehensive mental model of what constitutes a complete Low-Level Design.
By the end of this page, you will understand the full scope of Low-Level Design—every element that must be considered, every decision that must be made, and how these elements combine to form a coherent, implementable design.
Classes are the fundamental units of object-oriented design. In LLD, we don't just identify classes—we design them completely. This means specifying:
1.1 Class Identity and Purpose
Every class needs a clear, unambiguous identity:
| Stereotype | Purpose | Characteristics | Examples |
|---|---|---|---|
| Entity | Domain object with identity | Has unique ID, mutable, lifecycle | User, Order, Product |
| Value Object | Immutable data carrier | No ID, equality by attributes, immutable | Money, Address, DateRange |
| Service | Stateless business logic | No state, operations on entities | PaymentService, EmailService |
| Repository | Data access abstraction | CRUD operations, hides storage | UserRepository, OrderRepository |
| Factory | Object creation logic | Encapsulates construction complexity | OrderFactory, ConnectionFactory |
| Controller | Request/response handling | Thin, delegates to services | OrderController, AuthController |
| DTO | Data transfer across boundaries | Simple structure, no behavior | OrderRequest, UserResponse |
1.2 Attributes (State)
Attributes define what data a class holds. LLD specifies:
12345678910111213141516171819202122232425262728293031323334
// Comprehensive attribute specificationclass Order { // Identity - immutable after creation private readonly id: OrderId; // References - associations to other entities private readonly customerId: CustomerId; private shippingAddress: Address; // mutable - can change before shipping // Collections - order contains items private readonly items: OrderItem[] = []; // State tracking - evolves over lifecycle private status: OrderStatus = OrderStatus.CREATED; private statusHistory: StatusTransition[] = []; // Timestamps - audit trail private readonly createdAt: Date; private updatedAt: Date; private shippedAt?: Date; // optional - only set when shipped // Calculated values - derived from state get totalAmount(): Money { return this.items.reduce( (sum, item) => sum.add(item.subtotal), Money.zero(this.currency) ); } // Constraints expressed through types and validation // OrderId, CustomerId, Money, Address are all strongly typed // Optional fields use ?: syntax // Readonly fields cannot change after construction}1.3 Class Relationships
Classes rarely exist in isolation. LLD must specify how classes relate:
| Relationship | Description | UML Notation | Code Pattern |
|---|---|---|---|
| Association | Objects know each other | Solid line | Customer has reference to Orders |
| Dependency | One uses another temporarily | Dashed arrow → | Method parameter, local variable |
| Aggregation | Whole-part, independent lifecycle | Hollow diamond ◇ | Team contains Players (players exist independently) |
| Composition | Whole-part, dependent lifecycle | Filled diamond ◆ | Order contains OrderItems (items die with order) |
| Inheritance | IS-A relationship | Hollow triangle △ | SavingsAccount extends BankAccount |
| Realization | Implements interface | Dashed triangle △ | StripeGateway implements IPaymentGateway |
Interfaces are perhaps the most important element in LLD. They define contracts—what operations are available, what guarantees are made, what behaviors can be expected. Well-designed interfaces enable:
If classes are nouns, interfaces are verbs. They define what can be done, not how it's done. A well-designed interface should be stable—implementations change, but the contract remains.
2.1 Interface Design Principles
Effective interfaces follow several key principles:
IPayable, ISearchable, ICacheable123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// BAD: Fat interface - not all implementations need all methodsinterface IUserOperations { createUser(data: UserData): User; updateUser(id: string, data: Partial<UserData>): User; deleteUser(id: string): void; findById(id: string): User | null; findByEmail(email: string): User | null; searchUsers(query: string): User[]; bulkImport(users: UserData[]): ImportResult; exportToCSV(): string; sendPasswordReset(email: string): void; validateCredentials(email: string, password: string): boolean;} // GOOD: Segregated interfaces - each focused on one capabilityinterface IUserRepository { findById(id: string): User | null; findByEmail(email: string): User | null; save(user: User): void; delete(id: string): void;} interface IUserSearch { search(query: SearchQuery): PagedResult<User>; suggest(prefix: string): User[];} interface IUserAuthentication { validateCredentials(email: string, password: string): AuthResult; generateResetToken(email: string): ResetToken; resetPassword(token: ResetToken, newPassword: Password): void;} interface IUserDataExport { exportToCSV(criteria: ExportCriteria): Stream<string>; exportToJSON(criteria: ExportCriteria): Stream<object>;} // Now clients depend only on what they needclass LoginService { constructor(private auth: IUserAuthentication) {} // Only depends on authentication, not search or export} class UserSearchController { constructor(private search: IUserSearch) {} // Only depends on search, not authentication or repository}2.2 Common Interface Categories
LLD frequently defines interfaces in these categories:
| Category | Purpose | Examples |
|---|---|---|
| Repository | Data access abstraction | IUserRepository, IOrderRepository |
| Service | Business operation contracts | IPaymentService, INotificationService |
| Gateway | External system integration | IPaymentGateway, IEmailGateway |
| Factory | Object creation contracts | IConnectionFactory, IMessageFactory |
| Strategy | Interchangeable algorithms | IPricingStrategy, IRoutingStrategy |
| Observer | Event notification | IOrderListener, IPaymentObserver |
| Validator | Validation rules | IOrderValidator, IInputValidator |
Methods are where behavior lives. In LLD, we design methods with precision—not their internal implementation, but their contract with callers.
3.1 Method Signature Components
A complete method specification includes:
processPayment, validateOrder, notifyCustomer1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
interface IOrderService { /** * Places a new order for a customer. * * @param customerId - The customer placing the order (must exist) * @param items - Items to order (must be non-empty, all available) * @param shippingAddress - Delivery destination (must be valid) * * @returns OrderResult - Success with Order, or Failure with reason * * @precondition Customer exists and is active * @precondition All items are available in inventory * @precondition Shipping address is serviceable * * @postcondition Order is persisted with PENDING status * @postcondition Inventory is reserved for items * @postcondition OrderPlaced event is published * * @throws never - All errors returned in OrderResult */ placeOrder( customerId: CustomerId, items: OrderItem[], shippingAddress: Address ): Promise<OrderResult>; /** * Cancels an existing order. * * @param orderId - The order to cancel * @param reason - Cancellation reason for records * * @precondition Order exists * @precondition Order is in cancellable state (PENDING, CONFIRMED) * * @postcondition Order status is CANCELLED * @postcondition Inventory reservations are released * @postcondition Customer is notified * @postcondition Refund is initiated if payment was made */ cancelOrder( orderId: OrderId, reason: CancellationReason ): Promise<CancellationResult>;} // Result types make success/failure explicittype OrderResult = | { success: true; order: Order } | { success: false; error: OrderError }; type OrderError = | { code: 'CUSTOMER_NOT_FOUND'; customerId: string } | { code: 'ITEMS_UNAVAILABLE'; unavailable: OrderItem[] } | { code: 'INVALID_ADDRESS'; address: Address; reason: string } | { code: 'CREDIT_LIMIT_EXCEEDED'; limit: Money; requested: Money };3.2 Method Categories
Methods fall into distinct categories with different design considerations:
| Category | Purpose | Characteristics | Examples |
|---|---|---|---|
| Command | Modifies state | Returns void or result, has side effects | save(), delete(), processPayment() |
| Query | Retrieves data | Returns data, no side effects | findById(), getTotal(), isValid() |
| Factory | Creates objects | Returns new instance | create(), buildFromRequest() |
| Validator | Checks conditions | Returns boolean or result | validate(), canProcess() |
| Transformer | Converts data | Pure function, no side effects | toDTO(), fromJSON(), format() |
| Lifecycle | Manages state transitions | Changes object state | initialize(), dispose(), activate() |
A powerful principle: methods should either change state (commands) or return data (queries), but not both. This makes reasoning about code easier—you know queries are safe to call repeatedly, while commands have effects to consider.
Individual classes and methods are meaningless without understanding how they work together. LLD must specify object interactions—the choreography of method calls that implement features.
4.1 Sequence of Operations
For each use case, LLD describes the sequence of method calls:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
/** * Use Case: Place Order * * Participants: * - OrderController (entry point) * - OrderValidator (business rules) * - InventoryService (stock management) * - PaymentService (payment processing) * - OrderRepository (persistence) * - NotificationService (customer communication) * - EventPublisher (system events) * * Sequence: * 1. OrderController receives PlaceOrderRequest * 2. OrderController calls OrderValidator.validate(request) * - If invalid: return validation errors, END * 3. OrderController calls InventoryService.reserve(items) * - If insufficient: return availability errors, END * 4. OrderController creates Order domain object * 5. OrderController calls PaymentService.authorize(order, paymentDetails) * - If declined: release inventory, return payment error, END * 6. OrderController calls OrderRepository.save(order) * 7. OrderController calls NotificationService.sendConfirmation(order) * 8. OrderController calls EventPublisher.publish(OrderPlacedEvent) * 9. Return success with Order details * * Error Handling: * - Any exception after step 3: release inventory reservation * - Any exception after step 5: void payment authorization * - All operations are logged for audit */ class OrderController { constructor( private validator: IOrderValidator, private inventory: IInventoryService, private payment: IPaymentService, private repository: IOrderRepository, private notifications: INotificationService, private events: IEventPublisher ) {} async placeOrder(request: PlaceOrderRequest): Promise<PlaceOrderResponse> { // Step 2: Validate const validation = await this.validator.validate(request); if (!validation.isValid) { return PlaceOrderResponse.validationFailed(validation.errors); } // Step 3: Reserve inventory const reservation = await this.inventory.reserve(request.items); if (!reservation.success) { return PlaceOrderResponse.insufficientInventory(reservation.unavailable); } try { // Step 4: Create order const order = Order.create(request); // Step 5: Authorize payment const authorization = await this.payment.authorize( order.totalAmount, request.paymentDetails ); if (!authorization.success) { await this.inventory.release(reservation.id); return PlaceOrderResponse.paymentFailed(authorization.error); } try { // Step 6: Persist await this.repository.save(order); // Step 7: Notify (async, best-effort) this.notifications.sendConfirmation(order).catch(this.logError); // Step 8: Publish event await this.events.publish(new OrderPlacedEvent(order)); // Step 9: Success return PlaceOrderResponse.success(order); } catch (error) { // Rollback payment await this.payment.void(authorization.id); throw error; } } catch (error) { // Rollback inventory await this.inventory.release(reservation.id); throw error; } }}4.2 Interaction Patterns
LLD leverages common interaction patterns:
| Pattern | Description | When to Use |
|---|---|---|
| Request-Response | Caller waits for result | Synchronous operations, queries |
| Fire-and-Forget | Caller doesn't wait | Notifications, logging, async tasks |
| Publish-Subscribe | Publishers emit events, subscribers react | Loose coupling, multiple consumers |
| Chain of Responsibility | Request passes through handlers | Validation chains, middleware |
| Saga/Orchestration | Coordinator manages multi-step process | Distributed transactions, workflows |
| Observer | Objects notify interested parties of changes | UI updates, cache invalidation |
Sequence diagrams are invaluable for documenting interactions. They show objects as lifelines and method calls as arrows, making the choreography visible. For complex use cases, always include a sequence diagram in your LLD.
Knowing what not to design is as important as knowing what to design. Including too much detail makes LLD rigid and wastes effort.
Ask: 'If two developers made different decisions here, would it cause integration problems?' If yes, it belongs in LLD. If no, leave it to implementation.
A complete LLD brings together all four elements—classes, interfaces, methods, and interactions—into a coherent specification. Here's what comprehensive LLD documentation looks like:
Not every component needs exhaustive documentation. Match detail to risk and complexity. Critical payment processing needs more rigor than a simple CRUD admin panel.
What's next:
We've covered what LLD includes. The final page of this module explores LLD as a thinking discipline—not just documentation, but a mindset that shapes how you approach software design.
You now have a comprehensive understanding of LLD's scope—classes, interfaces, methods, and interactions. These four elements form the complete picture of what Low-Level Design encompasses. Next, we'll explore LLD as a thinking discipline.