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.
| Step | Contract Called | Key Data Flow |
|---|---|---|
| 1 | CheckOutBook(patron_A, copy_42) | Returns checkout record. Copy marked "checked out." |
| 2 | PlaceHold(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.) | — |
| 5 | ReturnBook(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.) | — |
| 7 | CheckOutBook(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
- Error cases outnumber happy paths — each contract has more error conditions than return values
- Side effects connect modules — the explicit side effects section shows cross-boundary impacts
- Contracts encode business rules — "can't place a hold if copies are available" is a policy, not a technical limitation
- Input specificity matters — copy_id vs. book_id is not a minor detail; it changes the entire meaning
- Contracts chain through events — one contract's side effect is another contract's trigger