# Library System

Course: [Low-Level Design](https://www.devtutorials.ai/learn/system-design/lld) · Section: LLD Case Studies
Source: https://www.devtutorials.ai/learn/system-design/lld/case-studies/library-system

> Design a library: copies, loans with due days and fines, a three-book limit and a fair waiting list, built against hidden tests.

QuickBite has a small office library with cookery and technology books. The library currently uses a notebook to record who borrowed each book, when it is due, and who is waiting for it.

Managing these details by hand can lead to missing records, incorrect due dates, and confusion about whose turn is next.

In this case study, you will design a simple library management system for borrowing and returning books, tracking due dates, calculating fines, and maintaining a fair waiting list.

**In simple words:** you will replace the notebook with three small classes. Each class will have a clear responsibility, and automated tests will check whether the library rules work correctly.

## What You Will Learn

In this case study, you will:

1. Tell a book title apart from its copies.
2. Record each loan with its due day and fine.
3. Check borrowing rules in a clear order.
4. Keep a fair, first-come waiting list.
5. Build the library in the sandbox and pass the hidden tests.

Library, hotel and movie-rental questions are all the same shape: **things**, **people**, and a **record** of who has what until when. Spot that shape and the classes follow.

## Step 1: Agree on the Requirements

Before designing classes or writing code, clearly define what the system must do.

In a system design interview, explain these requirements to the interviewer and check that the scope is correct. This helps avoid building features that are not required.

### What the System Must Do

The library system must support the following operations:

- `addBook(isbn, title, copies)` adds a book and its available copies to the library.
- `borrow(member, isbn, day)` lends an available copy to a member. The copy is due 14 days after it is borrowed.
- A member can borrow no more than three books at a time.
- A member cannot borrow two copies of the same book.
- `giveBack(member, isbn, day)` records the return and calculates a fine of 10 for each day the book is late.
- `reserve(member, isbn)` adds a member to the waiting list when no copy is available.
- When a copy is returned, it is reserved for the member who has waited the longest.

Here, `isbn` is the unique number used to identify a book title, while `day` is the day number used to calculate due dates and fines.

### What the System Will Not Include

To keep this case study focused, the first version will not include:

- Searching the library catalogue
- Renewing a loan
- Paying fines online

These features can be added later. Leaving them out now keeps the design small enough to understand, build, and test.

## Step 2: Find the Classes

A simple trick: underline the **nouns** in the requirements. Most of them become classes. The **verbs** become methods.

| Class | Its one job |
|---|---|
| Book | Knows the title, the number of free copies and the waiting list |
| Loan | Remembers the member, the book and the due day |
| Library | Checks the rules, lends and takes back books |

<LldDiagram caption="The class diagram for this design." code={"classDiagram\n  class Library {\n    -books: Map~string, Book~\n    -loans: Loan[]\n    +`addBook(isbn, title, copies)` void\n    +`borrow(member, isbn, day)` void\n    +`giveBack(member, isbn, day)` number\n    +`reserve(member, isbn)` void\n    +available(isbn) number\n  }\n  class Book {\n    +isbn: string\n    +title: string\n    +free: number\n    +waiting: string[]\n  }\n  class Loan {\n    +member: string\n    +isbn: string\n    +due: number\n  }\n  Library *-- Book\n  Library *-- Loan\n  Loan --> Book : for"} />

## Step 3: Build the Key Parts

### A Book Is Not a Copy

The library has one title, "Clean Code", but three physical copies. The title is the same for everyone; the number of free copies changes.

The picture shows one title with three copies, after one copy is lent out:

Here is the same idea in code:

```ts runnable title="A book title is not the same as a copy"
class Book {
  constructor(readonly isbn: string, readonly title: string, public free: number) {}
}

const cleanCode = new Book("111", "Clean Code", 3);
cleanCode.free--; // one copy lent out
console.log(`${cleanCode.title}: ${cleanCode.free} of 3 copies free`);
```

Expected output:

```text
Clean Code: 2 of 3 copies free
```

### The Loan

A **loan** is the record of one member having one copy. It remembers the due day, so it can work out the fine.

Step through asha's loan. She borrows on day 0, so it is due on day 14. The fine is 10 a day:

Here is the same idea in code:

```ts runnable title="A loan remembers who, what and when it is due"
class Loan {
  constructor(readonly member: string, readonly isbn: string, readonly due: number) {}
  fine(returnDay: number, perDay: number): number {
    return Math.max(0, returnDay - this.due) * perDay;
  }
}

const loan = new Loan("asha", "111", 0 + 14);
console.log("on time:", loan.fine(14, 10));
console.log("3 days late:", loan.fine(17, 10));
```

Expected output:

```text
on time: 0
3 days late: 30
```

### The Rules, in Order

Write the borrowing rules as plain checks, in a fixed order, each with a clear reason.

Step through four borrow requests. The checks are: the 3-book limit, a free copy, and whether someone else is first in the queue:

| Member | Books held | Free copies | First in queue | Answer |
|---|---|---|---|---|
| asha | 1 | 2 | none | ok |
| asha | 3 | 2 | none | refused: already has 3 books |
| meena | 0 | 1 | ravi | refused: reserved for ravi |
| ravi | 0 | 1 | ravi | ok |

Here is the same idea in code:

```ts runnable title="The rules, checked in a clear order"
const MAX_LOANS = 3;

function canBorrow(loansHeld: number, freeCopies: number, firstInQueue: string | null, member: string): string {
  if (loansHeld >= MAX_LOANS) return "refused: already has 3 books";
  if (freeCopies === 0) return "refused: no copy free";
  if (firstInQueue !== null && firstInQueue !== member) return `refused: reserved for ${firstInQueue}`;
  return "ok";
}

console.log(canBorrow(1, 2, null, "asha"));
console.log(canBorrow(3, 2, null, "asha"));
console.log(canBorrow(0, 1, "ravi", "meena"));
console.log(canBorrow(0, 1, "ravi", "ravi"));
```

Expected output:

```text
ok
refused: already has 3 books
refused: reserved for ravi
ok
```

**Easy way to remember the design:** a **Book** knows how many copies are free. A **Loan** knows who has a copy and when it is due. The **Library** checks the rules and keeps the waiting lists.

### The Waiting List

When no copy is free, a member can **reserve** the book. The waiting list is a **queue**: the first person to reserve gets the next free copy.

Step through three reservations and one returned copy:

Here is the same idea in code:

```ts runnable title="A waiting list is a queue: first come, first served"
const waiting: string[] = [];
waiting.push("ravi");
waiting.push("meena");
waiting.push("kiran");

console.log("next in line:", waiting[0]);
waiting.shift(); // ravi borrows the book
console.log("now waiting:", waiting.join(", "));
```

Expected output:

```text
next in line: ravi
now waiting: meena, kiran
```

A reserved copy must be held for the first person in the queue. If anyone could borrow it when it comes back, the person who waited longest might never get it.

### Room to Grow

Teachers may get more books for longer. Instead of `if (member.type === "teacher")` in many places, keep the rules in one table, or one strategy per member type.

The rules table has one row per member type:

| Member type | Max books | Loan days |
|---|---|---|
| student | 3 | 14 |
| teacher | 10 | 30 |

The `Library` reads the row for the member, so a new member type is one new row.

Here is the code:

```ts runnable title="Different members, different rules: a strategy"
interface MemberRules {
  maxLoans: number;
  loanDays: number;
}
const RULES: Record<string, MemberRules> = {
  student: { maxLoans: 3, loanDays: 14 },
  teacher: { maxLoans: 10, loanDays: 30 },
};

for (const kind of ["student", "teacher"]) {
  const r = RULES[kind];
  console.log(`${kind}: up to ${r.maxLoans} books for ${r.loanDays} days`);
}
```

Expected output:

```text
student: up to 3 books for 14 days
teacher: up to 10 books for 30 days
```

## Lab: Build It

Build the `Library` class. The hidden tests lend and return books, check the limit and fines, and check that the waiting list is fair.

1. Make `Book` (isbn, title, free copies, waiting list) and `Loan` (member, isbn, due day) classes.
2. In `borrow`, check in order: the 3-book limit, the same book twice, a free copy, and whether someone else is first in the queue.
3. In `giveBack`, remove the loan, free the copy and return the fine. In `reserve`, add the member to the waiting list.

Open the lab in the sandbox. Run the tests after each step. A failing test names the situation that broke, not the test code.

## Predict the Output

<CsPredict id="lib-1" code={"var waiting = new Queue<string>();\nwaiting.Enqueue(\"ravi\");\nwaiting.Enqueue(\"meena\");\nConsole.WriteLine(waiting.Dequeue());\nwaiting.Enqueue(\"kiran\");\nConsole.WriteLine(waiting.Peek());\nConsole.WriteLine(waiting.Count);"} expected={"ravi\nmeena\n2"} why="A queue is first in, first out. Ravi joined first, so he leaves first. Then meena is at the front, and two people are waiting." />

## Common Mistakes

| Mistake | Wrong | Correct | Why it happens |
|---|---|---|---|
| Title as one item | Only one person can borrow Clean Code. | A count of free copies. | It is easy to forget a library has copies. |
| Due day not stored | Working out due days from the borrow date everywhere. | Store due on the loan. | The rule "14 days" seems simple enough to repeat. |
| Unfair queue | A returned copy goes to whoever calls borrow first. | Hold it for the first person waiting. | The happy-path test has no queue. |
| Fine from the borrow date | Charging from day 0 instead of the due day. | Count only days after the due day. | Mixing up the two dates. |
| Vague errors | "cannot borrow" for every reason. | A clear reason for each rule. | One error message is quicker to write. |

## Practice

## What to Remember

- A **book** has a count of free **copies**.
- A **loan** records who, what and the **due day**.
- Check the rules in a **fixed order**, each with a clear reason.
- The waiting list is a **queue**: first come, first served.
- Keep member rules in a **table** or strategy.

## Further Reading

These are the sources used to check this case study:

- [MDN: Array.prototype.shift](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift): taking the first person from a queue.
- [Microsoft Learn: Queue](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1): queues in C#.
- [TypeScript Handbook: Classes](https://www.typescriptlang.org/docs/handbook/2/classes.html): static readonly constants.

Next: **Vending Machine**. You will use the State pattern to build a machine whose buttons do different things at different times.
