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
- Happy path scenarios
- Error scenarios
- Edge cases
- 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
- Core entities first: User, Booking, Show
- Main service methods: createBooking, cancelBooking
- Supporting classes: Request/Response DTOs
- Error handling: Basic validation and error responses
- 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
- Requirements: Clear? Assumptions documented?
- Entities: All nouns identified? Relationships clear?
- Architecture: Layered? Separation of concerns?
- Classes: Single responsibility? Proper visibility?
- Patterns: Right pattern for right problem?
- SOLID: Principles followed?
- Extensibility: Easy to add new features?
- Testing: Mockable? Testable design?
- Flows: Key scenarios validated?
- 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