💡 Library acts as the system facade coordinating all operations.
insert
Add method to add books to Library
We add add_book method to Library to insert new books or add copies if the book exists.
💡 This method handles the edge case of multiple copies and avoids duplicate book entries.
Line:def add_book(self, title, author, isbn, num_copies):
if isbn not in self.books:
book = Book(title, author, isbn)
self.books[isbn] = book
else:
book = self.books[isbn]
for i in range(num_copies):
copy_id = f"{isbn}_{len(book.copies)+1}"
book.copies.append(BookCopy(copy_id))
💡 Efficiently manages inventory by reusing existing book objects and adding copies.
insert
Add method to register users
We add register_user method to Library to add new users with roles.
💡 User registration is essential for managing borrow permissions and tracking loans.
Line:def register_user(self, user_id, name, role):
if user_id not in self.users:
self.users[user_id] = User(user_id, name, role)
💡 Ensures unique users and supports role-based access.
insert
Add method to loan a book copy
We add loan_book method to Library to loan an available copy of a book to a user.
💡 This method handles checking availability and creating a Loan record.
Line:def loan_book(self, isbn, user_id, loan_date):
if isbn not in self.books or user_id not in self.users:
return false
book = self.books[isbn]
user = self.users[user_id]
for copy in book.copies:
if copy.is_available:
copy.is_available = false
loan = Loan(copy, user, loan_date)
self.loans.append(loan)
return true
return false
💡 Demonstrates traversal and conditional logic to enforce availability constraints.
insert
Add method to return a book copy
We add return_book method to Library to mark a loaned copy as returned and update the loan record.
💡 Returning books updates availability and loan return date, critical for inventory accuracy.
Line:def return_book(self, copy_id, return_date):
for loan in self.loans:
if loan.book_copy.copy_id == copy_id and loan.return_date is null:
loan.return_date = return_date
loan.book_copy.is_available = true
return true
return false
💡 Shows traversal and state update to handle returns correctly.
prune
Add edge case handling for invalid operations
We add checks in loan_book and return_book to handle invalid ISBNs, user IDs, and copy IDs gracefully.
💡 Edge case handling prevents system crashes and ensures robustness.
Line:if isbn not in self.books or user_id not in self.users:
return false
# In return_book
if loan.book_copy.copy_id != copy_id or loan.return_date is not null:
continue
💡 Robust design anticipates and safely handles invalid or unexpected inputs.
reconstruct
Summary: Final class diagram state
The final design includes Book, BookCopy, User, Loan, and Library classes with their fields, methods, and relationships fully defined.
💡 This final state shows the complete system design ready for implementation.
💡 The design balances encapsulation, composition, and association to model a real-world library system.
class Book: # STEP 1
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self.copies = []
class BookCopy: # STEP 2
def __init__(self, copy_id):
self.copy_id = copy_id
self.is_available = true
class User: # STEP 3
def __init__(self, user_id, name, role):
self.user_id = user_id
self.name = name
self.role = role
class Loan: # STEP 4
def __init__(self, book_copy, user, loan_date):
self.book_copy = book_copy
self.user = user
self.loan_date = loan_date
self.return_date = null
class Library: # STEP 5
def __init__(self):
self.books = {}
self.users = {}
self.loans = []
def add_book(self, title, author, isbn, num_copies): # STEP 6
if isbn not in self.books:
book = Book(title, author, isbn)
self.books[isbn] = book
else:
book = self.books[isbn]
for i in range(num_copies):
copy_id = f"{isbn}_{len(book.copies)+1}"
book.copies.append(BookCopy(copy_id))
def register_user(self, user_id, name, role): # STEP 7
if user_id not in self.users:
self.users[user_id] = User(user_id, name, role)
def loan_book(self, isbn, user_id, loan_date): # STEP 8
if isbn not in self.books or user_id not in self.users:
return false
book = self.books[isbn]
user = self.users[user_id]
for copy in book.copies:
if copy.is_available:
copy.is_available = false
loan = Loan(copy, user, loan_date)
self.loans.append(loan)
return true
return false
def return_book(self, copy_id, return_date): # STEP 9
for loan in self.loans:
if loan.book_copy.copy_id == copy_id and loan.return_date is null:
loan.return_date = return_date
loan.book_copy.is_available = true
return true
return false
# STEP 10: Edge case handling is integrated in loan_book and return_book methods
📊
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 fill★Answer 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
Step 1: Understand the problem constraints
The problem requires adding features dynamically without subclass explosion.
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.
Final Answer:
Option D -> Option D
Quick Check:
Dynamic wrapping via composition avoids subclass explosion [OK]
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
Step 1: Understand strict LSP enforcement
Strict LSP means subclasses cannot alter behavior or method contracts in incompatible ways.
Step 2: Analyze trade-offs
This can restrict subclass implementations, making designs rigid and limiting specialization.
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.
Final Answer:
Option A -> Option A
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
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.
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.
Final Answer:
Option B -> Option B
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
Step 1: Identify extensibility needs
Pricing rules vary by multiple factors and may change frequently.
Step 2: Evaluate design options
Embedding logic in ParkingSpot or ParkingLot leads to rigid, hard-to-maintain code. Subclass explosion is unmanageable.
Step 3: Recognize Strategy pattern benefits
Strategy encapsulates pricing algorithms, allowing dynamic assignment and easy extension without modifying existing classes.
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
Step 1: Understand super() in multiple inheritance
super() does not simply call the immediate parent but follows the MRO linearization.
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.
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.