What L5 looks like
- Stores positions and highlights in a format-independent location, not pages, and explains why.
- Handles the device limit, download states, and stale offline position writes (timestamp check, furthest-read prompt).
- Makes purchases idempotent with a key and says where locking is needed.
Requirements
- A user has one account and a library of books they own.
- Buy a book from the catalog. Buying is idempotent: a retry or double click never charges twice, and buying an owned book is a no-op.
- Download an owned book to a registered device in the best format that device supports (KFX on a Kindle, EPUB in an app, PDF for fixed-layout titles).
- A license allows a book on at most N devices at once (say 6). Removing a download frees a slot; you still own the book.
- Sync reading position per book across devices: resume from the latest position, and offer to jump to the furthest position reached.
- Highlights with optional notes: add, list, delete; they sync across devices and formats.
- Out of scope: search and recommendations, the rendering engine, DRM crypto, subscriptions, lending, refunds (hooks under Extensions).
Core objects
-
Book— immutable catalog entry: id, title, price, one file URL perFormat(enum:KFX,EPUB,PDF), length in locations. -
FormatAdapter(ABC) — maps the format-independent location to a format's own unit (a PDF page). Reflowable formats address by location directly. -
Device— a registered reader: supported formats in preference order, aDownloadState(enum) per book. -
Entitlement— proof the user owns a book, plus the devices holding a copy. -
Position— latest reading location for a book, with timestamp and source device. -
Highlight— a location range, quoted text, optional note, tombstone flag. -
PaymentGateway(ABC) — charges money with an idempotency key. -
Library— the per-user aggregate and single thread-safe entry point for purchases, downloads, positions and highlights.
The key decision: positions and highlights are stored in locations (a stable index over the book's canonical text), never pages. Pages depend on font size, screen and format; locations are identical everywhere, so sync needs no translation.
Class design
{"type":"excalidraw","elements":[
{"type":"rectangle","id":"library","x":340,"y":0,"width":240,"height":110,"label":{"text":"Library\nbuy, start_download\nsave_position\nadd_highlight"}},
{"type":"rectangle","id":"gateway","x":760,"y":10,"width":260,"height":90,"label":{"text":"PaymentGateway\n(abstract)\ncharge(user, cents, key)"}},
{"type":"rectangle","id":"entitlement","x":0,"y":250,"width":240,"height":70,"label":{"text":"Entitlement\nbook, device_ids"}},
{"type":"rectangle","id":"position","x":340,"y":250,"width":240,"height":70,"label":{"text":"Position\nlocation"}},
{"type":"rectangle","id":"highlight","x":680,"y":250,"width":240,"height":70,"label":{"text":"Highlight\nstart, end"}},
{"type":"rectangle","id":"book","x":0,"y":460,"width":240,"height":90,"label":{"text":"Book\nbook_id, files\ntotal_locations"}},
{"type":"rectangle","id":"device","x":420,"y":460,"width":260,"height":90,"label":{"text":"Device\ndevice_id, supported\npreferred_format(book)"}},
{"type":"rectangle","id":"adapter","x":1100,"y":250,"width":260,"height":90,"label":{"text":"FormatAdapter\n(abstract)\nfrom_location(loc, book)"}},
{"type":"rectangle","id":"fixed","x":1100,"y":460,"width":220,"height":70,"label":{"text":"FixedPageAdapter"}},
{"type":"arrow","id":"lib-gw","x":580,"y":55,"width":180,"height":0,"start":{"id":"library"},"end":{"id":"gateway"},"label":{"text":"uses"}},
{"type":"arrow","id":"lib-ent","x":380,"y":110,"width":-260,"height":140,"start":{"id":"library"},"end":{"id":"entitlement"},"label":{"text":"has"}},
{"type":"arrow","id":"lib-pos","x":460,"y":110,"width":1,"height":140,"start":{"id":"library"},"end":{"id":"position"},"label":{"text":"has"}},
{"type":"arrow","id":"lib-hl","x":540,"y":110,"width":260,"height":140,"start":{"id":"library"},"end":{"id":"highlight"},"label":{"text":"has"}},
{"type":"arrow","id":"ent-book","x":120,"y":320,"width":1,"height":140,"start":{"id":"entitlement"},"end":{"id":"book"},"label":{"text":"refers to"}},
{"type":"arrow","id":"dev-book","x":420,"y":505,"width":-180,"height":0,"start":{"id":"device"},"end":{"id":"book"},"label":{"text":"downloads"}},
{"type":"arrow","id":"fixed-adapter","x":1210,"y":460,"width":20,"height":-120,"start":{"id":"fixed"},"end":{"id":"adapter"},"label":{"text":"extends"}}
]}
Key flows
Buy. Library.buy returns the existing Entitlement if the book is owned. Otherwise it calls PaymentGateway.charge with key user_id:book_id, so two concurrent buys charge once. On success it stores the entitlement with setdefault under the lock, so a racing thread gets the same object. The charge happens outside the lock so a slow gateway does not block reading.
Download. start_download checks the entitlement (PermissionError if missing), refuses a new device when device_ids is full, picks device.preferred_format(book), reserves the slot and returns the file URL. The device reports back with finish_download(ok); failure releases the slot. remove_download frees the slot but keeps ownership.
| From | Event | To |
|---|---|---|
| NOT_DOWNLOADED / FAILED | start_download, slot free | DOWNLOADING |
| DOWNLOADING | finish_download(ok=True) | DOWNLOADED |
| DOWNLOADING | finish_download(ok=False) | FAILED (slot released) |
| DOWNLOADED | remove_download | NOT_DOWNLOADED |
Sync position. The device converts its page to a location and calls save_position. The write wins only if its timestamp is newer (last writer wins), so an offline device replaying old positions cannot move the reader backwards. furthest tracks the maximum location; if the latest position is behind it, the device offers "Go to furthest page read". On open, a device reads position() and converts with its FormatAdapter.
Highlights. Stored in locations, so a highlight made in the PDF shows in the KFX copy. Delete writes a tombstone, so a device that syncs later learns of the delete instead of re-uploading the highlight.
Code
from __future__ import annotations
import threading, time, uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
class Format(Enum):
KFX = "kfx"; EPUB = "epub"; PDF = "pdf"
class DownloadState(Enum):
NOT_DOWNLOADED = 1; DOWNLOADING = 2; DOWNLOADED = 3; FAILED = 4
@dataclass(frozen=True)
class Book:
book_id: str; title: str; price_cents: int
files: dict[Format, str]; total_locations: int # format -> storage URL
class FormatAdapter(ABC): # strategy: format unit <-> location
@abstractmethod
def from_location(self, location: int, book: Book) -> int: ...
class FixedPageAdapter(FormatAdapter): # PDF: map to pages proportionally
def __init__(self, pages: int) -> None: self.pages = pages
def from_location(self, location: int, book: Book) -> int:
return min(self.pages, location * self.pages // book.total_locations + 1)
class PaymentGateway(ABC):
@abstractmethod
def charge(self, user_id: str, amount_cents: int, idem_key: str) -> bool: ...
@dataclass
class Device:
device_id: str; supported: list[Format] # formats in preference order
downloads: dict[str, DownloadState] = field(default_factory=dict)
def preferred_format(self, book: Book) -> Format:
for f in self.supported:
if f in book.files:
return f
raise ValueError(f"device {self.device_id} cannot open {book.title}")
@dataclass
class Entitlement:
book: Book; purchased_at: float
device_ids: set[str] = field(default_factory=set)
@dataclass(frozen=True)
class Position:
location: int; updated_at: float; device_id: str
@dataclass
class Highlight:
highlight_id: str; start: int; end: int; text: str
note: str | None = None
deleted: bool = False
class Library:
MAX_DEVICES_PER_BOOK = 6
def __init__(self, user_id: str, payments: PaymentGateway) -> None:
self.user_id, self._payments = user_id, payments
self._entitlements: dict[str, Entitlement] = {}
self._positions: dict[str, Position] = {}; self._furthest: dict[str, int] = {}
self._highlights: dict[str, dict[str, Highlight]] = {}
self._lock = threading.Lock()
def buy(self, book: Book) -> Entitlement:
with self._lock:
if book.book_id in self._entitlements:
return self._entitlements[book.book_id]
key = f"{self.user_id}:{book.book_id}" # gateway dedupes retries and races
if not self._payments.charge(self.user_id, book.price_cents, key):
raise RuntimeError("payment declined")
with self._lock:
return self._entitlements.setdefault(book.book_id, Entitlement(book, time.time()))
def _owned(self, book_id: str) -> Entitlement:
if book_id not in self._entitlements:
raise PermissionError(f"{book_id} not in library")
return self._entitlements[book_id]
def start_download(self, book_id: str, device: Device) -> str:
with self._lock:
ent = self._owned(book_id)
fmt = device.preferred_format(ent.book)
if device.device_id not in ent.device_ids:
if len(ent.device_ids) >= self.MAX_DEVICES_PER_BOOK:
raise PermissionError("device limit reached for this book")
ent.device_ids.add(device.device_id) # reserve the slot
device.downloads[book_id] = DownloadState.DOWNLOADING
return ent.book.files[fmt] # device fetches this URL
def finish_download(self, book_id: str, device: Device, ok: bool) -> None:
with self._lock:
device.downloads[book_id] = DownloadState.DOWNLOADED if ok else DownloadState.FAILED
if not ok:
self._owned(book_id).device_ids.discard(device.device_id)
def remove_download(self, book_id: str, device: Device) -> None:
with self._lock:
self._owned(book_id).device_ids.discard(device.device_id)
device.downloads[book_id] = DownloadState.NOT_DOWNLOADED
def save_position(self, book_id: str, device_id: str, location: int, ts: float) -> Position:
with self._lock:
location = max(0, min(location, self._owned(book_id).book.total_locations))
cur = self._positions.get(book_id)
if cur is None or ts > cur.updated_at: # last writer wins
self._positions[book_id] = Position(location, ts, device_id)
self._furthest[book_id] = max(self._furthest.get(book_id, 0), location)
return self._positions[book_id]
def position(self, book_id: str) -> tuple[Position | None, int]:
with self._lock:
return self._positions.get(book_id), self._furthest.get(book_id, 0)
def add_highlight(self, book_id: str, start: int, end: int, text: str,
note: str | None = None) -> Highlight:
with self._lock:
if not 0 <= start < end <= self._owned(book_id).book.total_locations:
raise ValueError("invalid highlight range")
h = Highlight(str(uuid.uuid4()), start, end, text, note)
self._highlights.setdefault(book_id, {})[h.highlight_id] = h
return h
def delete_highlight(self, book_id: str, highlight_id: str) -> None:
with self._lock:
h = self._highlights.get(book_id, {}).get(highlight_id)
if h:
h.deleted = True # tombstone, synced to devices
def highlights(self, book_id: str) -> list[Highlight]:
with self._lock:
live = (h for h in self._highlights.get(book_id, {}).values() if not h.deleted)
return sorted(live, key=lambda h: h.start)
class FakeGateway(PaymentGateway):
def charge(self, user_id: str, amount_cents: int, idem_key: str) -> bool: return True
if __name__ == "__main__":
book = Book("b1", "Dune", 999, {Format.KFX: "s3://b1.kfx", Format.PDF: "s3://b1.pdf"}, 8000)
lib = Library("u1", FakeGateway())
lib.buy(book); lib.buy(book) # second buy is a no-op
kindle, tablet = Device("k1", [Format.KFX]), Device("t1", [Format.EPUB, Format.PDF])
print(lib.start_download("b1", kindle), lib.start_download("b1", tablet)) # kfx, pdf
lib.finish_download("b1", kindle, ok=True); lib.finish_download("b1", tablet, ok=True)
lib.save_position("b1", "k1", 4200, ts=100.0)
lib.save_position("b1", "t1", 1000, ts=50.0) # stale offline write: ignored
pos, furthest = lib.position("b1")
print(pos.location, FixedPageAdapter(400).from_location(pos.location, book)) # 4200 211
lib.add_highlight("b1", 4200, 4230, "Fear is the mind-killer.", note="classic")
print([h.text for h in lib.highlights("b1")])
Design patterns
-
Strategy —
FormatAdapter. Each format converts between its own unit and the shared location space. A new format adds one adapter; library, sync and highlights stay unchanged. -
Aggregate / facade —
Libraryis the only way to change a user's books. It enforces the invariants (ownership before download, device limit, monotonic positions) in one place behind one lock. -
Dependency injection behind an interface —
PaymentGatewaylets tests use a fake and production swap processors; the idempotency key is part of the contract.
Extensions
-
Kindle Unlimited / lending: give
Entitlementasource(PURCHASE, SUBSCRIPTION, LOAN) andexpires_at;_ownedchecks expiry. A loan moves the entitlement between libraries for its term. -
Notes, bookmarks, popular highlights: generalize
Highlightinto anAnnotationwith akind. Popular highlights are counts per(book_id, start, end)computed by a background job, not inLibrary. - New edition shifts locations: ship a location map per edition version and migrate positions and highlights through it, anchoring on quoted text as a fallback.
Interviewer expectations
Mid-level (L4)
- Separates catalog
Bookfrom ownership (Entitlement) and from the per-device copy. - Implements buy, download, save position and add highlight with basic validation, and notices that buying twice must not charge twice.
Senior (L5)
- Stores positions and highlights in a format-independent location, not pages, and explains why.
- Handles the device limit, download states, and stale offline position writes (timestamp check, furthest-read prompt).
- Makes purchases idempotent with a key and says where locking is needed.
Staff (L6+)
- Reasons about sync semantics: last-writer-wins for positions, an append-only log with tombstones for highlights, clock skew (server timestamps or version counters).
- Plans for edition changes and subscriptions/loans, and separates this object model from the services behind it (payments, content delivery, DRM licenses).
Make it yours
Remix it: write each part yourself, grade it against this answer, then let the coach ask you follow-ups.
