Loading learning content...
Two areas of our Library Management System require especially careful design: fine calculation and reservation management. Both involve time-based logic, multiple business rules, and edge cases that can trip up naive implementations.
Why These Deserve Special Attention:
In this page, we'll design these subsystems with the detail and rigor they demand.
By the end of this page, you will be able to: • Design a robust fine calculation system with multiple rate tiers • Handle fine caps, waivers, and partial payments • Implement a fair reservation queue with proper FIFO ordering • Manage hold shelf logic with expiration and reassignment • Handle edge cases like concurrent reservations and race conditions
Fine calculation seems simple: days_overdue × daily_rate. But real-world requirements add complexity:
Business Requirements for Fines:
Let's design a FineCalculator service that handles all these requirements.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
// ============================================// Fine Configuration (Strategy Pattern)// ============================================ /** * Interface for fine calculation policies. * Different policies can implement tiered rates, caps, etc. */interface FinePolicy { /** * Calculates fine for given overdue days * @param daysOverdue Number of days past due date * @param memberPolicy The member's membership policy * @returns Calculated fine amount */ calculateFine( daysOverdue: number, memberPolicy: MembershipPolicy ): number; /** * Gets the maximum fine per item */ getMaxFinePerItem(): number; /** * Gets grace period in days (first N days free) */ getGracePeriodDays(): number;} /** * Standard fine policy with tiered rates */class StandardFinePolicy implements FinePolicy { private readonly maxFinePerItem: number = 25.00; private readonly gracePeriodDays: number = 1; calculateFine( daysOverdue: number, memberPolicy: MembershipPolicy ): number { // Apply grace period const chargeableDays = Math.max(0, daysOverdue - this.gracePeriodDays); if (chargeableDays === 0) { return 0; } const baseRate = memberPolicy.getDailyFineRate(); let totalFine = 0; // Tiered calculation: // Days 1-7: base rate // Days 8-14: 1.5x base rate // Days 15+: 2x base rate const tier1Days = Math.min(chargeableDays, 7); const tier2Days = Math.min(Math.max(chargeableDays - 7, 0), 7); const tier3Days = Math.max(chargeableDays - 14, 0); totalFine += tier1Days * baseRate; totalFine += tier2Days * (baseRate * 1.5); totalFine += tier3Days * (baseRate * 2.0); // Apply cap return Math.min(totalFine, this.maxFinePerItem); } getMaxFinePerItem(): number { return this.maxFinePerItem; } getGracePeriodDays(): number { return this.gracePeriodDays; }} /** * Holiday-aware fine policy * Excludes library-closed days from fine calculation */class HolidayAwareFinePolicy implements FinePolicy { private readonly basePolicy: FinePolicy; private readonly holidayCalendar: HolidayCalendar; constructor( basePolicy: FinePolicy, holidayCalendar: HolidayCalendar ) { this.basePolicy = basePolicy; this.holidayCalendar = holidayCalendar; } calculateFine( daysOverdue: number, memberPolicy: MembershipPolicy ): number { // This is a simplification - real implementation would // check specific date range for holidays const holidaysInPeriod = this.holidayCalendar .getHolidaysInLastNDays(daysOverdue); const adjustedDays = Math.max(0, daysOverdue - holidaysInPeriod); return this.basePolicy.calculateFine(adjustedDays, memberPolicy); } getMaxFinePerItem(): number { return this.basePolicy.getMaxFinePerItem(); } getGracePeriodDays(): number { return this.basePolicy.getGracePeriodDays(); }}Notice how HolidayAwareFinePolicy wraps another FinePolicy. This is the Decorator Pattern—it adds holiday awareness to any base policy without modifying it. You could stack decorators: new HolidayAwareFinePolicy(new PromotionalDiscountPolicy(new StandardFinePolicy())).
While we could track fines as a simple number on Member, creating a Fine entity provides better audit trails, payment tracking, and dispute resolution support.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
/** * Fine status tracking lifecycle */enum FineStatus { PENDING = 'PENDING', // Fine assessed, not paid PARTIALLY_PAID = 'PARTIALLY_PAID', PAID = 'PAID', // Fully settled WAIVED = 'WAIVED', // Forgiven by librarian DISPUTED = 'DISPUTED' // Under review} /** * Represents a monetary penalty for overdue items. * Maintains full audit trail of payments and adjustments. */class Fine { private readonly id: string; private readonly member: Member; private readonly loan: Loan; private readonly assessedAt: Date; private readonly originalAmount: number; private currentBalance: number; private status: FineStatus; private readonly payments: FinePayment[]; private waivedBy: Librarian | null = null; private waivedAt: Date | null = null; private waiverReason: string | null = null; constructor( id: string, member: Member, loan: Loan, amount: number ) { if (amount <= 0) { throw new InvalidFineAmountError('Fine amount must be positive'); } this.id = id; this.member = member; this.loan = loan; this.originalAmount = amount; this.currentBalance = amount; this.assessedAt = new Date(); this.status = FineStatus.PENDING; this.payments = []; } // ============ Payment Processing ============ /** * Applies a payment to this fine * @returns remaining balance after payment */ applyPayment(payment: FinePayment): number { if (this.status === FineStatus.PAID) { throw new FineAlreadyPaidError(); } if (this.status === FineStatus.WAIVED) { throw new FineWaivedError(); } const amount = payment.getAmount(); if (amount > this.currentBalance) { throw new PaymentExceedsBalanceError( `Payment $${amount} exceeds balance $${this.currentBalance}` ); } this.currentBalance -= amount; this.payments.push(payment); if (this.currentBalance === 0) { this.status = FineStatus.PAID; } else { this.status = FineStatus.PARTIALLY_PAID; } return this.currentBalance; } /** * Waives part or all of the remaining balance */ waive(librarian: Librarian, reason: string, amount?: number): void { if (!librarian.canWaiveFines()) { throw new UnauthorizedOperationError( 'Librarian not authorized to waive fines' ); } if (this.status === FineStatus.PAID) { throw new FineAlreadyPaidError( 'Cannot waive already paid fine' ); } const waiveAmount = amount ?? this.currentBalance; if (waiveAmount > this.currentBalance) { throw new InvalidWaiverAmountError( 'Waiver amount exceeds balance' ); } this.currentBalance -= waiveAmount; this.waivedBy = librarian; this.waivedAt = new Date(); this.waiverReason = reason; if (this.currentBalance === 0) { this.status = FineStatus.WAIVED; } } /** * Marks fine as disputed for review */ dispute(reason: string): void { if (this.status === FineStatus.PAID || this.status === FineStatus.WAIVED) { throw new InvalidOperationError( 'Cannot dispute settled fine' ); } this.status = FineStatus.DISPUTED; // Create dispute record for tracking } // ============ Query Methods ============ isPending(): boolean { return this.status === FineStatus.PENDING || this.status === FineStatus.PARTIALLY_PAID; } getTotalPaid(): number { return this.payments.reduce( (sum, p) => sum + p.getAmount(), 0 ); } getPaymentHistory(): ReadonlyArray<FinePayment> { return [...this.payments]; } // ============ Getters ============ getId(): string { return this.id; } getMember(): Member { return this.member; } getLoan(): Loan { return this.loan; } getOriginalAmount(): number { return this.originalAmount; } getCurrentBalance(): number { return this.currentBalance; } getStatus(): FineStatus { return this.status; } getAssessedAt(): Date { return new Date(this.assessedAt); }} /** * Records a payment against a fine */class FinePayment { private readonly id: string; private readonly amount: number; private readonly paidAt: Date; private readonly paymentMethod: PaymentMethod; private readonly transactionReference: string; constructor( id: string, amount: number, paymentMethod: PaymentMethod, transactionReference: string ) { this.id = id; this.amount = amount; this.paidAt = new Date(); this.paymentMethod = paymentMethod; this.transactionReference = transactionReference; } getAmount(): number { return this.amount; } getPaidAt(): Date { return new Date(this.paidAt); } getPaymentMethod(): PaymentMethod { return this.paymentMethod; }} enum PaymentMethod { CASH = 'CASH', CARD = 'CARD', ONLINE = 'ONLINE'}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
/** * Service for managing fine calculations and processing */class FineService { private readonly finePolicy: FinePolicy; private readonly fineRepository: FineRepository; private readonly memberRepository: MemberRepository; private readonly eventBus: LibraryEventBus; private readonly SUSPENSION_THRESHOLD = 25.00; constructor( finePolicy: FinePolicy, fineRepository: FineRepository, memberRepository: MemberRepository, eventBus: LibraryEventBus ) { this.finePolicy = finePolicy; this.fineRepository = fineRepository; this.memberRepository = memberRepository; this.eventBus = eventBus; } /** * Assesses a fine for a returned overdue loan * @returns The created Fine, or null if no fine due */ async assessFine(loan: Loan): Promise<Fine | null> { const daysOverdue = loan.getOverdueDays(); if (daysOverdue <= 0) { return null; // Not overdue } const member = loan.getMember(); const memberPolicy = MembershipPolicyFactory.getPolicy( member.getMembershipType() ); const amount = this.finePolicy.calculateFine( daysOverdue, memberPolicy ); if (amount === 0) { return null; // Within grace period } // Create and save fine const fine = new Fine( this.generateFineId(), member, loan, amount ); await this.fineRepository.save(fine); // Update member's total fines member.addFine(amount); await this.memberRepository.save(member); // Check suspension threshold if (member.getTotalFinesOwed() >= this.SUSPENSION_THRESHOLD) { member.suspend('Excessive unpaid fines'); await this.memberRepository.save(member); this.eventBus.publish(new MemberSuspendedEvent( member, 'Fines exceeded threshold' )); } // Publish fine assessed event this.eventBus.publish(new FineAssessedEvent(fine)); return fine; } /** * Processes a payment for a member's fines * Applies to oldest fines first (FIFO) */ async processPayment( memberId: string, amount: number, paymentMethod: PaymentMethod, transactionRef: string ): Promise<PaymentResult> { const member = await this.memberRepository.findById(memberId); if (!member) { throw new MemberNotFoundError(memberId); } // Get unpaid fines, ordered by assessment date const unpaidFines = await this.fineRepository .findUnpaidByMember(memberId, { orderBy: 'assessedAt' }); let remainingPayment = amount; const paidFines: Fine[] = []; for (const fine of unpaidFines) { if (remainingPayment <= 0) break; const paymentForThisFine = Math.min( remainingPayment, fine.getCurrentBalance() ); const payment = new FinePayment( this.generatePaymentId(), paymentForThisFine, paymentMethod, transactionRef ); fine.applyPayment(payment); await this.fineRepository.save(fine); paidFines.push(fine); remainingPayment -= paymentForThisFine; } // Update member's total fines const actualPaid = amount - remainingPayment; member.payFine(actualPaid); await this.memberRepository.save(member); // If member was suspended and fines cleared, reactivate if (member.getTotalFinesOwed() < this.SUSPENSION_THRESHOLD && member.getStatus() === MemberStatus.SUSPENDED) { member.reactivate(); await this.memberRepository.save(member); } return { amountPaid: actualPaid, remainingBalance: member.getTotalFinesOwed(), finesAffected: paidFines.length, overpayment: remainingPayment > 0 ? remainingPayment : 0 }; } /** * Gets fine breakdown for a member */ async getMemberFineReport(memberId: string): Promise<FineReport> { const unpaidFines = await this.fineRepository .findUnpaidByMember(memberId); return { totalOwed: unpaidFines.reduce( (sum, f) => sum + f.getCurrentBalance(), 0 ), fineCount: unpaidFines.length, oldestFine: unpaidFines[0]?.getAssessedAt() ?? null, fines: unpaidFines.map(f => ({ id: f.getId(), book: f.getLoan().getBookCopy().getBook().getTitle(), originalAmount: f.getOriginalAmount(), currentBalance: f.getCurrentBalance(), assessedAt: f.getAssessedAt() })) }; } private generateFineId(): string { return `FINE-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } private generatePaymentId(): string { return `PAY-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; }} interface PaymentResult { amountPaid: number; remainingBalance: number; finesAffected: number; overpayment: number;} interface FineReport { totalOwed: number; fineCount: number; oldestFine: Date | null; fines: Array<{ id: string; book: string; originalAmount: number; currentBalance: number; assessedAt: Date; }>;}Reservations form a queue for each book title. The design must handle:
Let's design a robust ReservationService that coordinates these behaviors.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
/** * Manages the complete reservation lifecycle */class ReservationService { private readonly reservationRepository: ReservationRepository; private readonly bookRepository: BookRepository; private readonly memberRepository: MemberRepository; private readonly eventBus: LibraryEventBus; private readonly HOLD_PERIOD_DAYS = 7; private readonly RESERVATION_EXPIRY_DAYS = 90; // Max wait time constructor( reservationRepository: ReservationRepository, bookRepository: BookRepository, memberRepository: MemberRepository, eventBus: LibraryEventBus ) { this.reservationRepository = reservationRepository; this.bookRepository = bookRepository; this.memberRepository = memberRepository; this.eventBus = eventBus; } // ============ Creating Reservations ============ /** * Creates a new reservation for a book * Validates member eligibility and prevents duplicates */ async createReservation( memberId: string, bookId: string ): Promise<ReservationResult> { // Load entities const member = await this.memberRepository.findById(memberId); if (!member) { throw new MemberNotFoundError(memberId); } const book = await this.bookRepository.findById(bookId); if (!book) { throw new BookNotFoundError(bookId); } // Validate member can make reservations this.validateMemberCanReserve(member); // Check for existing reservation by same member const existingReservation = await this.reservationRepository .findActiveByMemberAndBook(memberId, bookId); if (existingReservation) { throw new DuplicateReservationError( 'You already have an active reservation for this book' ); } // Create reservation const reservation = new Reservation( this.generateReservationId(), member, book ); // Add to book's queue and member's reservations book.addReservation(reservation); member.addReservation(reservation); await this.reservationRepository.save(reservation); await this.bookRepository.save(book); await this.memberRepository.save(member); // Calculate queue position const queuePosition = reservation.getQueuePosition(); // Publish event this.eventBus.publish(new ReservationCreatedEvent(reservation)); return { success: true, reservationId: reservation.getId(), queuePosition, estimatedWaitDays: this.estimateWaitTime(book, queuePosition) }; } /** * Validates that member can create a reservation */ private validateMemberCanReserve(member: Member): void { if (member.getStatus() !== MemberStatus.ACTIVE) { throw new MemberNotActiveError( 'Only active members can make reservations' ); } const maxReservations = member.getMaxReservations(); if (member.getReservations().length >= maxReservations) { throw new ReservationLimitReachedError( `Maximum reservations (${maxReservations}) reached` ); } } /** * Estimates wait time based on average loan duration */ private estimateWaitTime(book: Book, queuePosition: number): number { // Simplified: assume average loan duration of 21 days // Position 1 = next available, position 2 = after 21 days, etc. const AVERAGE_LOAN_DAYS = 21; return (queuePosition - 1) * AVERAGE_LOAN_DAYS; } // ============ Processing Returns ============ /** * Called when a book is returned. * Processes the reservation queue and notifies next member. */ async processReturn(bookCopy: BookCopy): Promise<HoldResult | null> { const book = bookCopy.getBook(); // Get next pending reservation const pendingReservation = await this.reservationRepository .findNextPendingForBook(book.getId()); if (!pendingReservation) { // No reservations - book goes back to shelf bookCopy.checkIn(false); return null; } // Mark reservation as ready pendingReservation.markReady(); // Place copy on hold shelf bookCopy.checkIn(true); // hasReservation = true bookCopy.placeOnHold(); // Calculate hold expiry const holdExpiresAt = this.calculateHoldExpiry(); pendingReservation.setExpiresAt(holdExpiresAt); await this.reservationRepository.save(pendingReservation); await this.bookRepository.save(book); // Notify member this.eventBus.publish(new ReservationReadyEvent(pendingReservation)); return { reservation: pendingReservation, holdShelfLocation: this.getHoldShelfLocation(bookCopy), expiresAt: holdExpiresAt, memberToNotify: pendingReservation.getMember() }; } private calculateHoldExpiry(): Date { const expiry = new Date(); expiry.setDate(expiry.getDate() + this.HOLD_PERIOD_DAYS); return expiry; } private getHoldShelfLocation(copy: BookCopy): string { // Typically organized by member last name const member = this.getCurrentHoldMember(copy); if (member) { const lastName = member.getName().split(' ').pop() || 'Unknown'; return `Hold Shelf - Section ${lastName.charAt(0).toUpperCase()}`; } return 'Hold Shelf'; } // ============ Hold Expiration ============ /** * Processes expired holds. * Called by a scheduled job (e.g., daily at midnight). */ async processExpiredHolds(): Promise<ExpirationResult> { const expiredReservations = await this.reservationRepository .findExpiredReady(); const results: ProcessedExpiration[] = []; for (const reservation of expiredReservations) { try { const result = await this.expireReservation(reservation); results.push(result); } catch (error) { // Log error but continue processing others console.error( `Error expiring reservation ${reservation.getId()}:`, error ); } } return { expired: results.length, reassigned: results.filter(r => r.reassignedTo !== null).length, details: results }; } /** * Expires a single reservation and possibly reassigns to next member */ private async expireReservation( reservation: Reservation ): Promise<ProcessedExpiration> { const book = reservation.getBook(); const previousMember = reservation.getMember(); // Expire this reservation reservation.expire(); previousMember.removeReservation(reservation); await this.reservationRepository.save(reservation); await this.memberRepository.save(previousMember); // Notify member of expiration this.eventBus.publish(new ReservationExpiredEvent(reservation)); // Find next reservation in queue const nextReservation = await this.reservationRepository .findNextPendingForBook(book.getId()); if (nextReservation) { // Reassign the hold nextReservation.markReady(); nextReservation.setExpiresAt(this.calculateHoldExpiry()); await this.reservationRepository.save(nextReservation); this.eventBus.publish(new ReservationReadyEvent(nextReservation)); return { expiredReservationId: reservation.getId(), previousMember: previousMember.getName(), reassignedTo: nextReservation.getMember().getName() }; } // No more reservations - release book to general circulation const copy = this.findHeldCopy(book); if (copy) { copy.releaseHold(); await this.bookRepository.save(book); } return { expiredReservationId: reservation.getId(), previousMember: previousMember.getName(), reassignedTo: null }; } // ============ Cancellation ============ /** * Cancels a reservation at member's request */ async cancelReservation( memberId: string, reservationId: string ): Promise<void> { const reservation = await this.reservationRepository .findById(reservationId); if (!reservation) { throw new ReservationNotFoundError(reservationId); } // Verify ownership if (reservation.getMember().getId() !== memberId) { throw new UnauthorizedOperationError( 'Cannot cancel another member\'s reservation' ); } // Can't cancel fulfilled reservations if (reservation.getStatus() === ReservationStatus.FULFILLED) { throw new InvalidOperationError( 'Cannot cancel fulfilled reservation' ); } const wasReady = reservation.getStatus() === ReservationStatus.READY; reservation.cancel(); reservation.getMember().removeReservation(reservation); await this.reservationRepository.save(reservation); await this.memberRepository.save(reservation.getMember()); // If this was a ready reservation, reassign to next if (wasReady) { const book = reservation.getBook(); const nextReservation = await this.reservationRepository .findNextPendingForBook(book.getId()); if (nextReservation) { nextReservation.markReady(); nextReservation.setExpiresAt(this.calculateHoldExpiry()); await this.reservationRepository.save(nextReservation); this.eventBus.publish( new ReservationReadyEvent(nextReservation) ); } else { // Release the held copy const copy = this.findHeldCopy(book); if (copy) { copy.releaseHold(); await this.bookRepository.save(book); } } } this.eventBus.publish(new ReservationCancelledEvent(reservation)); } private findHeldCopy(book: Book): BookCopy | undefined { return book.getCopies().find( c => c.getStatus() === BookCopyStatus.RESERVED ); } private generateReservationId(): string { return `RES-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; }}Real-world systems encounter edge cases that naive designs overlook. Let's address several critical scenarios:
| Edge Case | Problem | Solution |
|---|---|---|
| Concurrent reservations | Two members reserve simultaneously | Optimistic locking on queue position; retry on conflict |
| Member borrows own reservation | Member walks in when reserved copy is ready | Match member to reservation, fulfill directly |
| All copies lost | Last copy reported lost while reservations pending | Cancel all reservations with special notification |
| Member suspended while waiting | Member can't borrow when reservation ready | Hold expires normally; email explains situation |
| Fine threshold during hold | Member accrues fines during hold period | Allow collection but block new borrows |
| Renewal with reservations | Member tries to renew when others waiting | Deny renewal; inform of queue |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
// ============================================// Edge Case: Member Borrows Their Own Reservation// ============================================ class LibraryService { async borrowBook( memberId: string, copyBarcode: string, librarianId: string ): Promise<BorrowResult> { const book = await this.bookRepository.findByBarcode(copyBarcode); const copy = book?.getCopyByBarcode(copyBarcode); const member = await this.memberRepository.findById(memberId); if (!book || !copy || !member) { throw new NotFoundError(); } // Check if copy is reserved if (copy.getStatus() === BookCopyStatus.RESERVED) { // Check if THIS member has the reservation const memberReservation = await this.reservationRepository .findReadyByMemberAndBook(memberId, book.getId()); if (memberReservation) { // Fulfill the reservation - special path return this.fulfillReservation( member, copy, memberReservation, librarianId ); } // Reserved for someone else throw new BookReservedError( 'This book is being held for another member' ); } // Normal checkout flow... } private async fulfillReservation( member: Member, copy: BookCopy, reservation: Reservation, librarianId: string ): Promise<BorrowResult> { const librarian = await this.librarianRepository.findById(librarianId); // Create loan const loan = new Loan( this.generateLoanId(), member, copy, librarian! ); // Fulfill reservation (sets status, links to loan) reservation.fulfill(loan); // Update copy status from RESERVED to LOANED copy.checkOut(loan); // Update member member.addLoan(loan); member.removeReservation(reservation); await Promise.all([ this.loanRepository.save(loan), this.reservationRepository.save(reservation), this.bookRepository.save(copy.getBook()), this.memberRepository.save(member) ]); return { success: true, loan, wasReservation: true }; }} // ============================================// Edge Case: All Copies Lost// ============================================ class BookService { async reportCopyLost( copyId: string, loanId: string ): Promise<void> { const book = await this.bookRepository.findByCopyId(copyId); const copy = book?.getCopyById(copyId); if (!book || !copy) { throw new NotFoundError(); } copy.reportLost(); // Check if all copies are now unavailable const availableCopies = book.getCopies().filter( c => c.getStatus() !== BookCopyStatus.LOST && c.getStatus() !== BookCopyStatus.DAMAGED ); if (availableCopies.length === 0) { // No copies available - cancel all pending reservations await this.cancelAllReservations(book, 'AllCopiesLost'); } await this.bookRepository.save(book); } private async cancelAllReservations( book: Book, reason: string ): Promise<void> { const pendingReservations = await this.reservationRepository .findAllPendingForBook(book.getId()); for (const reservation of pendingReservations) { reservation.cancel(); reservation.getMember().removeReservation(reservation); await this.reservationRepository.save(reservation); await this.memberRepository.save(reservation.getMember()); // Special notification explaining the situation this.eventBus.publish(new ReservationCancelledEvent( reservation, `All copies of "${book.getTitle()}" are currently unavailable. ` + `Your reservation has been cancelled. We apologize for the inconvenience.` )); } }} // ============================================// Concurrency: Optimistic Locking// ============================================ /** * Reservation with version field for optimistic locking */class Reservation { private version: number = 0; // Incremented on each save // Repository checks version on save getVersion(): number { return this.version; } incrementVersion(): void { this.version++; }} class ReservationRepository { async save(reservation: Reservation): Promise<void> { const currentVersion = reservation.getVersion(); // Attempt update with version check const result = await this.db.updateOne({ id: reservation.getId(), version: currentVersion // Only update if version matches }, { ...reservation.toDTO(), version: currentVersion + 1 }); if (result.matchedCount === 0) { // Version mismatch - someone else modified throw new OptimisticLockException( 'Reservation was modified by another process' ); } reservation.incrementVersion(); }} // Usage with retryasync function createReservationWithRetry( memberId: string, bookId: string, maxRetries: number = 3): Promise<ReservationResult> { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await reservationService.createReservation( memberId, bookId ); } catch (error) { if (error instanceof OptimisticLockException && attempt < maxRetries) { // Wait briefly and retry await delay(100 * attempt); continue; } throw error; } } throw new Error('Max retries exceeded');}Interviewers often ask: "What happens if two people try to reserve simultaneously?" Mention optimistic locking and retry strategies. You don't need to implement distributed locks—just show awareness that concurrent access needs handling. This demonstrates senior-level thinking.
We've designed two critical subsystems with production-level detail:
What's Next:
With all major subsystems designed, the final page provides a complete design walkthrough—bringing together all entities, patterns, and services into a coherent whole. We'll present:
You now have deep expertise in fine calculation and reservation queue management. These subsystems demonstrate handling time-based logic, monetary calculations, queue semantics, and concurrency—skills that apply broadly beyond library systems. Next, we'll synthesize everything into a complete design walkthrough.