Low-Level Design (LLD) :Interview Framework



This content originally appeared on DEV Community and was authored by Dev Cookies

Overview

This framework provides a systematic approach to tackle any LLD interview problem. Follow these steps sequentially to design robust, scalable, and maintainable object-oriented systems.

1. Understanding Requirements and Clarifying Assumptions

The CRUD-F Method

Before writing any code, clarify the following:

Category Questions to Ask
Core What is the primary purpose? Who are the users?
Requirements What are the functional requirements? What’s out of scope?
Users How many users? Different user types?
Data What data needs to be stored? What are the relationships?
Flows What are the main user journeys? What are edge cases?

Example: BookMyShow System

Clarifying Questions:
- Do we need to handle multiple cities/theaters?
- Are we supporting different seat types (VIP, regular)?
- Do we need payment processing or just booking?
- How do we handle concurrent bookings?
- Should we support cancellations and refunds?

Assumption Documentation Template

Assumptions:
✓ System supports single city initially
✓ Payment integration is out of scope
✓ Focus on movie booking, not events
✓ Basic user authentication required
✓ Real-time seat availability needed

2. Identifying Entities, Behaviors, and Relationships

The NBR (Noun-Behavior-Relationship) Analysis

Step 1: Extract Nouns (Entities)

Read the problem statement and identify all nouns:

  • Primary entities: Core business objects (User, Movie, Theater)
  • Supporting entities: Helper objects (Seat, Booking, Payment)
  • System entities: Technical objects (NotificationService, DatabaseManager)

Step 2: Extract Verbs (Behaviors)

Identify actions and behaviors:

  • User actions: search, book, cancel, pay
  • System actions: validate, notify, update, persist

Step 3: Define Relationships

  • Composition: Theater HAS seats (strong relationship)
  • Aggregation: Movie theater SHOWS movies (weak relationship)
  • Inheritance: PremiumSeat IS-A Seat
  • Association: User BOOKS tickets

Example Entity Mapping

BookMyShow Entities:
├── User (name, email, phone)
├── Movie (title, duration, genre, rating)
├── Theater (name, location, screens)
├── Screen (number, capacity, seatMap)
├── Seat (row, column, type, price)
├── Show (movie, screen, time, date)
├── Booking (user, show, seats, status)
└── Payment (booking, amount, method, status)

3. Layered Architecture Design

The 4-Layer Architecture Pattern

┌─────────────────────────────────────┐
│           Interface Layer           │  ← Controllers, APIs, UI
├─────────────────────────────────────┤
│            Service Layer            │  ← Business Logic, Workflows
├─────────────────────────────────────┤
│            Manager Layer            │  ← Data Access, External APIs
├─────────────────────────────────────┤
│            Entity Layer             │  ← Domain Models, DTOs
└─────────────────────────────────────┘

Layer Responsibilities

Layer Purpose Examples
Interface Handle external communication BookingController, MovieAPI
Service Implement business logic BookingService, PaymentService
Manager Manage data and external systems DatabaseManager, NotificationManager
Entity Define domain models User, Movie, Booking

Example Layer Distribution

// Interface Layer
public class BookingController {
    public BookingResponse createBooking(BookingRequest request) { }
}

// Service Layer
public class BookingService {
    public Booking processBooking(User user, Show show, List<Seat> seats) { }
}

// Manager Layer
public class DatabaseManager {
    public void saveBooking(Booking booking) { }
}

// Entity Layer
public class Booking {
    private String bookingId;
    private User user;
    private Show show;
    private List<Seat> seats;
}

4. Class Design Principles

The AMPV Method (Attributes, Methods, Principles, Visibility)

Attributes Design

  • Primitive types: Use for simple data (int, String, boolean)
  • Object types: Use for complex relationships (User, List)
  • Collections: Use appropriate collection types (List, Set, Map)

Method Design

  • Naming: Use verbs for actions (calculatePrice, validateBooking)
  • Parameters: Keep parameter count low (max 3-4)
  • Return types: Be specific about return types
  • Single responsibility: Each method should do one thing

Visibility Guidelines

Visibility When to Use
private Internal implementation details
protected Subclass access needed
public External API, interface methods
package-private Same package access

Example Class Design

public class BookingService {
    private DatabaseManager dbManager;
    private NotificationManager notificationManager;
    private PaymentService paymentService;

    public BookingResult createBooking(BookingRequest request) {
        // Validate input
        if (!validateBookingRequest(request)) {
            return BookingResult.failure("Invalid request");
        }

        // Check seat availability
        if (!areSeatsAvailable(request.getSeats(), request.getShow())) {
            return BookingResult.failure("Seats not available");
        }

        // Process booking
        Booking booking = processBooking(request);
        dbManager.saveBooking(booking);
        notificationManager.sendConfirmation(booking);

        return BookingResult.success(booking);
    }

    private boolean validateBookingRequest(BookingRequest request) { }
    private boolean areSeatsAvailable(List<Seat> seats, Show show) { }
    private Booking processBooking(BookingRequest request) { }
}

5. Interface vs Abstract Class vs Concrete Class

Decision Matrix

Use Case Interface Abstract Class Concrete Class
Multiple inheritance
Common implementation
Contract definition
Instantiation

When to Use What

Interfaces

  • Define contracts for unrelated classes
  • Support multiple inheritance
  • Plugin architecture
interface PaymentProcessor {
    PaymentResult processPayment(PaymentRequest request);
}

class CreditCardProcessor implements PaymentProcessor { }
class PayPalProcessor implements PaymentProcessor { }

Abstract Classes

  • Share common implementation
  • Enforce template methods
  • Related class hierarchies
abstract class Seat {
    protected int row;
    protected int column;
    protected SeatType type;

    public abstract double getPrice();

    public String getSeatNumber() {
        return row + "-" + column;
    }
}

class RegularSeat extends Seat {
    public double getPrice() { return 100.0; }
}

Concrete Classes

  • Fully implemented functionality
  • Can be instantiated
  • Leaf nodes in inheritance hierarchy

6. Applying OOP and SOLID Principles

SOLID Principles in Practice

Single Responsibility Principle (SRP)

// Bad: Multiple responsibilities
class User {
    private String name;
    public void saveToDatabase() { }
    public void sendEmail() { }
    public void generateReport() { }
}

// Good: Single responsibility
class User {
    private String name;
    // Only user-related data and behavior
}

class UserRepository {
    public void saveUser(User user) { }
}

class EmailService {
    public void sendWelcomeEmail(User user) { }
}

Open/Closed Principle (OCP)

// Bad: Modifying existing code
class PriceCalculator {
    public double calculatePrice(Seat seat) {
        if (seat.getType() == SeatType.REGULAR) {
            return 100.0;
        } else if (seat.getType() == SeatType.VIP) {
            return 200.0;
        }
        // Adding new seat type requires modification
    }
}

// Good: Open for extension, closed for modification
interface PricingStrategy {
    double calculatePrice(Seat seat);
}

class RegularSeatPricing implements PricingStrategy {
    public double calculatePrice(Seat seat) { return 100.0; }
}

class VIPSeatPricing implements PricingStrategy {
    public double calculatePrice(Seat seat) { return 200.0; }
}

Liskov Substitution Principle (LSP)

// Good: Subclasses can replace base class
abstract class Seat {
    public abstract boolean isAvailable();
    public abstract double getPrice();
}

class RegularSeat extends Seat {
    public boolean isAvailable() { return !isBooked; }
    public double getPrice() { return 100.0; }
}

// Can use any Seat subclass
public void bookSeat(Seat seat) {
    if (seat.isAvailable()) {
        // Book the seat
    }
}

Interface Segregation Principle (ISP)

// Bad: Fat interface
interface MovieOperations {
    void addMovie();
    void removeMovie();
    void searchMovie();
    void bookTicket();
    void cancelBooking();
}

// Good: Segregated interfaces
interface MovieManagement {
    void addMovie();
    void removeMovie();
}

interface MovieSearch {
    List<Movie> searchMovies(String query);
}

interface BookingOperations {
    Booking bookTicket();
    void cancelBooking();
}

Dependency Inversion Principle (DIP)

// Bad: High-level module depends on low-level module
class BookingService {
    private MySQLDatabase database = new MySQLDatabase();

    public void saveBooking(Booking booking) {
        database.save(booking);
    }
}

// Good: Both depend on abstraction
interface DatabaseManager {
    void save(Booking booking);
}

class BookingService {
    private DatabaseManager dbManager;

    public BookingService(DatabaseManager dbManager) {
        this.dbManager = dbManager;
    }

    public void saveBooking(Booking booking) {
        dbManager.save(booking);
    }
}

7. Design Patterns with Real Use-Case Triggers

Pattern Selection Matrix

Pattern Use Case Trigger Question
Strategy Multiple algorithms “How do we handle different pricing/payment methods?”
Factory Object creation “How do we create different types of users/seats?”
Observer Event notifications “How do we notify multiple systems about bookings?”
Singleton Single instance “Should we have only one instance of this?”
Template Method Algorithm skeleton “Do we have similar workflows with variations?”
Decorator Add functionality “How do we add features without modifying existing code?”

Common Patterns in LLD

Strategy Pattern

// Use when: Different algorithms for same operation
interface PricingStrategy {
    double calculatePrice(Seat seat, Show show);
}

class WeekdayPricing implements PricingStrategy {
    public double calculatePrice(Seat seat, Show show) {
        return seat.getBasePrice() * 0.8; // 20% discount
    }
}

class WeekendPricing implements PricingStrategy {
    public double calculatePrice(Seat seat, Show show) {
        return seat.getBasePrice() * 1.2; // 20% premium
    }
}

class PriceCalculator {
    private PricingStrategy strategy;

    public void setPricingStrategy(PricingStrategy strategy) {
        this.strategy = strategy;
    }

    public double calculatePrice(Seat seat, Show show) {
        return strategy.calculatePrice(seat, show);
    }
}

Factory Pattern

// Use when: Creating different types of objects
interface UserFactory {
    User createUser(UserType type, String name, String email);
}

class SimpleUserFactory implements UserFactory {
    public User createUser(UserType type, String name, String email) {
        switch (type) {
            case REGULAR:
                return new RegularUser(name, email);
            case PREMIUM:
                return new PremiumUser(name, email);
            case ADMIN:
                return new AdminUser(name, email);
            default:
                throw new IllegalArgumentException("Unknown user type");
        }
    }
}

Observer Pattern

// Use when: Multiple objects need to be notified of changes
interface BookingObserver {
    void onBookingCreated(Booking booking);
    void onBookingCancelled(Booking booking);
}

class EmailNotificationService implements BookingObserver {
    public void onBookingCreated(Booking booking) {
        // Send confirmation email
    }

    public void onBookingCancelled(Booking booking) {
        // Send cancellation email
    }
}

class BookingService {
    private List<BookingObserver> observers = new ArrayList<>();

    public void addObserver(BookingObserver observer) {
        observers.add(observer);
    }

    public void createBooking(BookingRequest request) {
        Booking booking = processBooking(request);
        notifyObservers(booking, "CREATED");
    }

    private void notifyObservers(Booking booking, String action) {
        for (BookingObserver observer : observers) {
            if ("CREATED".equals(action)) {
                observer.onBookingCreated(booking);
            }
        }
    }
}

8. Class Diagram Generation with UML

UML Notation Guide

Class Representation

┌─────────────────────┐
│      ClassName      │
├─────────────────────┤
│ - attribute: type   │
│ + attribute: type   │
├─────────────────────┤
│ + method(): type    │
│ - method(): type    │
└─────────────────────┘

Relationship Types

  • Association: ────────>
  • Aggregation: ────────◇
  • Composition: ────────♦
  • Inheritance: ────────△
  • Implementation: ┄┄┄┄┄△

Example Class Diagram

User                    Booking
┌─────────────────┐    ┌─────────────────┐
│ - userId: String│    │ - bookingId: String│
│ - name: String  │    │ - user: User    │
│ - email: String │    │ - show: Show    │
└─────────────────┘    │ - seats: List<Seat>│
         │              │ - status: Status│
         │              └─────────────────┘
         │                       │
         └───────────────────────┘
                1              1..*

Show                    Movie
┌─────────────────┐    ┌─────────────────┐
│ - showId: String│    │ - movieId: String│
│ - movie: Movie  │────│ - title: String │
│ - screen: Screen│    │ - duration: int │
│ - time: DateTime│    │ - genre: String │
└─────────────────┘    └─────────────────┘
         │
         │
         ▼
Screen
┌─────────────────┐
│ - screenId: String│
│ - capacity: int │
│ - seats: List<Seat>│
└─────────────────┘

9. Designing for Extensibility, Testability, and Maintainability

The ETM Framework

Extensibility

  • Use interfaces and abstract classes for future implementations
  • Apply Strategy pattern for algorithm variations
  • Use configuration instead of hard-coded values
  • Design for plugin architecture when appropriate
// Extensible design
interface PaymentProcessor {
    PaymentResult process(PaymentRequest request);
}

class PaymentService {
    private Map<PaymentType, PaymentProcessor> processors;

    public PaymentResult processPayment(PaymentRequest request) {
        PaymentProcessor processor = processors.get(request.getType());
        return processor.process(request);
    }

    // Easy to add new payment methods
    public void addPaymentProcessor(PaymentType type, PaymentProcessor processor) {
        processors.put(type, processor);
    }
}

Testability

  • Use dependency injection for external dependencies
  • Create interfaces for mockable components
  • Keep methods small and focused
  • Avoid static methods and global state
// Testable design
class BookingService {
    private DatabaseManager dbManager;
    private NotificationService notificationService;

    // Constructor injection for testing
    public BookingService(DatabaseManager dbManager, 
                         NotificationService notificationService) {
        this.dbManager = dbManager;
        this.notificationService = notificationService;
    }

    public BookingResult createBooking(BookingRequest request) {
        // Easily mockable dependencies
        if (dbManager.isAvailable(request.getSeats())) {
            Booking booking = new Booking(request);
            dbManager.save(booking);
            notificationService.notify(booking);
            return BookingResult.success(booking);
        }
        return BookingResult.failure("Seats not available");
    }
}

Maintainability

  • Follow naming conventions consistently
  • Keep classes focused (Single Responsibility)
  • Use meaningful variable names
  • Add proper error handling
  • Document complex logic
// Maintainable design
public class SeatAvailabilityChecker {
    private static final int MAX_SEATS_PER_BOOKING = 10;

    public AvailabilityResult checkAvailability(List<Seat> requestedSeats, Show show) {
        if (requestedSeats.isEmpty()) {
            return AvailabilityResult.invalid("No seats requested");
        }

        if (requestedSeats.size() > MAX_SEATS_PER_BOOKING) {
            return AvailabilityResult.invalid("Too many seats requested");
        }

        List<Seat> unavailableSeats = findUnavailableSeats(requestedSeats, show);
        if (!unavailableSeats.isEmpty()) {
            return AvailabilityResult.unavailable(unavailableSeats);
        }

        return AvailabilityResult.available();
    }

    private List<Seat> findUnavailableSeats(List<Seat> seats, Show show) {
        // Implementation details
    }
}

10. Simulating Key Flows to Validate Design

Flow Validation Checklist

Primary Flows

  1. Happy path scenarios
  2. Error scenarios
  3. Edge cases
  4. Concurrent scenarios

Example: Booking Flow Simulation

// Test Case: Successful booking
public void testSuccessfulBooking() {
    // Setup
    User user = new User("John", "john@email.com");
    Show show = new Show(movie, screen, DateTime.now());
    List<Seat> seats = Arrays.asList(seat1, seat2);

    // Execute
    BookingService service = new BookingService(dbManager, notificationService);
    BookingResult result = service.createBooking(new BookingRequest(user, show, seats));

    // Validate
    assertTrue(result.isSuccess());
    assertEquals(BookingStatus.CONFIRMED, result.getBooking().getStatus());
    verify(dbManager).save(any(Booking.class));
    verify(notificationService).notify(any(Booking.class));
}

// Test Case: Concurrent booking
public void testConcurrentBooking() {
    // Two users trying to book same seats simultaneously
    // Should handle race conditions properly
}

Flow Scenarios to Test

Scenario Expected Behavior Validation Points
Happy Path User successfully books available seats Booking created, payment processed, notification sent
Seat Unavailable User tries to book taken seats Error returned, no booking created
Concurrent Booking Two users book same seats Only one succeeds, proper locking
Payment Failure Payment processing fails Booking marked as failed, seats released
System Error Database/service unavailable Graceful error handling, retry mechanism

11. Time-Boxed Coding Best Practices

The 45-Minute Rule

Time Allocation

  • Requirements clarification: 5-7 minutes
  • Design and architecture: 15-20 minutes
  • Core implementation: 15-20 minutes
  • Testing and validation: 3-5 minutes

Coding Strategy

// Step 1: Start with core entities (5 minutes)
class User {
    private String userId;
    private String name;
    private String email;
    // Basic getters/setters
}

class Booking {
    private String bookingId;
    private User user;
    private Show show;
    private List<Seat> seats;
    private BookingStatus status;
    // Basic getters/setters
}

// Step 2: Add key service methods (10 minutes)
class BookingService {
    public BookingResult createBooking(BookingRequest request) {
        // Core logic implementation
    }

    public BookingResult cancelBooking(String bookingId) {
        // Cancellation logic
    }
}

// Step 3: Add supporting classes (5 minutes)
class BookingRequest {
    private User user;
    private Show show;
    private List<Seat> seats;
}

class BookingResult {
    private boolean success;
    private Booking booking;
    private String errorMessage;
}

Prioritization Strategy

  1. Core entities first: User, Booking, Show
  2. Main service methods: createBooking, cancelBooking
  3. Supporting classes: Request/Response DTOs
  4. Error handling: Basic validation and error responses
  5. Design patterns: Only if time permits

12. Final Design Checklist

Pre-Submission Validation

✅ Requirement Compliance

  • [ ] All functional requirements addressed
  • [ ] Assumptions clearly documented
  • [ ] Edge cases identified and handled

✅ Design Quality

  • [ ] SOLID principles applied
  • [ ] Appropriate design patterns used
  • [ ] Clear separation of concerns
  • [ ] Proper abstraction levels

✅ Code Quality

  • [ ] Meaningful class and method names
  • [ ] Consistent naming conventions
  • [ ] Proper visibility modifiers
  • [ ] Appropriate data structures used

✅ Architecture

  • [ ] Layered architecture implemented
  • [ ] Dependencies properly managed
  • [ ] Interfaces used for abstraction
  • [ ] Extensibility considerations

✅ Error Handling

  • [ ] Input validation implemented
  • [ ] Exception handling strategy
  • [ ] Graceful failure modes
  • [ ] Proper error messages

✅ Testing Strategy

  • [ ] Unit testable design
  • [ ] Mock-friendly interfaces
  • [ ] Clear test scenarios identified
  • [ ] Validation points defined

13. Smart Clarifying Questions

The SCALE Framework

Scope Questions

  • “Should we focus on the core booking flow or include advanced features?”
  • “Are we designing for a single theater or multiple theaters?”
  • “Do we need to handle different user types (admin, customer, theater manager)?”

Constraints Questions

  • “What’s the expected scale? (concurrent users, bookings per second)”
  • “Are there any technology constraints or preferences?”
  • “What’s the acceptable response time for booking operations?”

Assumptions Questions

  • “Can we assume users are authenticated when making bookings?”
  • “Should we handle payment processing or just booking?”
  • “Are we supporting mobile and web platforms?”

Limitations Questions

  • “What’s out of scope for this design?”
  • “Are there any features we should explicitly not implement?”
  • “Should we prioritize certain user flows over others?”

Edge Cases Questions

  • “How do we handle concurrent bookings for the same seat?”
  • “What happens if payment fails after seat reservation?”
  • “How do we handle system failures during booking?”

Question Categories by System Type

E-commerce/Booking Systems

  • “Do we need to handle inventory management?”
  • “How do we handle cancellations and refunds?”
  • “Are there different pricing tiers or promotions?”

Social Media/Chat Systems

  • “How do we handle message ordering and delivery?”
  • “Should we support group conversations?”
  • “Do we need to handle media attachments?”

Ride-sharing/Location Systems

  • “How do we handle driver matching algorithms?”
  • “Do we need real-time location tracking?”
  • “Should we support different vehicle types?”

Financial/Payment Systems

  • “How do we ensure transaction consistency?”
  • “Are there regulatory compliance requirements?”
  • “Do we need to handle multiple currencies?”

Quick Reference Card

30-Second Mental Checklist

  1. Requirements: Clear? Assumptions documented?
  2. Entities: All nouns identified? Relationships clear?
  3. Architecture: Layered? Separation of concerns?
  4. Classes: Single responsibility? Proper visibility?
  5. Patterns: Right pattern for right problem?
  6. SOLID: Principles followed?
  7. Extensibility: Easy to add new features?
  8. Testing: Mockable? Testable design?
  9. Flows: Key scenarios validated?
  10. Quality: Clean code? Good naming?

Common Pitfalls to Avoid

  • ❌ Starting to code without clarifying requirements
  • ❌ Creating god classes with too many responsibilities
  • ❌ Ignoring error handling and edge cases
  • ❌ Over-engineering with unnecessary patterns
  • ❌ Not considering concurrent access scenarios
  • ❌ Forgetting to validate the design with key flows
  • ❌ Using inappropriate data structures
  • ❌ Not considering future extensibility

Success Indicators

  • ✅ Interviewer asks follow-up questions about design choices
  • ✅ You can easily explain trade-offs and alternatives
  • ✅ Design can handle new requirements with minimal changes
  • ✅ Code is readable and self-documenting
  • ✅ You can walk through complex scenarios confidently

Remember: The goal is not to create the perfect design, but to demonstrate systematic thinking, solid engineering principles, and the ability to make reasonable trade-offs under time constraints.


This content originally appeared on DEV Community and was authored by Dev Cookies