Product design

Readwise Reader: put saved articles to use again

Readwise Reader is a reading tool for people who regularly work with web articles, PDFs, and newsletters. It connects saved material, annotations, and later review so readers can return to what mattered.

Break down the problem and try your own design and implementation first. Then explore the reasoning to check states, edge cases, trade-offs, and ways to test your approach. The guide offers a reasoning path, not a complete implementation.

01
CODE EXERCISEIntermediate

Keep late events from restoring a deleted article

This is an exercise protocol, not Readwise's internal implementation. Each event has a docId, a positive integer version, and an action: save, archive, or delete. Larger versions are later operations for the same document. Events may arrive out of order or more than once. The input guarantees the same action for a given docId/version pair.

  1. Implement a state processor. Initially there are no documents. save sets a document to inbox, archive to archived, and delete to deleted. Each valid event is a complete state instruction: receiving archive first still produces archived.
  2. Output each docId's highest version and current state. Repeated or older versions must not overwrite it. A deleted document must retain its highest version until a higher-version save or archive changes its state.
  3. Explain the result of A/1/save → A/3/delete → A/2/archive → A/3/delete. Then add A/4/save and explain when the document may reappear.
  4. Submit the processor, state explanation, and tests. Cover empty input, interleaved documents, duplicates, and deletion arriving before an older save. Do not implement real network requests.
Explore the reasoning
  1. Compare versions within each document, not across the whole event stream. A's version must not block B's update. Each docId needs its own highest version and state. The guaranteed agreement between same-version actions lets you ignore duplicates without inventing a conflict-resolution rule.

  2. Walk through the example. A/1 produces inbox; A/3 produces deleted. Late A/2 is older and cannot change it to archived, and repeated A/3 changes nothing. Only the subsequent A/4/save returns it to inbox. This trace also shows why deletion must retain version information.

  3. Use a minimal counterexample: A/3/delete followed by A/1/save. If deletion erases every trace of the state, the old save can incorrectly restore the article. Keeping deletion as a versioned state prevents that. This conclusion depends on the exercise's version rules; it is not a complete real-world sync design.

  4. Check that each output matches the state set by that document's highest-version event. Reordering the same events or inserting duplicates should preserve the result. Empty input produces an empty map, and documents update independently. Explain the storage cost of retaining deletion records; the exercise provides no safe condition for removing them.

02
FRONTEND EXERCISEIntermediate

Separate browsing cards from completing a review

Build a hypothetical highlight-review interface. The component accepts an array of cards, including an empty array. The main example has cards A, B, and C: A is initially visible and none are processed. Each card contains a source excerpt and title; no Reader connection is required.

  1. Previous/Next only changes the current position. Disable it at the boundaries and do not wrap around. Keep or Discard processes a card. After success, mark it processed but stay on the same card so the user can navigate deliberately.
  2. Use a controllable asynchronous save simulator that can succeed or fail. During saving, disable navigation and both processing buttons. Successfully processed cards cannot be submitted again. Failure leaves a card unprocessed and allows retry. Progress counts distinct cards processed successfully.
  3. Show the current card, processed count, saving state, and error feedback. Buttons must work with a keyboard, and assistive technology must be able to detect state changes.
  4. Submit the page and interaction tests. Keep on A fails, then succeeds on retry; navigate through B to C. The result must show C and progress 1/3. Also test rapid double-clicks, an empty array, and end-of-list navigation. Empty input must show a no-content state and disable navigation and processing.
Explore the reasoning
  1. Separate current position, per-card processing state, and current request state. Seeing C does not mean A and B were processed. Sharing one counter for navigation and completion would report incorrect progress. For an empty array, render the empty state and disable actions instead of accessing a nonexistent first card.

  2. Trace failure explicitly. When A's Keep request fails, progress remains 0/3 and A remains actionable. A successful retry changes it to 1/3. Navigating through B to C adds nothing. This sequence exposes state confusion more effectively than checking whether each button can be clicked.

  3. Disabling navigation while saving prevents the user from switching cards before the request finishes. The submission handler must still reject a second in-flight request, because a rapid second click may arrive before disabled controls render. Store success by card identity rather than inferring it from the current position. On failure, restore actions and keep the user's context.

  4. Use native buttons and clear names for keyboard access; do not communicate errors through red alone. Announce save results through a status region that assistive technology can detect, while preserving sensible focus. Updating progress should not steal focus. Test repeatable failures, successes, and repeated triggers, and explain the waiting cost of disabling navigation and why this exercise uses that simplification.