Contracts — Example: Library Checkout System

The Scenario

A patron visits the library, finds a book, and checks it out. Later, they return it. If it's late, a fine is assessed. They can also place a hold on a book that's currently checked out by someone else.

We'll define the full contract for every operation in the Circulation module.


Contract 1: Check Out a Book

CONTRACT: CheckOutBook

ACCEPTS:
  - patron_id: text — required, must be a valid library card number (format: LIB-XXXXX 
    where X is a digit)
  - copy_id: text — required, must be a valid physical copy ID (format: CPY-XXXXXXX)

RETURNS:
  - checkout_record:
    - checkout_id: text (unique identifier for this checkout)
    - patron_id: text
    - copy_id: text
    - book_title: text (included for convenience — pulled from Catalog)
    - checkout_date: date (YYYY-MM-DD, always today)
    - due_date: date (YYYY-MM-DD, always 14 days from checkout_date)

ERRORS:
  - patron_id not found → error: "Unknown patron" (caller error)
  - patron account is expired → error: "Patron account expired. Renewal required." (caller error)
  - patron account is suspended → error: "Patron account suspended. Contact librarian." (caller error)
  - patron has reached checkout limit (10 books) → error: "Checkout limit reached. 
    Return a book before checking out another." (caller error)
  - patron has unpaid fines over $25 → error: "Outstanding fines exceed limit. 
    Payment required before checkout." (caller error)
  - copy_id not found → error: "Unknown copy" (caller error)
  - copy is not currently available (already checked out) → error: "Copy not available. 
    Currently checked out. Consider placing a hold." (caller error)
  - copy is marked damaged/withdrawn → error: "Copy not available for checkout" (caller error)
  - database unreachable → error: "System temporarily unavailable. Please try again." (system error)

SIDE EFFECTS:
  - Copy status changed from "available" to "checked out" in Catalog
  - Patron's active checkout count incremented
  - Checkout event logged with timestamp, patron_id, copy_id, librarian_id (who processed it)
  - If patron had a hold on this book, the hold is consumed (removed from hold queue)

Why This Level of Detail Matters

Notice the error cases. There are 9 distinct error conditions. A beginner would list 2 or 3 ("book not found, patron not found"). An experienced engineer knows that each of these 9 conditions requires a different response from the caller:

  • "Patron expired" → the librarian can renew them on the spot
  • "Fines exceed limit" → the librarian directs them to payment
  • "Copy not available" → suggest placing a hold (a different operation)
  • "System unavailable" → retry later (completely different from the others)

Each error tells the caller what to do next. That's a good contract.


Contract 2: Return a Book

CONTRACT: ReturnBook

ACCEPTS:
  - copy_id: text — required, must be a valid physical copy ID

    Note: patron_id is NOT required. The system looks up who has this copy checked out.
    This matches real-world behavior — you return a book, not "your checkout record."

RETURNS:
  - return_record:
    - return_id: text
    - checkout_id: text (the original checkout this return closes)
    - patron_id: text (who had it)
    - copy_id: text
    - checkout_date: date
    - due_date: date
    - return_date: date (today)
    - days_overdue: number (0 if on time, positive if late)
    - fine_assessed: currency (0.00 if on time)

ERRORS:
  - copy_id not found → error: "Unknown copy"
  - copy is not currently checked out → error: "This copy is not checked out"
  - database unreachable → error: "System temporarily unavailable"

SIDE EFFECTS:
  - Copy status changed from "checked out" to "available" in Catalog
  - Patron's active checkout count decremented
  - If days_overdue > 0, a fine is created in the Finances module
    (amount = days_overdue × $0.25, capped at replacement cost of book)
  - If there is a hold queue for this book, the next patron in the queue is notified
    (via Communication module)
  - Return event logged with timestamp, copy_id, condition notes (if any)

Key Design Decisions in This Contract

The return contract accepts copy_id, not patron_id. This is a deliberate design choice that matches the physical reality: a librarian scans the book, not the patron's card. The system figures out who had it. This reduces errors (the patron doesn't need their card to return).

The fine is a side effect, not a return value. The return operation calculates the fine and includes it in the return record (for display), but the actual fine creation is a side effect handled by the Finances module. Return doesn't need to know how fines are stored or managed.

Hold notification is a cascading side effect. Returning a book might mean someone else is waiting for it. The contract documents this so that whoever implements it knows they must check the hold queue.


Contract 3: Place a Hold

CONTRACT: PlaceHold

ACCEPTS:
  - patron_id: text — required, valid library card number
  - book_id: text — required, valid book ID (not copy_id — the patron wants
    the book, not a specific physical copy)

RETURNS:
  - hold_record:
    - hold_id: text
    - patron_id: text
    - book_id: text
    - book_title: text
    - hold_date: date (today)
    - queue_position: number (1 = you're next, 2 = one person ahead of you, etc.)
    - estimated_availability: text ("approximately 2 weeks" based on due dates
      of current checkouts and queue length)

ERRORS:
  - patron_id not found → error: "Unknown patron"
  - patron account expired/suspended → error: "Account not active"
  - book_id not found → error: "Unknown book"
  - patron already has a hold on this book → error: "Hold already exists for this book"
  - patron currently has this book checked out → error: "You currently have this book.
    Return it instead of placing a hold."
  - patron has reached hold limit (5 holds) → error: "Hold limit reached"
  - all copies of this book are available (no need for a hold) → error: "Copies are
    available now. No hold needed — check it out directly."
  - database unreachable → error: "System temporarily unavailable"

SIDE EFFECTS:
  - Hold is added to the queue for this book
  - Hold event logged

What Makes This Contract Interesting

Book vs. Copy distinction. When checking out, you specify a copy (a physical item). When placing a hold, you specify a book (the title). The system decides which copy to assign when one becomes available. This distinction matters because it's a different level of abstraction — and the contract makes it explicit.

"Copies are available" is an error. You can place a hold on a book that has copies available — but this contract treats it as an error because the correct action is to check it out, not hold it. This is a business rule baked into the contract. A different library might allow it. The contract forces the decision to be explicit.

Estimated availability is a best guess. The contract says "approximately" — this sets expectations. The caller knows not to treat this as a guarantee.


Contract 4: Cancel a Hold

CONTRACT: CancelHold

ACCEPTS:
  - hold_id: text — required

RETURNS:
  - confirmation:
    - hold_id: text
    - status: "cancelled"
    - cancelled_date: date

ERRORS:
  - hold_id not found → error: "Unknown hold"
  - hold has already been fulfilled (book was checked out) → error: "Hold already
    fulfilled. Book was checked out on [date]."
  - hold was already cancelled → error: "Hold was already cancelled on [date]"
  - database unreachable → error: "System temporarily unavailable"

SIDE EFFECTS:
  - Hold removed from queue
  - All patrons behind this one in the queue move up one position
  - If the book has an available copy and there's a next-in-line patron,
    that patron is notified
  - Cancellation event logged

How These Contracts Work Together

Let's trace a complete scenario:

Patron A checks out the last copy of "Dune." Patron B wants it and places a hold. Patron A returns it late.

StepContract CalledKey Data Flow
1CheckOutBook(patron_A, copy_42)Returns checkout record. Copy marked "checked out."
2PlaceHold(patron_B, book_dune)Returns hold record. Queue position = 1. Estimated availability = "approximately 2 weeks."
3(14 days pass. Patron A doesn't return the book.)
4(Day 17. Patron A returns the book.)
5ReturnBook(copy_42)Returns: days_overdue = 3, fine_assessed = $0.75. Side effects: (a) Fine created in Finances. (b) Hold queue checked — Patron B is next. (c) Communication module notifies Patron B: "Your hold is ready."
6(Patron B receives notification and comes to the library.)
7CheckOutBook(patron_B, copy_42)Returns checkout record. Side effect: Patron B's hold is consumed (removed from queue).

Notice how the contracts chain together through side effects. ReturnBook doesn't call PlaceHold or Communication directly — but its side effects trigger actions in other modules. The contracts document this so that the chain is visible and predictable.


Summary: What This Example Teaches

  1. Error cases outnumber happy paths — each contract has more error conditions than return values
  2. Side effects connect modules — the explicit side effects section shows cross-boundary impacts
  3. Contracts encode business rules — "can't place a hold if copies are available" is a policy, not a technical limitation
  4. Input specificity matters — copy_id vs. book_id is not a minor detail; it changes the entire meaning
  5. Contracts chain through events — one contract's side effect is another contract's trigger