Design a Library Management System

SPOILER ALERT: this is a long post with code examples.

This is my first take on the famous "Design a Library Management System". Feel free to provide feedback on what's wrong / how this can be improved.

Requirements

Books have the following information:

  • Unique id
  • Title
  • Author
  • Publication Date

There can be multiple copies of the same book (book items). Each book item has a unique barcode.

There can be 2 types of users:

  • Librarians - Can add and remove books, book items and users, search the catalog (by title, author or publication date). Can also checkout, renew and return books.
  • General members - can search the catalog (by title, author or publication date), as well as check-out, renew, and return a book.

Each user has a unique barcode and a name.

Also, we have the following limitations:

  • A member can checkout at most 3 books
  • A member can keep a book at most 20 days.
  • The system should be able to calculate the fine for the users who return the books after the expected deadline.

My Solution:

We have the following classes/interfaces:

A BookDetails class representing the details for a given book (id, title, author, etc). This is NOT a book item, just a class describing book details. Notice that it is immutable (once created, cannot be modified) so it can be used as a key for Maps:

public class BookDetails {
	private String id;
	private String authorName;
	private LocalDateTime publicationDate;

	public BookDetails(String id, String authorName, LocalDateTime publicationDate) {
		this.id = id;
		this.authorName = authorName;
		this.publicationDate = publicationDate;
	}

	public String getId() {
		return id;
	}

	public String getAuthorName() {
		return authorName;
	}

	public LocalDateTime getPublicationDate() {
		return publicationDate;
	}

}

A BookItem class representing a single instance of a given book. We use aggregation here to get the book details. In addition to the book details, we are also adding a barcode property that is unique to each BookItem:

public class BookItem {
	public BookItem(String bookItemId, BookDetails bookDetails) {
		this.bookItemId = bookItemId;
		this.bookDetails = bookDetails;
	}

	private String bookItemId;

	private BookDetails bookDetails;

	public String getBookItemId() {
		return bookItemId;
	}

	public BookDetails getBookDetails() {
		return bookDetails;
	}
}

A UserType enum representing user types.

public enum UserType {
	LIBRARIAN, MEMBER
}

A User class representing the library users. Some solutions suggest creating multiple classes for different types of users (not sure what we are achieving by doing that):

public class User {
	public User(String barcode, String name, UserType userType) {
		this.barcode = barcode;
		this.name = name;
		this.userType = userType;
	}

	private String barcode;
	private String name;
	private UserType userType;

	public String getBarcode() {
		return barcode;
	}

	public UserType getUserType() {
		return userType;
	}

	public String getName() {
		return name;
	}
}

Now, let's look at the main class - the Library class that exposes all the necessary actions:

public class Library {

	private User currentUser;
	private UserRepository userRepository;
	private BookCatalog bookCatalog;
	private LendingService lendingService;

	private void ensureLoggedInUser() {
		Objects.requireNonNull(this.currentUser, "There should be a logged in user");
	}

	private void ensureLibrarianAccess() throws RestrictedAccessException {
		ensureLoggedInUser();
		if (currentUser.getUserType() != UserType.LIBRARIAN) {
			throw new RestrictedAccessException();
		}
	}

	public void loginUser(String barcode) {
		User user = userRepository.getUserByBarCode(barcode);

		Objects.requireNonNull(user, "Wrong barcode");

		this.currentUser = user;
	}

	public void createUser(User newUser) throws RestrictedAccessException {
		ensureLibrarianAccess();

		userRepository.addUser(newUser);
	}

	public void removeUser(String barcode) throws RestrictedAccessException {
		ensureLibrarianAccess();

		User user = userRepository.getUserByBarCode(barcode);

		Objects.requireNonNull(user, "No user found");

		userRepository.removeUser(barcode);
	}

	public void addBookItem(BookItem bookItem) throws RestrictedAccessException {
		ensureLibrarianAccess();

		bookCatalog.addBookItem(bookItem);
	}

	public void removeBookItem(String bookItemID) throws RestrictedAccessException  {
		ensureLibrarianAccess();

		bookCatalog.removeBookItem(bookItemID);
	}

	public List<BookDetails> searchByTitle(String title) {
		ensureLoggedInUser();

		return bookCatalog.searchByTitle(title);
	}

	public List<BookDetails> searchByAuthor(String name) {
		ensureLoggedInUser();

		return bookCatalog.searchByAuthor(name);
	}

	public List<BookDetails> searchByPublicationDate(LocalDateTime publicationDate) {
		ensureLoggedInUser();

		return bookCatalog.searchByPublicationDate(publicationDate);
	}

	public List<BookItem> getCheckoutBooks() {
		ensureLoggedInUser();

		return lendingService.getCheckedOutBooks(currentUser);
	}

	public List<User> getLendingUsers(String bookID) throws RestrictedAccessException {
		ensureLibrarianAccess();

		return lendingService.getLendingUsers(bookID);
	}

	public int getOverdueFines() {
		ensureLoggedInUser();
		return lendingService.getOverdueFineAmount(currentUser);
	}

	public BookItem checkout(String bookID) {
		ensureLoggedInUser();

		return lendingService.checkout(currentUser, bookID);
	}

	public boolean renew(BookItem item) {
		ensureLoggedInUser();

		return lendingService.renew(currentUser, item);
	}

	public boolean returnBookItem(BookItem item) {
		ensureLoggedInUser();

		return lendingService.returnBook(currentUser, item);
	}

	public Library(
		UserRepository userRepository,
		BookCatalog bookCatalog,
		LendingService lendingService
	) {
		this.userRepository = userRepository;
		this.bookCatalog = bookCatalog;
		this.lendingService = lendingService;
	}
}

A couple of important notes about this class:

  • Throught constructor injection we get a UserRepository (a repository handling user persistence), a BookCatalog (allowing us to search/add/remove books and book items) and a LendingService (responsible for actions like checkout, renew, return and find calculation). UserRepository , BookCatalog and LendingService are all interfaces exposing appropriate actions (they will be discussed later). This makes unit testing easier and results in a loosely coupled code. The Library class uses these services to perform all the necessary actions. This seems to be a neat way to organize the responsibilities (if you feel that this can be further improved, please, let me know).
  • Since we have to make access checks, we have to introduce the notion of the currently logged in user (it is saved in the currentUser field). Also, we have a couple of access check related methods (ensureLoggedInUser, ensureLibrarianAccess) that are throwing exceptions if certain invariants are violated. As a potential improvement, we could create an AccessCheckService that does this.
  • The rest of the code is fairly simple. For each action, we ensure that the current user can perform it (otherwise, an exception is thrown) and then the action is performed through the services.

Now, let's look at the service interfaces:

Fairly simple user persistence abstraction. I haven't provided a concrete implementation, can be done by having a Map from barcode to user instance:

public interface UserRepository {
	User getUserByBarCode(String barCode);
	void addUser(User user);
	void removeUser(String barCode);
}

The BookCatalog interface - represents a book catalog. For brevity, I have not have provided an implementation for this either but its easy to code a Map based implementation:

public interface BookCatalog {
	void addBookItem(BookItem bookItem);
	void removeBookItem(String bookItemID);
	List<BookDetails> searchByTitle(String title);
	List<BookDetails> searchByAuthor(String name);
	List<BookDetails> searchByPublicationDate(LocalDateTime publicationDate);
}

A LendingService interface - performs the lending related tasks:

public interface LendingService {
	List<BookItem> getCheckedOutBooks(User user);
	List<User> getLendingUsers(String bookID);
	int getOverdueFineAmount(User user);
	BookItem checkout(User user, String bookID);
	boolean renew(User user, BookItem bookItem);
	boolean returnBook(User user, BookItem bookItem);
}

This is my first time solving this kind of problems. Any feedback is much appreciated.

Comments (20)