Loading learning content...
With requirements clearly defined, we now extract the core entities that will form the backbone of our domain model. Entity identification is both science and art—we apply systematic techniques while using domain intuition to make judgment calls.
The Noun Extraction Technique:
The most reliable method for identifying entities is to extract nouns from requirements and use cases, then filter for relevance:
Entity Criteria:
A noun qualifies as an entity if it has:
By the end of this page, you will be able to: • Apply noun extraction to identify candidate entities • Define complete entity specifications with attributes and behaviors • Distinguish between entities and value objects • Understand the responsibilities of each core entity • Design entity classes with proper encapsulation
Let's apply noun extraction to our Library Management System requirements. We'll examine key phrases from our use cases:
"Member presents library card and book" "System creates loan record with due date" "Librarian can modify book details" "Fine accrues daily based on membership type" "Reservations processed in FIFO order"
| Noun | Classification | Reasoning |
|---|---|---|
| Book | Entity ✓ | Has identity (ISBN), lifecycle, central to domain |
| Member | Entity ✓ | Has identity (member ID), lifecycle, owns loans/fines |
| Loan | Entity ✓ | Has identity, tracks state transitions, audit requirements |
| Librarian | Entity ✓ | Has identity, performs actions, different from Member |
| Reservation | Entity ✓ | Has identity, lifecycle (pending→fulfilled/expired) |
| Fine | Entity ✓ | Has identity, tracks payments, accumulates over time |
| Library Card | Value Object | Describes member, no independent identity |
| Due Date | Value Object | Attribute of Loan, just a date value |
| Membership Type | Enumeration | Fixed set of values with associated rules |
| System | External Actor | Not an entity—it's the system itself |
| Book Details | Not Entity | Attributes within Book entity |
Entities have identity—two loans for the same book by the same member are still different loans. Value Objects are defined by their attributes—two dates of '2024-03-15' are interchangeable. When in doubt, ask: 'Do I need to track this thing's lifecycle independently?' If yes, it's likely an entity.
Additional Entities Discovered:
Beyond direct noun extraction, domain analysis reveals additional entities:
| Entity | Discovery Source | Role |
|---|---|---|
| BookCopy | 'Multiple copies of the same book' | Physical instance that can be loaned |
| Author | Books have authors (may be shared) | Supports catalog search by author |
| Notification | 'System notifies member' | Tracks pending communications |
| Payment | 'Process fine payments' | Records payment transactions |
Entity Hierarchy Insight:
Notice that Book and BookCopy represent different concepts:
This distinction is crucial: a library might have 5 copies of 'Clean Code'. Each copy has its own status (available, loaned, damaged), but they share the same title metadata.
The fundamental resource in any library system is the book. Our design separates the intellectual work (Book) from its physical manifestations (BookCopy), enabling proper inventory tracking.
Book Entity:
Represents a unique title in the catalog. Think of it as what you'd find in a bibliographic database.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
/** * Represents a book title in the library catalog. * This is the intellectual work, not a physical copy. */class Book { private readonly id: string; private isbn: string; private title: string; private authors: Author[]; private publisher: string; private publicationYear: number; private subject: string; private description: string; private readonly copies: BookCopy[]; private readonly createdAt: Date; private updatedAt: Date; constructor( id: string, isbn: string, title: string, authors: Author[], publisher: string, publicationYear: number, subject: string ) { this.id = id; this.isbn = isbn; this.title = title; this.authors = authors; this.publisher = publisher; this.publicationYear = publicationYear; this.subject = subject; this.description = ""; this.copies = []; this.createdAt = new Date(); this.updatedAt = new Date(); } // ============ Core Behaviors ============ /** * Adds a new physical copy of this book to the library */ addCopy(barcode: string, rackLocation: string): BookCopy { const copy = new BookCopy( this.generateCopyId(), barcode, this, rackLocation ); this.copies.push(copy); this.markUpdated(); return copy; } /** * Removes a copy from circulation (damaged, lost, etc.) * Throws if copy has active loan */ removeCopy(copyId: string): void { const copy = this.findCopy(copyId); if (!copy) { throw new Error(`Copy ${copyId} not found`); } if (copy.isCurrentlyLoaned()) { throw new Error('Cannot remove copy with active loan'); } const index = this.copies.indexOf(copy); this.copies.splice(index, 1); this.markUpdated(); } /** * Finds an available copy for checkout * Returns null if no copies available */ findAvailableCopy(): BookCopy | null { return this.copies.find(copy => copy.isAvailable()) || null; } /** * Gets count of available copies */ getAvailableCopyCount(): number { return this.copies.filter(copy => copy.isAvailable()).length; } /** * Gets total copy count in inventory */ getTotalCopyCount(): number { return this.copies.length; } /** * Checks if any copy is available for borrowing */ hasAvailableCopy(): boolean { return this.copies.some(copy => copy.isAvailable()); } // ============ Catalog Operations ============ updateDetails( title?: string, publisher?: string, description?: string ): void { if (title) this.title = title; if (publisher) this.publisher = publisher; if (description) this.description = description; this.markUpdated(); } addAuthor(author: Author): void { if (!this.authors.includes(author)) { this.authors.push(author); this.markUpdated(); } } // ============ Private Helpers ============ private findCopy(copyId: string): BookCopy | undefined { return this.copies.find(c => c.getId() === copyId); } private generateCopyId(): string { return `${this.id}-COPY-${this.copies.length + 1}`; } private markUpdated(): void { this.updatedAt = new Date(); } // ============ Getters ============ getId(): string { return this.id; } getIsbn(): string { return this.isbn; } getTitle(): string { return this.title; } getAuthors(): Author[] { return [...this.authors]; } getPublisher(): string { return this.publisher; } getPublicationYear(): number { return this.publicationYear; } getSubject(): string { return this.subject; } getCopies(): BookCopy[] { return [...this.copies]; }}BookCopy Entity:
Represents a specific physical copy that can be checked out. Each copy tracks its own status and location.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
/** * Status of a physical book copy */enum BookCopyStatus { AVAILABLE = 'AVAILABLE', // Can be borrowed LOANED = 'LOANED', // Currently checked out RESERVED = 'RESERVED', // Held for pickup by reservation LOST = 'LOST', // Reported lost DAMAGED = 'DAMAGED', // Under repair REFERENCE = 'REFERENCE' // Cannot leave library} /** * Represents a physical copy of a book. * This is what gets loaned to members. */class BookCopy { private readonly id: string; private readonly barcode: string; private readonly book: Book; // Reference to parent Book private status: BookCopyStatus; private rackLocation: string; private readonly addedDate: Date; private currentLoan: Loan | null; constructor( id: string, barcode: string, book: Book, rackLocation: string ) { this.id = id; this.barcode = barcode; this.book = book; this.rackLocation = rackLocation; this.status = BookCopyStatus.AVAILABLE; this.addedDate = new Date(); this.currentLoan = null; } // ============ Status Management ============ /** * Marks copy as loaned to a member */ checkOut(loan: Loan): void { if (!this.isAvailable()) { throw new Error( `Cannot checkout copy ${this.id}: status is ${this.status}` ); } this.status = BookCopyStatus.LOANED; this.currentLoan = loan; } /** * Marks copy as returned * @param hasReservation - if true, status becomes RESERVED */ checkIn(hasReservation: boolean): void { if (this.status !== BookCopyStatus.LOANED) { throw new Error( `Cannot check in copy ${this.id}: not currently loaned` ); } this.currentLoan = null; this.status = hasReservation ? BookCopyStatus.RESERVED : BookCopyStatus.AVAILABLE; } /** * Places copy on hold for reservation pickup */ placeOnHold(): void { if (this.status !== BookCopyStatus.AVAILABLE) { throw new Error( `Cannot hold copy ${this.id}: status is ${this.status}` ); } this.status = BookCopyStatus.RESERVED; } /** * Releases hold, making copy available again * Called when reservation expires or is cancelled */ releaseHold(): void { if (this.status !== BookCopyStatus.RESERVED) { throw new Error( `Cannot release hold on copy ${this.id}: not reserved` ); } this.status = BookCopyStatus.AVAILABLE; } /** * Marks copy as lost */ reportLost(): void { this.status = BookCopyStatus.LOST; this.currentLoan = null; } /** * Marks copy as damaged */ reportDamaged(): void { this.status = BookCopyStatus.DAMAGED; } // ============ Query Methods ============ isAvailable(): boolean { return this.status === BookCopyStatus.AVAILABLE; } isCurrentlyLoaned(): boolean { return this.status === BookCopyStatus.LOANED; } canBeLoaned(): boolean { return this.status === BookCopyStatus.AVAILABLE || this.status === BookCopyStatus.RESERVED; } // ============ Getters ============ getId(): string { return this.id; } getBarcode(): string { return this.barcode; } getBook(): Book { return this.book; } getStatus(): BookCopyStatus { return this.status; } getRackLocation(): string { return this.rackLocation; } getCurrentLoan(): Loan | null { return this.currentLoan; }}Notice that BookCopy holds a reference to its parent Book, establishing a bidirectional relationship. This allows navigation from a copy to its metadata (for display) while Book maintains its collection of copies (for availability queries). The Book.findAvailableCopy() method encapsulates collection traversal, hiding the implementation from callers.
The Member entity represents library patrons. It tracks personal information, membership status, active loans, and fines. The member's MembershipType determines borrowing limits, loan periods, and fine rates.
Key Design Considerations:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
/** * Types of membership with different borrowing privileges */enum MembershipType { STUDENT = 'STUDENT', FACULTY = 'FACULTY', GENERAL = 'GENERAL'} /** * Account status affecting borrowing ability */enum MemberStatus { ACTIVE = 'ACTIVE', SUSPENDED = 'SUSPENDED', EXPIRED = 'EXPIRED', CLOSED = 'CLOSED'} /** * Represents a library member who can borrow books. */class Member { private readonly id: string; private readonly cardNumber: string; private name: string; private email: string; private phone: string; private address: Address; private membershipType: MembershipType; private status: MemberStatus; private readonly activeLoans: Loan[]; private readonly loanHistory: Loan[]; private readonly reservations: Reservation[]; private totalFinesOwed: number; private readonly memberSince: Date; private membershipExpiry: Date; constructor( id: string, cardNumber: string, name: string, email: string, membershipType: MembershipType ) { this.id = id; this.cardNumber = cardNumber; this.name = name; this.email = email; this.phone = ""; this.address = new Address("", "", "", ""); this.membershipType = membershipType; this.status = MemberStatus.ACTIVE; this.activeLoans = []; this.loanHistory = []; this.reservations = []; this.totalFinesOwed = 0; this.memberSince = new Date(); this.membershipExpiry = this.calculateExpiryDate(); } // ============ Borrowing Operations ============ /** * Checks if member can borrow more books * Validates status, limit, and fine threshold */ canBorrow(): BorrowEligibility { if (this.status !== MemberStatus.ACTIVE) { return { eligible: false, reason: `Account is ${this.status.toLowerCase()}` }; } if (this.isAtBorrowingLimit()) { return { eligible: false, reason: `At borrowing limit (${this.getBorrowingLimit()} books)` }; } if (this.hasExcessiveFines()) { return { eligible: false, reason: `Outstanding fines exceed threshold ($${this.totalFinesOwed})` }; } return { eligible: true }; } /** * Records a new loan for this member */ addLoan(loan: Loan): void { const eligibility = this.canBorrow(); if (!eligibility.eligible) { throw new Error(`Cannot add loan: ${eligibility.reason}`); } this.activeLoans.push(loan); } /** * Marks a loan as returned and moves to history */ completeLoan(loan: Loan): void { const index = this.activeLoans.indexOf(loan); if (index === -1) { throw new Error('Loan not found in active loans'); } this.activeLoans.splice(index, 1); this.loanHistory.push(loan); } /** * Gets remaining borrowing capacity */ getRemainingBorrowingCapacity(): number { return this.getBorrowingLimit() - this.activeLoans.length; } // ============ Fine Management ============ /** * Adds a fine to the member's account */ addFine(amount: number): void { if (amount <= 0) { throw new Error('Fine amount must be positive'); } this.totalFinesOwed += amount; // Auto-suspend if over threshold if (this.hasExcessiveFines()) { this.suspend('Excessive unpaid fines'); } } /** * Processes a fine payment */ payFine(amount: number): void { if (amount <= 0) { throw new Error('Payment amount must be positive'); } if (amount > this.totalFinesOwed) { throw new Error('Payment exceeds outstanding fines'); } this.totalFinesOwed -= amount; // Reactivate if fines cleared and was suspended for fines if (this.totalFinesOwed === 0 && this.status === MemberStatus.SUSPENDED) { this.reactivate(); } } /** * Waives all or part of fines (librarian action) */ waiveFine(amount: number): void { const waiveAmount = Math.min(amount, this.totalFinesOwed); this.totalFinesOwed -= waiveAmount; } // ============ Reservation Management ============ /** * Adds a reservation for this member */ addReservation(reservation: Reservation): void { if (this.reservations.length >= this.getMaxReservations()) { throw new Error( `Maximum reservations (${this.getMaxReservations()}) reached` ); } this.reservations.push(reservation); } /** * Removes a reservation (fulfilled, cancelled, or expired) */ removeReservation(reservation: Reservation): void { const index = this.reservations.indexOf(reservation); if (index !== -1) { this.reservations.splice(index, 1); } } // ============ Account Status ============ suspend(reason: string): void { this.status = MemberStatus.SUSPENDED; // In real system: log reason, notify member } reactivate(): void { if (this.hasExcessiveFines()) { throw new Error('Cannot reactivate: outstanding fines'); } this.status = MemberStatus.ACTIVE; } renewMembership(years: number = 1): void { const now = new Date(); const baseDate = this.membershipExpiry > now ? this.membershipExpiry : now; this.membershipExpiry = new Date( baseDate.setFullYear(baseDate.getFullYear() + years) ); if (this.status === MemberStatus.EXPIRED) { this.status = MemberStatus.ACTIVE; } } // ============ Membership Type Rules ============ getBorrowingLimit(): number { switch (this.membershipType) { case MembershipType.FACULTY: return 10; case MembershipType.STUDENT: return 5; case MembershipType.GENERAL: return 3; } } getLoanPeriodDays(): number { switch (this.membershipType) { case MembershipType.FACULTY: return 30; case MembershipType.STUDENT: return 21; case MembershipType.GENERAL: return 14; } } getDailyFineRate(): number { switch (this.membershipType) { case MembershipType.FACULTY: return 0.10; case MembershipType.STUDENT: return 0.25; case MembershipType.GENERAL: return 0.50; } } getMaxReservations(): number { return 5; // Same for all types in our design } // ============ Private Helpers ============ private isAtBorrowingLimit(): boolean { return this.activeLoans.length >= this.getBorrowingLimit(); } private hasExcessiveFines(): boolean { const FINE_SUSPENSION_THRESHOLD = 25.00; return this.totalFinesOwed >= FINE_SUSPENSION_THRESHOLD; } private calculateExpiryDate(): Date { const expiry = new Date(); expiry.setFullYear(expiry.getFullYear() + 1); return expiry; } // ============ Getters ============ getId(): string { return this.id; } getCardNumber(): string { return this.cardNumber; } getName(): string { return this.name; } getEmail(): string { return this.email; } getMembershipType(): MembershipType { return this.membershipType; } getStatus(): MemberStatus { return this.status; } getActiveLoans(): Loan[] { return [...this.activeLoans]; } getTotalFinesOwed(): number { return this.totalFinesOwed; } getReservations(): Reservation[] { return [...this.reservations]; }} interface BorrowEligibility { eligible: boolean; reason?: string;}The current design uses switch statements for membership-specific rules. This works but violates the Open/Closed Principle—adding a new membership type requires modifying Member. In the patterns section, we'll refactor this using the Strategy Pattern to encapsulate membership policies in separate classes.
The Loan entity is the heart of the circulation system. It tracks the borrowing transaction from checkout to return, manages state transitions, and calculates fines for overdue items.
Key Design Considerations:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
/** * Loan status tracking the lifecycle of a borrowing transaction */enum LoanStatus { ACTIVE = 'ACTIVE', // Currently borrowed RETURNED = 'RETURNED', // Returned on time OVERDUE = 'OVERDUE', // Past due date (still active) LOST = 'LOST', // Reported as lost COMPLETED = 'COMPLETED' // Returned (may have been overdue)} /** * Represents a book borrowing transaction. * Tracks the complete lifecycle from checkout to return. */class Loan { private readonly id: string; private readonly member: Member; private readonly bookCopy: BookCopy; private readonly checkoutDate: Date; private dueDate: Date; private returnDate: Date | null; private status: LoanStatus; private renewalCount: number; private fineAmount: number; private readonly processedBy: Librarian; private static readonly MAX_RENEWALS = 2; constructor( id: string, member: Member, bookCopy: BookCopy, processedBy: Librarian ) { this.id = id; this.member = member; this.bookCopy = bookCopy; this.processedBy = processedBy; this.checkoutDate = new Date(); this.dueDate = this.calculateDueDate(); this.returnDate = null; this.status = LoanStatus.ACTIVE; this.renewalCount = 0; this.fineAmount = 0; } // ============ Core Operations ============ /** * Processes the return of this loan * Calculates fine if overdue */ returnBook(): FineResult { if (this.status === LoanStatus.RETURNED || this.status === LoanStatus.COMPLETED) { throw new Error('Loan already returned'); } this.returnDate = new Date(); const wasOverdue = this.isOverdue(); if (wasOverdue) { this.fineAmount = this.calculateFine(); this.member.addFine(this.fineAmount); } this.status = LoanStatus.COMPLETED; this.bookCopy.checkIn(false); // Reservation check done by service return { wasOverdue, fineAmount: this.fineAmount, daysOverdue: wasOverdue ? this.getOverdueDays() : 0 }; } /** * Renews the loan, extending the due date * @throws if renewal limit exceeded or book is reserved */ renew(hasReservation: boolean): void { this.validateRenewal(hasReservation); // Extend from today, not from original due date this.dueDate = this.calculateDueDateFromNow(); this.renewalCount++; // If was overdue, now back to active if (this.status === LoanStatus.OVERDUE) { this.status = LoanStatus.ACTIVE; } } /** * Reports the book as lost * Applies replacement fine */ reportLost(): void { if (this.status !== LoanStatus.ACTIVE && this.status !== LoanStatus.OVERDUE) { throw new Error('Cannot report lost: invalid loan status'); } const LOST_BOOK_FINE = 50.00; // Replacement cost this.fineAmount = LOST_BOOK_FINE + this.calculateFine(); this.member.addFine(this.fineAmount); this.status = LoanStatus.LOST; this.bookCopy.reportLost(); } // ============ Status Checking ============ /** * Checks if loan is past due date */ isOverdue(): boolean { if (this.status === LoanStatus.RETURNED || this.status === LoanStatus.COMPLETED) { return false; } const checkDate = this.returnDate || new Date(); return checkDate > this.dueDate; } /** * Gets number of days overdue (0 if not overdue) */ getOverdueDays(): number { if (!this.isOverdue()) return 0; const checkDate = this.returnDate || new Date(); const diffTime = checkDate.getTime() - this.dueDate.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * Gets days remaining until due (negative if overdue) */ getDaysRemaining(): number { const now = new Date(); const diffTime = this.dueDate.getTime() - now.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * Checks if more renewals are allowed */ canRenew(hasReservation: boolean): RenewalEligibility { if (this.renewalCount >= Loan.MAX_RENEWALS) { return { canRenew: false, reason: `Maximum renewals (${Loan.MAX_RENEWALS}) reached` }; } if (hasReservation) { return { canRenew: false, reason: 'Book has pending reservation' }; } if (this.status !== LoanStatus.ACTIVE && this.status !== LoanStatus.OVERDUE) { return { canRenew: false, reason: 'Loan is no longer active' }; } return { canRenew: true }; } // ============ Fine Calculation ============ /** * Calculates fine based on overdue days and member rate */ private calculateFine(): number { const days = this.getOverdueDays(); const dailyRate = this.member.getDailyFineRate(); const fine = days * dailyRate; // Optional: Cap fine at maximum const MAX_FINE_PER_ITEM = 25.00; return Math.min(fine, MAX_FINE_PER_ITEM); } // ============ Private Helpers ============ private calculateDueDate(): Date { const due = new Date(); due.setDate(due.getDate() + this.member.getLoanPeriodDays()); return due; } private calculateDueDateFromNow(): Date { const due = new Date(); due.setDate(due.getDate() + this.member.getLoanPeriodDays()); return due; } private validateRenewal(hasReservation: boolean): void { const eligibility = this.canRenew(hasReservation); if (!eligibility.canRenew) { throw new Error(`Cannot renew: ${eligibility.reason}`); } } // ============ Getters ============ getId(): string { return this.id; } getMember(): Member { return this.member; } getBookCopy(): BookCopy { return this.bookCopy; } getCheckoutDate(): Date { return new Date(this.checkoutDate); } getDueDate(): Date { return new Date(this.dueDate); } getReturnDate(): Date | null { return this.returnDate ? new Date(this.returnDate) : null; } getStatus(): LoanStatus { return this.status; } getRenewalCount(): number { return this.renewalCount; } getFineAmount(): number { return this.fineAmount; } getProcessedBy(): Librarian { return this.processedBy; }} interface FineResult { wasOverdue: boolean; fineAmount: number; daysOverdue: number;} interface RenewalEligibility { canRenew: boolean; reason?: string;}Notice how getter methods for dates return new Date objects instead of direct references. This prevents callers from accidentally mutating internal state. Similarly, returning result objects (FineResult, RenewalEligibility) instead of multiple return values makes the API clearer and more extensible.
The Librarian represents staff members who perform privileged operations. Unlike Members, librarians can process checkouts, waive fines, and manage the catalog.
Key Design Considerations:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
/** * Permission levels for librarians */enum LibrarianRole { ASSISTANT = 'ASSISTANT', // Basic checkout/return LIBRARIAN = 'LIBRARIAN', // + catalog management, fine waiving SENIOR = 'SENIOR', // + member management, reports ADMIN = 'ADMIN' // Full access} /** * Represents a library staff member with system privileges. */class Librarian { private readonly id: string; private readonly employeeId: string; private name: string; private email: string; private role: LibrarianRole; private isActive: boolean; private readonly hireDate: Date; constructor( id: string, employeeId: string, name: string, email: string, role: LibrarianRole = LibrarianRole.ASSISTANT ) { this.id = id; this.employeeId = employeeId; this.name = name; this.email = email; this.role = role; this.isActive = true; this.hireDate = new Date(); } // ============ Permission Checks ============ /** * Checks if librarian can perform checkout/return */ canProcessLoans(): boolean { return this.isActive; // All roles can process loans } /** * Checks if librarian can modify catalog */ canManageCatalog(): boolean { return this.isActive && (this.role === LibrarianRole.LIBRARIAN || this.role === LibrarianRole.SENIOR || this.role === LibrarianRole.ADMIN); } /** * Checks if librarian can waive fines */ canWaiveFines(): boolean { return this.isActive && (this.role === LibrarianRole.LIBRARIAN || this.role === LibrarianRole.SENIOR || this.role === LibrarianRole.ADMIN); } /** * Checks if librarian can manage member accounts */ canManageMembers(): boolean { return this.isActive && (this.role === LibrarianRole.SENIOR || this.role === LibrarianRole.ADMIN); } /** * Checks if librarian can access reports */ canAccessReports(): boolean { return this.isActive && (this.role === LibrarianRole.SENIOR || this.role === LibrarianRole.ADMIN); } /** * Checks if librarian can manage other librarians */ canManageStaff(): boolean { return this.isActive && this.role === LibrarianRole.ADMIN; } // ============ Account Management ============ promote(newRole: LibrarianRole): void { this.role = newRole; } deactivate(): void { this.isActive = false; } reactivate(): void { this.isActive = true; } // ============ Getters ============ getId(): string { return this.id; } getEmployeeId(): string { return this.employeeId; } getName(): string { return this.name; } getEmail(): string { return this.email; } getRole(): LibrarianRole { return this.role; } getIsActive(): boolean { return this.isActive; }}Our Librarian entity handles authorization (what can this user do?) but not authentication (who is this user?). Authentication would involve password verification, sessions, and tokens—infrastructure concerns outside our LLD scope. We assume the system provides an authenticated Librarian object to our domain.
Beyond the four core entities, several supporting entities complete our domain model. These handle reservations, authors, and addresses.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
/** * Reservation status lifecycle */enum ReservationStatus { PENDING = 'PENDING', // Waiting for book to become available READY = 'READY', // Book available, waiting for pickup FULFILLED = 'FULFILLED', // Member collected the book CANCELLED = 'CANCELLED', // Cancelled by member or system EXPIRED = 'EXPIRED' // Not collected within hold period} /** * Represents a member's request for a currently unavailable book. * Implements a queue system for fair distribution. */class Reservation { private readonly id: string; private readonly member: Member; private readonly book: Book; // Reservation is for a title, not specific copy private status: ReservationStatus; private readonly createdAt: Date; private notifiedAt: Date | null; private expiresAt: Date | null; private fulfilledByLoan: Loan | null; private static readonly HOLD_PERIOD_DAYS = 7; constructor(id: string, member: Member, book: Book) { this.id = id; this.member = member; this.book = book; this.status = ReservationStatus.PENDING; this.createdAt = new Date(); this.notifiedAt = null; this.expiresAt = null; this.fulfilledByLoan = null; } /** * Marks reservation as ready (book returned) * Starts the hold period countdown */ markReady(): void { if (this.status !== ReservationStatus.PENDING) { throw new Error('Can only mark pending reservations as ready'); } this.status = ReservationStatus.READY; this.notifiedAt = new Date(); this.expiresAt = new Date(); this.expiresAt.setDate( this.expiresAt.getDate() + Reservation.HOLD_PERIOD_DAYS ); } /** * Fulfills reservation when member borrows the book */ fulfill(loan: Loan): void { if (this.status !== ReservationStatus.READY) { throw new Error('Can only fulfill ready reservations'); } this.status = ReservationStatus.FULFILLED; this.fulfilledByLoan = loan; this.member.removeReservation(this); } /** * Cancels reservation (member request or system cleanup) */ cancel(): void { if (this.status === ReservationStatus.FULFILLED) { throw new Error('Cannot cancel fulfilled reservation'); } this.status = ReservationStatus.CANCELLED; this.member.removeReservation(this); } /** * Expires reservation if not collected in time */ expire(): void { if (this.status !== ReservationStatus.READY) { throw new Error('Can only expire ready reservations'); } this.status = ReservationStatus.EXPIRED; this.member.removeReservation(this); } /** * Checks if reservation has expired */ isExpired(): boolean { if (this.status !== ReservationStatus.READY || !this.expiresAt) { return false; } return new Date() > this.expiresAt; } // Getters getId(): string { return this.id; } getMember(): Member { return this.member; } getBook(): Book { return this.book; } getStatus(): ReservationStatus { return this.status; } getCreatedAt(): Date { return new Date(this.createdAt); } getExpiresAt(): Date | null { return this.expiresAt ? new Date(this.expiresAt) : null; }} /** * Represents a book author. * Shared across multiple books. */class Author { private readonly id: string; private name: string; private biography: string; constructor(id: string, name: string) { this.id = id; this.name = name; this.biography = ""; } updateBiography(bio: string): void { this.biography = bio; } getId(): string { return this.id; } getName(): string { return this.name; } getBiography(): string { return this.biography; }} /** * Value Object: Address * No independent identity - defined entirely by its attributes. */class Address { private readonly street: string; private readonly city: string; private readonly state: string; private readonly zipCode: string; constructor( street: string, city: string, state: string, zipCode: string ) { this.street = street; this.city = city; this.state = state; this.zipCode = zipCode; } // Value objects are typically immutable // No setters - create new instance for changes getFullAddress(): string { return `${this.street}, ${this.city}, ${this.state} ${this.zipCode}`; } equals(other: Address): boolean { return this.street === other.street && this.city === other.city && this.state === other.state && this.zipCode === other.zipCode; } getStreet(): string { return this.street; } getCity(): string { return this.city; } getState(): string { return this.state; } getZipCode(): string { return this.zipCode; }}Let's consolidate our entity identification with a summary table:
| Entity | Type | Key Responsibility | Key Relationships |
|---|---|---|---|
| Book | Aggregate Root | Catalog entry, manages copies | → BookCopy (1:many), → Author (many:many) |
| BookCopy | Entity | Physical item for checkout | → Book (many:1), → Loan (1:1 current) |
| Member | Aggregate Root | Patron account, borrowing | → Loan (1:many), → Reservation (1:many) |
| Loan | Entity | Borrowing transaction | → Member (many:1), → BookCopy (many:1) |
| Librarian | Entity | Staff actions, permissions | → Loan (processes) |
| Reservation | Entity | Hold request queue | → Member (many:1), → Book (many:1) |
| Author | Entity | Book creator | → Book (many:many) |
| Address | Value Object | Location data | Embedded in Member |
What's Next:
With entities defined, we need to formally establish relationships and design the class structure. The next page covers:
You've identified and designed all core entities for the Library Management System. Each entity has clear responsibilities, well-defined attributes, and meaningful behaviors. The enum types and value objects support the main entities with type safety and domain clarity. Next, we'll connect these entities through formal relationship modeling.