Design Library Management
A library has a long oak desk at the front, a card catalog along one wall, and miles of numbered shelves behind it. The librarian behind the desk is not a wizard. She is a person with three pieces of paper — the list of books and how many copies the building owns, the list of members and what each one is currently holding, and the date stamp she pushes into the back of every book she lends out. Every borrow, every return, every overdue notice flows through those three pieces of paper. Pick the right shape for each one and the librarian can run the whole building from that desk. Pick the wrong shape and she ends up walking the stacks every five minutes to figure out where a book went.

Melvil Dewey opened the first formal library school at Columbia in 1887 and the thing he was teaching was not poetry. It was bookkeeping. Before Dewey, a small library lost track of who had what and a large one was unusable. His circulation system was three card files — one for books with a pocket in the back, one for borrowers, and one for each loan with a due date stamped on it. When IBM rolled out the 357 Data Collection System in 1961 and the first online library catalogs went live at Ohio State in 1971, they did not invent a new model. They put Dewey's three card files into a database. The shapes that survived a century of paper are the same shapes the code is going to use.
Start with what a single copy of a book is doing right now.
#[derive(Debug, Clone, PartialEq, Eq)]
enum CopyState {
OnShelf,
LentTo { member: usize, days_left: i32 },
}
struct Book {
title: &'static str,
author: &'static str,
copies: Vec<CopyState>,
}
struct Member {
name: &'static str,
held: usize,
}
struct Library {
books: Vec<Book>,
members: Vec<Member>,
loan_days: i32,
borrow_limit: usize,
day: i32,
}A CopyState is exactly one of two things — sitting on the shelf, or lent to a specific member with a specific number of days left on the loan. Using an enum here instead of a bool plus a separate "due date" field is the first design choice and it pays back twice. The compiler will refuse to let any code read days_left when the copy is on the shelf because the shelf variant does not carry that field. And the two facts a librarian always wants together — who has the book and when is it due — are welded into one value so the illegal combination of "lent but with no due date" cannot exist.
A Book is the title, the author, and a Vec<CopyState> because the library owns two copies of Dune and one copy of 1984. A single shared is_lent flag would have erased the difference between the copies. The vector lets the librarian say "Aarit has the first copy of Dune, the second is still on the shelf." A Member is a name plus a counter for how many books they currently hold. The counter lives on the member instead of being recomputed by walking every book on every borrow because the borrow check happens many more times than a return and the counter makes it free.
The Library itself is the desk. It owns the book list, the member list, the loan length in days, the borrow limit per member, and the current day. Time is an integer that only moves when the librarian advances it. No clock, no sleep, no real calendar — just a number she increments at the end of each day. That choice is what makes the whole program testable in one breath.

The desk has three actions and the signatures matter more than the bodies.
impl Library {
fn new() -> Self {
Self {
books: vec![
Book { title: "Dune", author: "Herbert", copies: vec![CopyState::OnShelf, CopyState::OnShelf] },
Book { title: "1984", author: "Orwell", copies: vec![CopyState::OnShelf] },
Book { title: "Foundation", author: "Asimov", copies: vec![CopyState::OnShelf] },
Book { title: "Hobbit", author: "Tolkien", copies: vec![CopyState::OnShelf, CopyState::OnShelf] },
],
members: vec![
Member { name: "Aarit", held: 0 },
Member { name: "Bea", held: 0 },
Member { name: "Cyrus", held: 0 },
],
loan_days: 3,
borrow_limit: 3,
day: 0,
}
}
fn borrow(&mut self, m: usize, title: &str) -> Result<(), &'static str> {
if self.members[m].held >= self.borrow_limit {
return Err("borrow limit reached");
}
let book = self.books.iter_mut().find(|b| b.title == title).ok_or("no such title")?;
let slot = book.copies.iter().position(|c| matches!(c, CopyState::OnShelf));
match slot {
Some(i) => {
book.copies[i] = CopyState::LentTo { member: m, days_left: self.loan_days };
self.members[m].held += 1;
Ok(())
}
None => Err("no copy on shelf"),
}
}
fn return_book(&mut self, m: usize, title: &str) -> Result<(), &'static str> {
let book = self.books.iter_mut().find(|b| b.title == title).ok_or("no such title")?;
let slot = book.copies.iter().position(|c| matches!(c, CopyState::LentTo { member, .. } if *member == m));
match slot {
Some(i) => {
book.copies[i] = CopyState::OnShelf;
self.members[m].held -= 1;
Ok(())
}
None => Err("member did not hold a copy"),
}
}
fn tick(&mut self) -> Vec<String> {
self.day += 1;
let mut alerts = Vec::new();
for book in &mut self.books {
for copy in &mut book.copies {
if let CopyState::LentTo { member, days_left } = copy {
*days_left -= 1;
if *days_left == -1 {
alerts.push(format!("OVERDUE: {} held by {}", book.title, self.members[*member].name));
}
}
}
}
alerts
}
}Read borrow first. It takes &mut self because lending changes the library. It takes a member index and a title. It returns Result<(), &'static str> because a borrow can fail for three honest reasons — the title does not exist, the member is already at the limit, or every copy of that title is out — and the type forces the caller to look at the answer. A version that returned a bool would silently throw away the reason. A version that panicked would crash the whole circulation desk because one member tried to take a fourth book. Result is the only safe answer, and the &'static str payload is the cheapest way to name the failure without allocating.
The body of borrow is a checklist run in a fixed order. Check the limit first because it is the cheapest test and it rejects without touching the book list. Find the book. Find a copy that is on the shelf. Only after every check passes does the librarian stamp the card and flip the copy from OnShelf to LentTo. The order matters — every guard runs before any state changes, so a rejected borrow leaves the library exactly as it was. The same idea a database transaction uses. Either the whole borrow happens or none of it does.
return_book is the mirror. Find the book, find the copy that this specific member is holding, flip it back to OnShelf, decrement the held counter. If the member did not actually hold a copy the function returns an error instead of guessing. Cyrus cannot return a book Bea is reading.
tick is the part that makes the system feel alive without using a real clock. Each call advances the day by one and walks every lent copy, subtracting one day from the time remaining. The exact moment a copy's days_left crosses from 0 to -1, the librarian pushes an alert onto the return list. The function hands those alerts back to the caller so the desk can decide whether to email the member, charge a fine, or just print them. The library does not know about email and that is the point. It produces facts. Other code decides what to do with them.

The render helper paints the whole desk in one pass so a reader can see the state move on every event.
fn show(lib: &Library) {
println!(" day {}", lib.day);
for book in &lib.books {
print!(" {:11} by {:8} [", book.title, book.author);
for (i, copy) in book.copies.iter().enumerate() {
if i > 0 { print!(" "); }
match copy {
CopyState::OnShelf => print!("shelf"),
CopyState::LentTo { member, days_left } => {
print!("{}({})", lib.members[*member].name, days_left);
}
}
}
println!("]");
}
print!(" held: ");
for (i, m) in lib.members.iter().enumerate() {
if i > 0 { print!(", "); }
print!("{}={}", m.name, m.held);
}
println!();
}
fn fmt(r: Result<(), &'static str>) -> String {
match r {
Ok(()) => "ok".to_string(),
Err(e) => format!("err: {e}"),
}
}The format is a row per book with the shelf or the member's name and remaining days in parentheses, then a line at the bottom with the held count for every member. No fancy graphics. The point is that every change to the library has to show up somewhere on this picture, and if it does not the design is hiding state.
Now drive the design through a scripted day and watch every output.
fn main() {
let mut lib = Library::new();
let aarit = 0;
let bea = 1;
let cyrus = 2;
println!("--- opening day ---");
show(&lib);
println!();
let r = lib.borrow(aarit, "Dune");
println!("Aarit borrows Dune: {}", fmt(r));
show(&lib); println!();
let r = lib.borrow(bea, "Dune");
println!("Bea borrows Dune: {}", fmt(r));
show(&lib); println!();
let r = lib.borrow(cyrus, "Dune");
println!("Cyrus borrows Dune (no copy left): {}", fmt(r));
show(&lib); println!();
let r = lib.borrow(aarit, "Hobbit");
println!("Aarit borrows Hobbit: {}", fmt(r));
let r = lib.borrow(aarit, "1984");
println!("Aarit borrows 1984: {}", fmt(r));
let r = lib.borrow(aarit, "Foundation");
println!("Aarit borrows Foundation (over limit): {}", fmt(r));
show(&lib); println!();
let r = lib.return_book(cyrus, "Dune");
println!("Cyrus returns Dune (never had one): {}", fmt(r));
show(&lib); println!();
for _ in 0..4 {
let alerts = lib.tick();
println!("tick -> day {}", lib.day);
for a in &alerts {
println!(" {a}");
}
show(&lib);
println!();
}
let r = lib.return_book(aarit, "Dune");
println!("Aarit returns Dune: {}", fmt(r));
show(&lib);
}The first three borrows hit Dune one after another. The first two succeed because Dune has two copies. The third — Cyrus — gets no copy on shelf because both copies are now in members' hands. Then Aarit grabs three more titles, hits the borrow limit on the fourth, and the limit check refuses Foundation before the book list is even searched. Cyrus tries to return a book he never borrowed and the desk rejects it because the copy's LentTo variant does not name him. After that the librarian advances the day four times. The first three ticks just count down. The fourth tick crosses zero and the desk announces every overdue copy in one batch, naming the title and the member who is now late. Finally Aarit returns Dune and the first copy flips back to shelf while Bea's overdue copy keeps counting down.
--- opening day ---
day 0
Dune by Herbert [shelf shelf]
1984 by Orwell [shelf]
Foundation by Asimov [shelf]
Hobbit by Tolkien [shelf shelf]
held: Aarit=0, Bea=0, Cyrus=0
Aarit borrows Dune: ok
day 0
Dune by Herbert [Aarit(3) shelf]
1984 by Orwell [shelf]
Foundation by Asimov [shelf]
Hobbit by Tolkien [shelf shelf]
held: Aarit=1, Bea=0, Cyrus=0
Bea borrows Dune: ok
day 0
Dune by Herbert [Aarit(3) Bea(3)]
1984 by Orwell [shelf]
Foundation by Asimov [shelf]
Hobbit by Tolkien [shelf shelf]
held: Aarit=1, Bea=1, Cyrus=0
Cyrus borrows Dune (no copy left): err: no copy on shelf
day 0
Dune by Herbert [Aarit(3) Bea(3)]
1984 by Orwell [shelf]
Foundation by Asimov [shelf]
Hobbit by Tolkien [shelf shelf]
held: Aarit=1, Bea=1, Cyrus=0
Aarit borrows Hobbit: ok
Aarit borrows 1984: ok
Aarit borrows Foundation (over limit): err: borrow limit reached
day 0
Dune by Herbert [Aarit(3) Bea(3)]
1984 by Orwell [Aarit(3)]
Foundation by Asimov [shelf]
Hobbit by Tolkien [Aarit(3) shelf]
held: Aarit=3, Bea=1, Cyrus=0
Cyrus returns Dune (never had one): err: member did not hold a copy
day 0
Dune by Herbert [Aarit(3) Bea(3)]
1984 by Orwell [Aarit(3)]
Foundation by Asimov [shelf]
Hobbit by Tolkien [Aarit(3) shelf]
held: Aarit=3, Bea=1, Cyrus=0
tick -> day 1
day 1
Dune by Herbert [Aarit(2) Bea(2)]
1984 by Orwell [Aarit(2)]
Foundation by Asimov [shelf]
Hobbit by Tolkien [Aarit(2) shelf]
held: Aarit=3, Bea=1, Cyrus=0
tick -> day 2
day 2
Dune by Herbert [Aarit(1) Bea(1)]
1984 by Orwell [Aarit(1)]
Foundation by Asimov [shelf]
Hobbit by Tolkien [Aarit(1) shelf]
held: Aarit=3, Bea=1, Cyrus=0
tick -> day 3
day 3
Dune by Herbert [Aarit(0) Bea(0)]
1984 by Orwell [Aarit(0)]
Foundation by Asimov [shelf]
Hobbit by Tolkien [Aarit(0) shelf]
held: Aarit=3, Bea=1, Cyrus=0
tick -> day 4
OVERDUE: Dune held by Aarit
OVERDUE: Dune held by Bea
OVERDUE: 1984 held by Aarit
OVERDUE: Hobbit held by Aarit
day 4
Dune by Herbert [Aarit(-1) Bea(-1)]
1984 by Orwell [Aarit(-1)]
Foundation by Asimov [shelf]
Hobbit by Tolkien [Aarit(-1) shelf]
held: Aarit=3, Bea=1, Cyrus=0
Aarit returns Dune: ok
day 4
Dune by Herbert [shelf Bea(-1)]
1984 by Orwell [Aarit(-1)]
Foundation by Asimov [shelf]
Hobbit by Tolkien [Aarit(-1) shelf]
held: Aarit=2, Bea=1, Cyrus=0Read the output from the top. The opening day shows every book at full count and every member holding zero. The day stays at 0 through all the borrows and returns because the librarian only advances time when she calls tick. The "no copy on shelf" line is the moment to watch — Dune now reads [Aarit(3) Bea(3)] and the next borrow attempt bounces, but the rest of the library is untouched. Same with the over-limit attempt. Aarit holds held=3 and Foundation never moves off the shelf. When tick runs four times in a row the numbers in parentheses drop from 3 to -1 and the overdue alerts only print on the day a copy crosses the line, not every day after.
One question worth asking — why does the overdue alert print only once per copy, on the day it goes overdue, instead of every tick after that? Look at the check inside tick. The alert fires when *days_left == -1, which is only true the single tick that crossed from 0 to negative. The next tick takes it to -2 and the condition is false. That is intentional. An alert that fires every day forever would flood the member's inbox and train them to ignore it. One clean alert when the copy first goes overdue, and the rest of the policy — when to send the second notice, when to apply a fine — lives in the layer above the library that consumes these alerts.
The thing this design cannot do on its own is hold a reservation. A member who walks up to the desk and finds every copy of Dune is out has no way to say "tell me when one comes back." Adding a holds queue to each book is the obvious next step, and as soon as the queue exists the return logic has to check it before flipping the copy back to OnShelf — which is the next bottleneck, and the reason real circulation software has more code for holds than for borrows.