Cracking The Coding Interview Together – Problem 7.5: Online Book Reader
At first glance, designing an online book reader seems straightforward: you store some text files, fetch a book by its ID, and display a page on screen. However, when we transition from a simple local reader app to a concurrent, multi-user system like Kindle Cloud Reader or Google Play Books, state management becomes our primary bottleneck. How do we cleanly handle active reading sessions? How do we ensure fast lookup times without overwhelming memory when thousands of users are reading simultaneously? How do we gracefully decouple user accounts from the rendering engine?
In this post, we are going to work through Problem 7.5: Online Book Reader from Cracking the Coding Interview. Together, we will walk through the object-oriented design choices, evaluate data structure trade-offs, and implement a robust, thread-safe Java solution that models a real-world digital reading system.
Problem Statement
Design the data structures for an online book reader system. The system needs to support basic functionalities such as:
- Adding, searching, and removing books from the system library.
- Registering and managing user accounts.
- Tracking active reading sessions for users (including their active book and current page).
- Navigating pages (turning pages forward or backward).
- Displaying active reading information on screen.
Our operational goals are clean separation of concerns, high-efficiency lookups for active sessions, and thread safety for concurrent access.
Analyze the Problem Together
To design an elegant system, we need to decompose the problem into its fundamental domain models and manager services. Let's look at the entities involved:
- User: Represents account details, user preference settings, and subscription status.
- Book: Represents static book metadata (title, author, ID) and content (pages).
- Library: Manages the collection of all available books in the application.
- Display: Renders the active book and current page to the user interface.
- UserManager: Manages registered users and tracks who is currently online or actively reading.
The core design challenge—our "Aha!" moment—is deciding where the active reading state belongs. Should the User class hold a reference to its current Book and current page number? Or should the central application orchestrate active sessions?
If we attach active reading state directly to the User object, we couple user identity with session runtime state. If a user logs into multiple devices, or if our application scales to millions of users, holding session state permanently inside user domain objects creates unnecessary memory overhead. Instead, separating static domain objects (User, Book) from active runtime controllers (UserManager, Display, LibraryManager) gives us an extensible architecture.
| Design Strategy | Pros | Cons |
|---|---|---|
| Monolithic State in User Object | Simple setup; user carries active page and book directly. | Tight coupling; poor concurrency scaling; hard to support multi-device reads. |
| Decoupled Manager Services (Selected) | High extensibility; clean separation of concerns; easy to scale and make thread-safe. | Slightly higher initial boilerplate code. |
Questions for the Interviewer
When approaching object-oriented design problems in an interview, clarifying the scope upfront demonstrates senior engineering maturity. Here are 4 key questions we should ask our interviewer:
- Is this an in-memory single-node design or a distributed service? (For this problem, we will focus on an in-memory Object-Oriented Design that models scalable core components.)
- Can a user read multiple books at once, or is there only one active reading session per user? (We will assume one primary active session per user at any given time.)
- How is book content represented? (A list or array of page strings is standard and sufficient for modeling.)
- Are active session updates required to be thread-safe? (Yes, concurrent access should be handled safely.)
Think About the Solution
Let's map out our architecture cleanly using the Facade Pattern and Manager Pattern:
OnlineBookReaderSystemacts as the primary API Facade, wiring together our sub-managers.LibraryManagerholds an in-memoryConcurrentHashMap<Integer, Book>for $O(1)$ book retrieval and updates.UserManagermanages registered users via aConcurrentHashMap<Integer, User>and active reading sessions viaConcurrentHashMap<Integer, UserSession>.Displaymanages visual rendering state for the active reader view.BookandUserserve as core, immutable entity models.
Edge cases we must handle in code:
- Navigating past the last page or before page 0.
- Selecting a book that does not exist in the library.
- Attempting reading operations on an inactive session or unregistered user.
Write Code and Explain
Here is our complete, production-ready Java implementation:
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// --- ENTITY MODELS ---
class Book {
private final int bookId;
private final String title;
private final String author;
private final List<String> pages;
public Book(int bookId, String title, String author, List<String> pages) {
this.bookId = bookId;
this.title = title;
this.author = author;
this.pages = pages;
}
public int getBookId() { return bookId; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
public String getPage(int pageNumber) {
if (pageNumber < 0 || pageNumber >= pages.size()) {
return null;
}
return pages.get(pageNumber);
}
public int getTotalPages() { return pages.size(); }
}
class User {
private final int userId;
private String username;
private String email;
public User(int userId, String username, String email) {
this.userId = userId;
this.username = username;
this.email = email;
}
public int getUserId() { return userId; }
public String getUsername() { return username; }
public String getEmail() { return email; }
}
class ActiveSession {
private final User user;
private Book activeBook;
private int currentPage;
public ActiveSession(User user, Book activeBook) {
this.user = user;
this.activeBook = activeBook;
this.currentPage = 0;
}
public User getUser() { return user; }
public Book getActiveBook() { return activeBook; }
public void setActiveBook(Book activeBook) {
this.activeBook = activeBook;
this.currentPage = 0;
}
public int getCurrentPage() { return currentPage; }
public void setCurrentPage(int currentPage) { this.currentPage = currentPage; }
public boolean nextPage() {
if (currentPage + 1 < activeBook.getTotalPages()) {
currentPage++;
return true;
}
return false;
}
public boolean previousPage() {
if (currentPage > 0) {
currentPage--;
return true;
}
return false;
}
}
// --- MANAGERS ---
class LibraryManager {
private final Map<Integer, Book> books = new ConcurrentHashMap<>();
public void addBook(Book book) {
books.put(book.getBookId(), book);
}
public boolean removeBook(int bookId) {
return books.remove(bookId) != null;
}
public Book findBook(int bookId) {
return books.get(bookId);
}
}
class UserManager {
private final Map<Integer, User> users = new ConcurrentHashMap<>();
private final Map<Integer, ActiveSession> activeSessions = new ConcurrentHashMap<>();
public void registerUser(User user) {
users.put(user.getUserId(), user);
}
public User findUser(int userId) {
return users.get(userId);
}
public ActiveSession createSession(int userId, Book book) {
User user = users.get(userId);
if (user == null || book == null) return null;
ActiveSession session = new ActiveSession(user, book);
activeSessions.put(userId, session);
return session;
}
public ActiveSession getActiveSession(int userId) {
return activeSessions.get(userId);
}
public void endSession(int userId) {
activeSessions.remove(userId);
}
}
class Display {
private ActiveSession currentSession;
public void displaySession(ActiveSession session) {
this.currentSession = session;
refresh();
}
public void refresh() {
if (currentSession == null || currentSession.getActiveBook() == null) {
System.out.println("Display: No active reading session.");
return;
}
Book book = currentSession.getActiveBook();
int pageNum = currentSession.getCurrentPage();
String content = book.getPage(pageNum);
System.out.println("========================================");
System.out.println("Reading: " + book.getTitle() + " by " + book.getAuthor());
System.out.println("Page " + (pageNum + 1) + " of " + book.getTotalPages());
System.out.println("----------------------------------------");
System.out.println(content);
System.out.println("========================================");
}
}
// --- FACADE ORCHESTRATOR ---
public class OnlineBookReader {
private final LibraryManager libraryManager;
private final UserManager userManager;
private final Display display;
public OnlineBookReader() {
this.libraryManager = new LibraryManager();
this.userManager = new UserManager();
this.display = new Display();
}
public LibraryManager getLibraryManager() { return libraryManager; }
public UserManager getUserManager() { return userManager; }
public Display getDisplay() { return display; }
public void setActiveUserAndBook(int userId, int bookId) {
Book book = libraryManager.findBook(bookId);
if (book == null) {
System.out.println("Error: Book ID " + bookId + " not found.");
return;
}
ActiveSession session = userManager.createSession(userId, book);
if (session != null) {
display.displaySession(session);
} else {
System.out.println("Error: User ID " + userId + " not found.");
}
}
public void turnPageForward(int userId) {
ActiveSession session = userManager.getActiveSession(userId);
if (session != null && session.nextPage()) {
display.refresh();
} else {
System.out.println("Cannot turn to the next page.");
}
}
public void turnPageBackward(int userId) {
ActiveSession session = userManager.getActiveSession(userId);
if (session != null && session.previousPage()) {
display.refresh();
} else {
System.out.println("Cannot turn to the previous page.");
}
}
}
Code Walkthrough:
1. ConcurrentHashMap for Thread-Safe Lookups: Both LibraryManager and UserManager utilize ConcurrentHashMap. This guarantees safe multi-threaded updates when readers join, leave, or register simultaneously without locking the entire dictionary.
2. Decoupled ActiveSession Object: ActiveSession encapsulates the combination of user state, current book, and page pointer. This avoids bloating the core User domain entity.
3. Facade Design Pattern: The top-level OnlineBookReader class provides unified access methods (setActiveUserAndBook, turnPageForward), concealing internal subsystem routing from clients.
Time & Space Complexity
Let's analyze the time and space complexity for key operations in our system:
- Find / Add Book (LibraryManager): O(1) time complexity using
ConcurrentHashMapkey lookups. - User Login & Active Session Creation: O(1) time complexity to read from user map and store in session map.
- Page Turn (Forward / Backward): O(1) time complexity because page lookup from array list is index-based indexing.
- Overall Space Complexity: O(B.P + U + S), where B is the total number of books, P is average pages per book, U is total registered users, and S is active concurrent sessions.
Why This Problem Matters (The Senior Perspective)
In real-world distributed architectures, this problem directly mirrors how large-scale content platforms like Amazon Kindle or Spotify handle session synchronization (e.g., WhisperSync).
If we were transitioning this system to a microservice architecture at scale:
- State Synchronization: The
ActiveSessionstate would be backed by a distributed cache like Redis rather than local memory. Every page turn event writes lightweight offset coordinates (e.g.,{userId: 101, bookId: 405, page: 42}) with write-behind persistence to database storage. - Content Storage & Lazy Loading: Holding every page string of millions of books in JVM heap memory isn't realistic. In production,
Bookcontents reside in Object Storage (e.g., AWS S3 or CDN edge servers). The reader system lazily fetches chunks/pages in blocks as the user navigates close to current boundary markers.
Final Thoughts
By separating static entities (Book, User) from runtime session controllers (ActiveSession, UserManager, Display), we constructed a maintainable, clean, and thread-safe solution for the Online Book Reader problem.
How would you extend this system to support multi-device syncing or bookmarking features? Let us know your thoughts in the comments below, and happy coding!
Member discussion