problemmediumooddesign-data-structures-for-an-online-book-reader-systemdesign data structures for an online book reader systemdesigndatastructuresforanonlinebookreadersystem

OOD - Online Book Reader System

MediumUpdated: Jan 1, 2026

1. Problem Statement

Design a simple online book reader that models users, books, and the relationship between them. The system should let users search/open books and track per-user progress (current page, last accessed). Keep the design clean and minimal with clear separation of concerns.

Core goals:

  • Manage books and users independently
  • Support m:n relationship between users and books (many readers per book; many books per reader)
  • Provide operations to search books/users and assign books to users
  • Track per-user reading metadata (current page, last accessed)

2. System Requirements

Functional Requirements:

  • Create and manage books (add, update, delete, find)
  • Create and manage users (add, renew membership, find)
  • Assign books to users; list a user’s books and a book’s users
  • Search books and users by id; open/read books (update progress)

Non-Functional Requirements:

  • In-memory data structures (no persistence)
  • Simple, readable APIs; avoid database-specific details in core classes

Assumptions:

  • Book and User IDs are unique
  • Reading progress is tracked per (user, book) pair
  • Authentication/authorization is out of scope

3. Use Case Diagram

Actors: Reader (User), System

Use Cases: Search Book, Open Book, Assign Book to User, Renew Membership, Manage Books/Users

graph TB
  subgraph "Online Book Reader System"
	UC1["Search Book"]
	UC2["Open/Read Book"]
	UC3["Assign Book to User"]
	UC4["Renew Membership"]
	UC5["Manage Books"]
	UC6["Manage Users"]
  end
  Reader([Reader])
  System([System])
  Reader --> UC1
  Reader --> UC2
  System --> UC3
  System --> UC4
  System --> UC5
  System --> UC6
  style Reader fill:#4CAF50,color:#fff
  style System fill:#2196F3,color:#fff

4. Class Diagram

Core Classes:

  • Book: id, title, author
  • User: id, username, email, accountType
  • Books: manages collection of Book
  • Users: manages collection of User
  • BookUsers: bidirectional maps between books and users + progress
  • OnlineReaderSystem: façade coordinating operations
classDiagram
  class Book {
	+long id
	+String title
	+String author
	+getId() long
  }
  class User {
	+long id
	+String username
	+String email
	+int accountType
	+getId() long
  }
  class Books {
	-Set~Book~ books
	+addBook(long, String, String) void
	+update(Book) void
	+delete(Book) void
	+find(long) Book
  }
  class Users {
	-Set~User~ users
	+addUser(long, String, String, int) void
	+renewMembership(User) void
	+find(long) User
  }
  class BookUsers {
	-Map~Book, Set~User~~ bookUsers
	-Map~User, Set~Book~~ userBooks
	-Map~String, int~ currentPageByPair
	-Map~String, datetime~ lastAccessedByPair
	+assign(Book, User) void
	+getUsers(Book) Set~User~
	+getBooks(User) Set~Book~
	+updateProgress(Book, User, int) void
  }
  class OnlineReaderSystem {
	-Books books
	-Users users
	-BookUsers bookUsers
	+assignBookToUser(Book, User) void
	+searchBook(long) Book
	+searchUser(long) User
  }
  
  OnlineReaderSystem --> Books
  OnlineReaderSystem --> Users
  OnlineReaderSystem --> BookUsers
  Books "1" o-- "*" Book
  Users "1" o-- "*" User
  BookUsers "1" o-- "*" Book
  BookUsers "1" o-- "*" User

5. Activity Diagrams

Assign Book to User

graph TB
  A[Select Book and User] --> B[Validate both exist]
  B --> C[Add User to BookUsers map]
  C --> D[Add Book to UserBooks map]
	D --> E[Initialize progress page 1 timestamp]
  E --> F[Return success]

Read Book (Update Progress)

graph TB
  A[User opens book] --> B[Fetch current page]
  B --> C[User reads / navigates pages]
	C --> D[Update current page for user and book]
  D --> E[Update last accessed timestamp]
  E --> F[Return updated status]

6. Java Implementation

import java.util.*;

class Book {
	private final long id;
	private final String title;
	private final String author;

	public Book(long id, String title, String author) {
		this.id = id;
		this.title = title;
		this.author = author;
	}
	public long getId() { return id; }
	public String getTitle() { return title; }
	public String getAuthor() { return author; }
}

class User {
	private final long id;
	private final String username;
	private final String email;
	private final int accountType;

	public User(long id, String username, String email, int accountType) {
		this.id = id;
		this.username = username;
		this.email = email;
		this.accountType = accountType;
	}
	public long getId() { return id; }
}

class Books {
	private final Set<Book> books = new HashSet<>();

	public void addBook(long id, String title, String author) {
		books.add(new Book(id, title, author));
	}
	public void update(Book b) { /* no-op skeleton */ }
	public void delete(Book b) { books.remove(b); }
	public Book find(long id) {
		for (Book b : books) {
			if (b.getId() == id) return b;
		}
		return null;
	}
}

class Users {
	private final Set<User> users = new HashSet<>();

	public void addUser(long id, String username, String email, int accountType) {
		users.add(new User(id, username, email, accountType));
	}
	public void renewMembership(User u) { /* no-op skeleton */ }
	public User find(long id) {
		for (User u : users) {
			if (u.getId() == id) return u;
		}
		return null;
	}
}

class BookUsers {
	private final Map<Book, Set<User>> bookUsers = new HashMap<>();
	private final Map<User, Set<Book>> userBooks = new HashMap<>();
	private final Map<String, Integer> currentPageByPair = new HashMap<>();
	private final Map<String, Date> lastAccessedByPair = new HashMap<>();

	private String key(Book b, User u) { return b.getId() + ":" + u.getId(); }

	public void assign(Book b, User u) {
		bookUsers.computeIfAbsent(b, k -> new HashSet<>()).add(u);
		userBooks.computeIfAbsent(u, k -> new HashSet<>()).add(b);
		currentPageByPair.putIfAbsent(key(b, u), 1);
		lastAccessedByPair.put(key(b, u), new Date());
	}

	public void updateProgress(Book b, User u, int page) {
		currentPageByPair.put(key(b, u), page);
		lastAccessedByPair.put(key(b, u), new Date());
	}

	public Set<User> getUsers(Book b) { return bookUsers.getOrDefault(b, Collections.emptySet()); }
	public Set<Book> getBooks(User u) { return userBooks.getOrDefault(u, Collections.emptySet()); }
}

class OnlineReaderSystem {
	private final Books books;
	private final Users users;
	private final BookUsers bookUsers;

	public OnlineReaderSystem(Books books, Users users, BookUsers bookUsers) {
		this.books = books;
		this.users = users;
		this.bookUsers = bookUsers;
	}

	public void assignBookToUser(Book b, User u) { bookUsers.assign(b, u); }
	public Book searchBook(long id) { return books.find(id); }
	public User searchUser(long id) { return users.find(id); }
}

7. Python Implementation

from __future__ import annotations
from dataclasses import dataclass
from typing import Set, Dict
from datetime import datetime

@dataclass(frozen=True)
class Book:
	id: int
	title: str
	author: str

@dataclass(frozen=True)
class User:
	id: int
	username: str
	email: str
	account_type: int

class Books:
	def __init__(self) -> None:
		self.books: Set[Book] = set()

	def add_book(self, id: int, title: str, author: str) -> None:
		self.books.add(Book(id, title, author))

	def update(self, book: Book) -> None:
		pass

	def delete(self, book: Book) -> None:
		self.books.discard(book)

	def find(self, id: int) -> Book | None:
		for b in self.books:
			if b.id == id:
				return b
		return None

class Users:
	def __init__(self) -> None:
		self.users: Set[User] = set()

	def add_user(self, id: int, username: str, email: str, account_type: int) -> None:
		self.users.add(User(id, username, email, account_type))

	def renew_membership(self, user: User) -> None:
		pass

	def find(self, id: int) -> User | None:
		for u in self.users:
			if u.id == id:
				return u
		return None

class BookUsers:
	def __init__(self) -> None:
		self.book_users: Dict[Book, Set[User]] = {}
		self.user_books: Dict[User, Set[Book]] = {}
		self.current_page_by_pair: Dict[tuple[int, int], int] = {}
		self.last_accessed_by_pair: Dict[tuple[int, int], datetime] = {}

	def assign(self, book: Book, user: User) -> None:
		self.book_users.setdefault(book, set()).add(user)
		self.user_books.setdefault(user, set()).add(book)
		key = (book.id, user.id)
		self.current_page_by_pair.setdefault(key, 1)
		self.last_accessed_by_pair[key] = datetime.now()

	def update_progress(self, book: Book, user: User, page: int) -> None:
		key = (book.id, user.id)
		self.current_page_by_pair[key] = page
		self.last_accessed_by_pair[key] = datetime.now()

	def get_users(self, book: Book) -> Set[User]:
		return self.book_users.get(book, set())

	def get_books(self, user: User) -> Set[Book]:
		return self.user_books.get(user, set())

class OnlineReaderSystem:
	def __init__(self, books: Books, users: Users, mapping: BookUsers) -> None:
		self.books = books
		self.users = users
		self.mapping = mapping

	def assign_book_to_user(self, book: Book, user: User) -> None:
		self.mapping.assign(book, user)

	def search_book(self, id: int) -> Book | None:
		return self.books.find(id)

	def search_user(self, id: int) -> User | None:
		return self.users.find(id)
public class Book {

	private long id;
	private String title;
	private long authorId;
	
	public long getID() {
		return ID;
	}

	public Book(long id, String title, long authorId) {
		// ...
	}
}

User Class


public class User {

	private long ID;

	private String username;
	private String email;
	private int accountType;

	public long getID() {
		return ID;
	}

	public Book searchLibrary(long id) {
		return Book.find(id);
	}

	public void renewMembership() {}
}

Now we need an management system, which can manage them. The users and books have m:n relationship between them, i.e. multiple users can have same book and same user can have multiple books. To solve this we will have new table in database, which will have book to user mapping, with foreign key reference to user and book id, both being the composite key.

The reader system will look like this.

Managing books

public class Books {

	private Set<Book> books;

	public void addBook(long iD, String details) {
		books.add(new Book(iD, details));
	}

	public void update() {}

	public void delete(Book b) {
		books.remove(b);
	}

	public Book find(long id) {
		for (Book b: books)
			if (b.getID() == id)
				return b;
		return null;
	}
}

Managing users

public class Users {

	private Set<User> users;

	public Book searchLibrary(long id) {
		return Book.find(id);
	}

	public void renewMembership() {}

	public static User find(long ID) {
		for (User u: users) {
			if (u.getID() == ID)
				return u;
		}

		return null;
	}

	public void addUser(long ID, String details,
		int accountType) {
		users.add(new User(ID, details, accountType));
	}

	public User(long iD, String details, int accountType) {}
}

Now we need the 1:1 mapping between book and user, which is managed by Book User, and online reader finally manages everything :

public class BookUsers {

	private HashMap < Book, Set<User>> bookUsers;

	private HashMap < User, Set<Book>> userBooks;

	private void addUserToBookUserList(Book b, User u) {
		if (bookUsers.containsKey(b)) {
			Set<User> users = bookUsers.getValue(b);
			users.add(u);
		} else {
			bookUsers.add(b, new HashMap<User>() {
				u
			});
		}
	}

	private void addBookToUserBookList(Book b, User u) {
		if (userBooks.containsKey(u)) {
			Set<Book> books = userBooks.getValue(u);
			books.add(b);
		} else {
			userBooks.add(u, new HashMap<Book>() {
				b
			});
		}
	}

	public void assignBookToUser(Book b, User u) {
		addUserToBookUserList(b, u);
		addBookToUserBookList(b, u);

	}
}


public class OnlineReaderSystem {

	private Books books;

	private Users users;

	private BookUsers bookUsers;

	private HashMap < Book, Set<User>> bookUsers;

	private HashMap < User, Set<Book>> userBooks;

	public void assignBookToUser() {
		bookUsers.assignBookToUser(b, u);

	}

	public OnlineReaderSystem(Books books, Users users) {}

	public void listenRequest() {}

	public Book searchBook(long ID) {
		return books.find(ID);
	}

	public User searchUser(long ID) {
		return users.find(ID);
	}

	public void display() {}

}

8. ER Diagram

Unfortunately, BookUser will change when we take into account db relational model, as we dont have to keep things in-memory. But this is what I got. Here is Crowe's notation for entity relationship :

 erDiagram
     BOOK ||--o{ BOOK_USER : included_in
     BOOK {
         int id PK "Primary Key"
         string title "Book title"
         string author "Book author"
     }
     USER ||--o{ BOOK_USER : has
     USER {
         int id PK "Primary Key"
         string username "Unique username"
         string email "User's email address"
     }
     BOOK_USER {
         int user_id FK "Foreign Key to USER"
         int book_id FK "Foreign Key to BOOK"
         datetime last_accessed "Timestamp of last access"
         int current_page "User's current page"
     }

Comments