Bird
Raised Fist0
Interview Prepoop-design-patternshardAmazonGoogleMicrosoftFlipkartSwiggyRazorpay

Design a Library Management System - LLD with Relationships & Edge Cases

Choose your preparation mode3 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Steps
setup

Define the Book class

We start by defining the Book class, which represents a book entity with fields for title, author, ISBN, and copies available.

💡 Defining the Book class first establishes the core entity around which the library system revolves.
Line:class Book: def __init__(self, title, author, isbn): self.title = title self.author = author self.isbn = isbn self.copies = []
💡 The Book class encapsulates book details and manages multiple copies, a key design for handling inventory.
📊
Design a Library Management System - LLD with Relationships & Edge Cases - Watch the Algorithm Execute, Step by Step
Watching the design unfold step-by-step reveals how each class responsibility and relationship is established, making the system's structure clear without reading dense code.
Step 1/11
·Active fillAnswer cell
Encapsulation of book attributes and inventory management.
Book
title: string
author: string
isbn: string
copies: List[BookCopy]
+__init__()
Composition relationship: Book owns multiple BookCopies.
Book
title: string
author: string
isbn: string
copies: List[BookCopy]
+__init__()
BookCopy
copy_id: string
is_available: bool
+__init__()
Book BookCopy (1:0..*)
User class encapsulates identity and role for access control.
Book
title: string
author: string
isbn: string
copies: List[BookCopy]
+__init__()
BookCopy
copy_id: string
is_available: bool
+__init__()
User
user_id: string
name: string
role: string
+__init__()
Book BookCopy (1:0..*)
Loan associates BookCopy and User with temporal data.
Book
title: string
author: string
isbn: string
copies: List[BookCopy]
+__init__()
BookCopy
copy_id: string
is_available: bool
+__init__()
User
user_id: string
name: string
role: string
+__init__()
Loan
book_copy: BookCopy
user: User
loan_date: date
return_date: date|null
+__init__()
Book BookCopy (1:0..*)Loan BookCopy (1:1)Loan User (1:1)
Library aggregates all main entities and manages system state.
Book
title: string
author: string
isbn: string
copies: List[BookCopy]
+__init__()
BookCopy
copy_id: string
is_available: bool
+__init__()
User
user_id: string
name: string
role: string
+__init__()
Loan
book_copy: BookCopy
user: User
loan_date: date
return_date: date|null
+__init__()
Library
books: Dict[string, Book]
users: Dict[string, User]
loans: List[Loan]
+__init__()
Book BookCopy (1:0..*)Loan BookCopy (1:1)Loan User (1:1)Library Book (1:0..*)Library User (1:0..*)Library Loan (1:0..*)
Method handles book insertion and copy management.
Library
books: Dict[string, Book]
users: Dict[string, User]
loans: List[Loan]
+add_book()
Library Book (1:0..*)
Method manages user registration with uniqueness check.
Library
books: Dict[string, Book]
users: Dict[string, User]
loans: List[Loan]
+register_user()
Library User (1:0..*)
Method enforces availability and user validation before loaning.
Library
books: Dict[string, Book]
users: Dict[string, User]
loans: List[Loan]
+loan_book()
Library Book (1:0..*)Library User (1:0..*)Library Loan (1:0..*)
Method updates loan and copy status upon return.
Library
books: Dict[string, Book]
users: Dict[string, User]
loans: List[Loan]
+return_book()
Library Loan (1:0..*)
Edge case checks improve system reliability.
Library
books: Dict[string, Book]
users: Dict[string, User]
loans: List[Loan]
+loan_book()
+return_book()
Complete system design with all classes and relationships.
Book
title: string
author: string
isbn: string
copies: List[BookCopy]
+__init__()
BookCopy
copy_id: string
is_available: bool
+__init__()
User
user_id: string
name: string
role: string
+__init__()
Loan
book_copy: BookCopy
user: User
loan_date: date
return_date: date|null
+__init__()
Library
books: Dict[string, Book]
users: Dict[string, User]
loans: List[Loan]
+add_book()
+register_user()
+loan_book()
+1 more
Book BookCopy (1:0..*)Loan BookCopy (1:1)Loan User (1:1)Library Book (1:0..*)Library User (1:0..*)Library Loan (1:0..*)

Key Takeaways

The design clearly separates entities (Book, BookCopy, User, Loan) and their responsibilities.

This separation is hard to grasp from code alone but is visually clear in the class diagram.

Composition and aggregation relationships model ownership and associations effectively.

Seeing these relationships visually helps understand how objects are linked and managed.

Edge case handling is integrated into methods to ensure robustness without complicating the core design.

Visualizing these checks clarifies how the system avoids invalid operations gracefully.

Practice

(1/5)
1. You need to add multiple optional features to a core object dynamically at runtime without creating a subclass for every possible combination. Which design approach best supports this requirement?
easy
A. Apply memoization to cache feature combinations for reuse
B. Create a large inheritance hierarchy with subclasses for each feature combination
C. Use a greedy algorithm to select features based on priority
D. Use composition to wrap the core object with feature objects that add behavior dynamically

Solution

  1. Step 1: Understand the problem constraints

    The problem requires adding features dynamically without subclass explosion.
  2. Step 2: Identify the design pattern that supports dynamic behavior wrapping

    The Decorator Pattern uses composition to wrap objects and add behavior at runtime, avoiding subclassing for every combination.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Dynamic wrapping via composition avoids subclass explosion [OK]
Hint: Dynamic wrapping avoids subclass explosion [OK]
Common Mistakes:
  • Thinking subclassing is the only way to add features
  • Confusing greedy algorithms with design patterns
2. What is a common trade-off or limitation when strictly enforcing the Liskov Substitution Principle in a large inheritance hierarchy?
medium
A. It can lead to overly rigid designs that prevent useful specialization.
B. It always improves code flexibility and reduces bugs without downsides.
C. It allows subclasses to freely change method signatures for optimization.
D. It eliminates the need for interface segregation.

Solution

  1. Step 1: Understand strict LSP enforcement

    Strict LSP means subclasses cannot alter behavior or method contracts in incompatible ways.
  2. Step 2: Analyze trade-offs

    This can restrict subclass implementations, making designs rigid and limiting specialization.
  3. Step 3: Evaluate options

    It can lead to overly rigid designs that prevent useful specialization. correctly identifies rigidity as a trade-off. It always improves code flexibility and reduces bugs without downsides. is false; strict LSP can reduce flexibility. It allows subclasses to freely change method signatures for optimization. is incorrect; method signatures must respect variance rules. It eliminates the need for interface segregation. is false; interface segregation addresses different concerns.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    Strict LSP can constrain subclass design choices.
Hint: Strict LSP enforces substitutability but can limit subclass flexibility.
Common Mistakes:
  • Believing strict LSP has no downsides
  • Confusing LSP with interface segregation
  • Assuming method signatures can be freely changed
3. What is the time complexity of notifying observers in the thread-safe observer pattern implementation where observers are stored in a dictionary mapping event types to sets of observers, and a snapshot list is created during notification?
medium
A. O(n) where n is total number of all observers across all event types
B. O(k) where k is the number of observers subscribed to the notified event type
C. O(k + n) where k is observers for event type and n is total observers
D. O(1) constant time due to hash set usage

Solution

  1. Step 1: Identify the data structure and notification process

    Observers are stored per event type in sets. Notification locks and copies only the observers for the specific event type.
  2. Step 2: Analyze complexity of notify()

    Notify creates a snapshot list of observers for the event type (size k) and iterates over it, so time is proportional to k, not total n.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Notify cost depends only on observers for that event type [OK]
Hint: Notify complexity depends on observers for event type only [OK]
Common Mistakes:
  • Assuming notify iterates over all observers regardless of event type
4. If the parking lot system needs to support dynamic pricing based on spot location, time, and vehicle type, which design approach best supports this extensibility without modifying existing classes?
hard
A. Use the Strategy pattern to encapsulate different pricing algorithms and assign them dynamically
B. Add pricing logic directly inside the ParkingSpot class with multiple if-else conditions
C. Hardcode pricing rules in the ParkingLot class for all spot and vehicle combinations
D. Create separate subclasses of ParkingSpot for each pricing rule variant

Solution

  1. Step 1: Identify extensibility needs

    Pricing rules vary by multiple factors and may change frequently.
  2. Step 2: Evaluate design options

    Embedding logic in ParkingSpot or ParkingLot leads to rigid, hard-to-maintain code. Subclass explosion is unmanageable.
  3. Step 3: Recognize Strategy pattern benefits

    Strategy encapsulates pricing algorithms, allowing dynamic assignment and easy extension without modifying existing classes.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    Strategy pattern supports flexible, maintainable pricing logic [OK]
Hint: Use Strategy pattern to keep pricing logic flexible and decoupled [OK]
Common Mistakes:
  • Embedding complex logic inside core classes
  • Using inheritance for every pricing variant
5. In a language that supports multiple inheritance with MRO, what happens if two parent classes define a method with the same name, but one parent class inherits from the other (forming a diamond), and the child class overrides that method? How does the MRO affect which method is called when the child class method calls super()?
hard
A. super() follows the MRO linearization, calling the next method in the MRO sequence, which may skip some classes.
B. super() calls the method from the immediate parent class only, ignoring the diamond structure.
C. super() calls all parent methods with the same name in parallel, combining their effects.
D. super() always calls the method from the base class at the top of the diamond first.

Solution

  1. Step 1: Understand super() in multiple inheritance

    super() does not simply call the immediate parent but follows the MRO linearization.
  2. Step 2: MRO linearization

    MRO creates a linear order of classes to avoid ambiguity and duplication, so super() calls the next method in this order.
  3. Step 3: Eliminate incorrect options

    super() calls the method from the immediate parent class only, ignoring the diamond structure is incorrect because super() is not limited to immediate parent. super() calls all parent methods with the same name in parallel, combining their effects is wrong because super() does not call methods in parallel. super() always calls the method from the base class at the top of the diamond first is incorrect because super() does not always start at the base class.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    super() respects MRO linearization, ensuring consistent method calls in diamond inheritance.
Hint: super() = next in MRO chain
Common Mistakes:
  • Thinking super() calls only immediate parent
  • Believing super() calls all parents simultaneously
  • Assuming super() always calls base class first