OOD - Online Book Reader System
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" }