Design Amazon Kindle (object-oriented)

0
Remix

Model an e-reader library: buy and download books, keep several formats, track reading position across devices, and support highlights.

AmazonCodingEasy

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.
Interviewer expectations for every level ↓

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 per Format (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, a DownloadState (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 — Library is 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 — PaymentGateway lets tests use a fake and production swap processors; the idempotency key is part of the contract.

Extensions

  • Kindle Unlimited / lending: give Entitlement a source (PURCHASE, SUBSCRIPTION, LOAN) and expires_at; _owned checks expiry. A loan moves the entitlement between libraries for its term.
  • Notes, bookmarks, popular highlights: generalize Highlight into an Annotation with a kind. Popular highlights are counts per (book_id, start, end) computed by a background job, not in Library.
  • 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 Book from 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.