Katkat

Add Hold-Available and Loan Due-Date Notifications to a Library Lending System

InstacartSoftware Engineer · Online Assessment

Last updated

Tests domain modeling, queue management, and idempotent notification triggering for an event- and time-driven library lending workflow.

Problem Statement

Design and implement a notification manager for a digital library lending system. The system manages book copies, patron loans, and hold queues. Your task is to process lending events and periodic time ticks to generate notifications for patrons while enforcing hold priority, loan renewal rules, and duplicate notification suppression.

Requirements

Implement the LibraryNotificationManager class with the following methods:

  1. place_hold(patron_id: str, book_id: str, timestamp: int) -> bool

    • Adds a patron to the hold queue for book_id. A patron can only have one active hold per book. Returns True if hold is successfully placed, False if duplicate.
  2. return_book(copy_id: str, book_id: str, timestamp: int) -> dict | None

    • Marks a returned copy as available. If patrons are waiting in the hold queue for book_id, allocate the copy to the patron with the earliest hold timestamp, transition the hold to READY_FOR_PICKUP (with a pickup window of 48 hours: expires_at = timestamp + 172800), and return a notification object:
      {
          "type": "HOLD_AVAILABLE",
          "patron_id": str,
          "book_id": str,
          "copy_id": str,
          "expires_at": int,
          "timestamp": timestamp
      }
      
    • If no holds exist, the copy remains available in inventory, returning None.
  3. checkout(patron_id: str, copy_id: str, book_id: str, timestamp: int, duration_days: int = 14) -> bool

    • If the patron had this copy reserved via a READY_FOR_PICKUP hold, consume the hold. If the copy is free in general inventory, loan it directly. Creates a loan with due_date = timestamp + (duration_days * 86400). Returns True on success, False if the copy is not available to this patron.
  4. check_due_reminders(current_time: int) -> list[dict]

    • Scans all active loans and returns a list of reminder notifications for:
      • Due Soon: Loans due within the next 24 hours (0 due_date). Each loan should receive at most one OVERDUE reminder per calendar day (every 86,400 seconds) while still unreturned.
    • Notification format:
      {
          "type": "LOAN_DUE_SOON" | "LOAN_OVERDUE",
          "patron_id": str,
          "copy_id": str,
          "due_date": int,
          "timestamp": current_time
      }
      

Constraints

  • All timestamps are Unix epoch timestamps in seconds.
  • Up to 10^4 calls across all methods.
  • A copy can only be checked out to one patron at a time.
  • Reminder queries must be idempotent: calling check_due_reminders twice at the same current_time must not emit duplicate notifications.

Example

manager = LibraryNotificationManager()
manager.place_hold("p1", "book_A", 1000)
manager.place_hold("p2", "book_A", 1050)

# Book returned; p1 gets first hold
notif = manager.return_book("copy_1", "book_A", 2000)
# notif == {
#   "type": "HOLD_AVAILABLE",
#   "patron_id": "p1",
#   "book_id": "book_A",
#   "copy_id": "copy_1",
#   "expires_at": 174800,
#   "timestamp": 2000
# }

# Checkout copy_1 to p1
assert manager.checkout("p1", "copy_1", "book_A", 2500, duration_days=2)
# Loan due at: 2500 + 2 * 86400 = 175300

# 30 hours later (timestamp = 110500), 175300 - 110500 = 64800 (<= 86400, due soon)
reminders = manager.check_due_reminders(110500)
# reminders contains 1 LOAN_DUE_SOON for patron 'p1'

# Calling again at same timestamp produces no duplicate
assert manager.check_due_reminders(110500) == []

Hints

Hint 1

Separate hold queue state (FIFO per book) from hold reservation state (copy assigned to patron with an expiration deadline).

Hint 2

Track notification dispatch history per loan using a state flag or record of the last emitted reminder timestamp to prevent duplicate alerts.

Hint 3

When a hold pickup window expires without a checkout, ensure the copy can be reassigned to the next patron in the hold queue or returned to general availability.

Follow-up questions

  • How would you handle hold cancellations and expired pickups when `check_due_reminders` runs?
  • If millions of active loans exist, how would you optimize finding loans due within 24 hours without an O(N) linear scan?
  • How would you design the data persistence layer to ensure hold allocations and checkouts remain consistent during concurrent patron requests?