<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Product Development on Haseeb Annadamban</title><link>https://haseebeqx.com/tags/product-development/</link><description>Recent content in Product Development on Haseeb Annadamban</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>Haseeb Annadamban</copyright><lastBuildDate>Sun, 13 Sep 2026 08:20:06 +0000</lastBuildDate><atom:link href="https://haseebeqx.com/tags/product-development/index.xml" rel="self" type="application/rss+xml"/><item><title>Building an Anki inspired math and reading practice app</title><link>https://haseebeqx.com/case-studies/ltb-cards-fsrs-learning-platform/</link><pubDate>Sun, 13 Sep 2026 08:20:06 +0000</pubDate><guid>https://haseebeqx.com/case-studies/ltb-cards-fsrs-learning-platform/</guid><description>&lt;h2 id="at-a-glance"&gt;At a glance&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Turn an Anki-inspired study method into a focused product that parents could use with children for daily math and reading practice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Product architecture, adaptive scheduling, study queues, reusable card content, learner profiles, phone authentication, localization, and deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A free production Rails application with math and reading decks, adaptive review intervals, phonics support, and an English/Spanish interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delivery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Core product engineering for Learn To Be, from the initial data model through launch and production fixes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;p&gt;Learn To Be wanted a focused product that parents could use with children for daily math and reading practice. The intended routine was deliberately small: choose a deck, ask the learner to recall an answer, reveal it, rate the response, and return when the right cards are due again.&lt;/p&gt;
&lt;p&gt;The product uses spaced repetition, a study method that brings material back for review at increasing intervals as it becomes easier to remember. Its scheduler is based on the Free Spaced Repetition Scheduler (FSRS), an open-source algorithm that uses the learner&amp;rsquo;s recall ratings to estimate memory strength and decide when each card should return.&lt;/p&gt;
&lt;p&gt;The simple interface concealed several connected engineering problems. The application needed to decide when every card should return, mix cards at different stages without losing or duplicating them, keep each child&amp;rsquo;s progress separate, and support very different material—from multiplication facts to words with audio and phoneme breakdowns. It also needed to remain practical on a phone and understandable in both English and Spanish.&lt;/p&gt;
&lt;p&gt;I built the core application in Rails 8 and carried it through to the production service at &lt;a href="https://ltb.cards/"&gt;ltb.cards&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-i-built"&gt;What I built&lt;/h2&gt;
&lt;h3 id="implemented-adaptive-scheduling-with-fsrs"&gt;Implemented adaptive scheduling with FSRS&lt;/h3&gt;
&lt;p&gt;The scheduler is an in-application Ruby implementation of the &lt;a href="https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm"&gt;Free Spaced Repetition Scheduler (FSRS)&lt;/a&gt;. Instead of giving every learner a fixed sequence of delays, it maintains a memory model for each card using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;retrievability&lt;/strong&gt;, the estimated likelihood that the learner can recall the card now;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stability&lt;/strong&gt;, how long that memory is expected to remain retrievable; and&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;difficulty&lt;/strong&gt;, which changes in response to the learner&amp;rsquo;s ratings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After the answer is revealed, the interface shows four choices—Again, Hard, Good, and Easy—and previews the interval produced by each one. A response updates the card&amp;rsquo;s memory state and calculates its next due date. The default target retention is 90%, intervals are bounded between one day and 36,500 days after graduation, and longer intervals receive a small amount of fuzzing so many cards do not repeatedly become due together.&lt;/p&gt;
&lt;p&gt;The implementation treats a card as a state machine rather than only a date. New material moves through one- and ten-minute learning steps before graduating to review. A failed mature card enters a ten-minute relearning step, while its stability and difficulty are updated from the FSRS formulas. This made short-term practice and long-term scheduling part of one explicit model.&lt;/p&gt;
&lt;p&gt;Every response is also stored as a review event with its rating, timestamp, resulting interval, resulting difficulty value, and time taken. That leaves an audit trail for understanding the scheduler&amp;rsquo;s decisions and makes future analysis possible.&lt;/p&gt;
&lt;h3 id="made-the-queue-a-single-source-of-truth"&gt;Made the queue a single source of truth&lt;/h3&gt;
&lt;p&gt;Scheduling a date is only half of a study system. The application also has to choose the next card correctly.&lt;/p&gt;
&lt;p&gt;I separated queue construction from card selection. &lt;code&gt;CardQueueService&lt;/code&gt; defines the database relations for new, learning, review, and relearning cards; both the next-card selector and the on-screen counters use those same relations. This change removed an earlier class of bugs where the displayed counts and the actual study sequence could disagree.&lt;/p&gt;
&lt;p&gt;The selector prioritizes learning cards that are due now, then mature reviews, relearning cards, and the day&amp;rsquo;s new material. When no other work remains, it can bring forward a learning card due within the next 20 minutes. Daily new-card limits prevent a learner from taking on too much material at once, and all day boundaries are calculated in the learner&amp;rsquo;s own timezone.&lt;/p&gt;
&lt;p&gt;A particularly subtle product rule was when to create progress. A card schedule is created only when its answer is revealed, not merely when its question is displayed. Reloading or leaving an unseen card therefore does not consume the learner&amp;rsquo;s daily allowance or incorrectly mark the card as started.&lt;/p&gt;
&lt;h3 id="designed-content-that-could-grow-beyond-flashcard-text"&gt;Designed content that could grow beyond flashcard text&lt;/h3&gt;
&lt;p&gt;I used a note-based model inspired by Anki, separating content from presentation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a &lt;strong&gt;note type&lt;/strong&gt; defines fields and one or more templates;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;note&lt;/strong&gt; stores the field values in PostgreSQL JSONB; and&lt;/li&gt;
&lt;li&gt;one or more &lt;strong&gt;cards&lt;/strong&gt; select templates from that note.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One vocabulary note can therefore produce recognition and production cards without duplicating its source content. The same model handles arithmetic, formulas, words, explanations, images, and audio.&lt;/p&gt;
&lt;p&gt;Templates use &lt;code&gt;{{field_name}}&lt;/code&gt; placeholders and allow a controlled subset of HTML and Tailwind classes. The rendering component substitutes values, applies consistent defaults, expands approved custom elements, and sanitizes the result. Media stored in S3 receives a short-lived signed URL, with generated URLs cached to avoid repeating that work on every render.&lt;/p&gt;
&lt;p&gt;For reading decks, I added a custom &lt;code&gt;&amp;lt;word-breakdown&amp;gt;&lt;/code&gt; element. It looks up a word in the Carnegie Mellon Pronouncing Dictionary and maps its phonemes back to visible grapheme groups using a set of position- and context-aware rules. A word can then be presented as the letter groups and sounds a learner should practise, including common multi-letter sounds and silent letters. Normal- and slow-speed audio can sit alongside the same card.&lt;/p&gt;
&lt;h3 id="kept-the-learner-experience-focused"&gt;Kept the learner experience focused&lt;/h3&gt;
&lt;p&gt;A single account can contain multiple learner profiles, each with independent deck choices, schedules, review history, daily limits, and timezone. Passwordless phone authentication keeps sign-in concise, while an administrative data portal supports the client&amp;rsquo;s operational work.&lt;/p&gt;
&lt;p&gt;The interface is responsive and uses Turbo for the answer reveal, so the question can transition into feedback without turning the study flow into a client-side application. The complete learner-facing experience is localized in English and Spanish.&lt;/p&gt;
&lt;h2 id="result"&gt;Result&lt;/h2&gt;
&lt;p&gt;The result is a deployed learning product rather than a scheduling prototype. &lt;a href="https://ltb.cards/"&gt;LTB Cards&lt;/a&gt; combines the algorithm, queue semantics, reusable content system, authentication, profiles, localization, administration, and production infrastructure required to run daily study sessions.&lt;/p&gt;
&lt;p&gt;The architecture also gave Learn To Be room to expand the curriculum without rebuilding the study engine. Deck-generation tasks produce multiple levels of addition, subtraction, multiplication, division, and reading practice. New card formats can be introduced through templates, while specialized teaching interactions can be added as controlled custom components.&lt;/p&gt;
&lt;p&gt;The most important outcome was preserving a simple product surface over a non-trivial domain model. A parent sees one card and four clear choices; behind that interaction, the application records the attempt, updates the learner&amp;rsquo;s memory model, chooses a future interval, maintains daily limits, and places the card into the correct queue.&lt;/p&gt;
&lt;h2 id="validation-and-iteration"&gt;Validation and iteration&lt;/h2&gt;
&lt;p&gt;The FSRS specification covers defaults, custom scheduler options, each transition between learning, review, and relearning, retrievability, and minimum and maximum intervals. Model specs cover schedules, decks, profiles, notes, note types, and review records. Queue behavior received dedicated regression coverage for due cards and learn-ahead boundaries.&lt;/p&gt;
&lt;p&gt;Production iteration exposed the cases that are easy to miss in a first implementation: queue counters diverging from selection logic, new cards being consumed after a reload, incorrect timezone use, daily statistics failing to reset at midnight, and relearning behavior after a lapse. I addressed these as explicit domain rules rather than view-level patches, culminating in the shared queue service used by both selection and reporting.&lt;/p&gt;
&lt;p&gt;The application remains live at &lt;a href="https://ltb.cards/"&gt;ltb.cards&lt;/a&gt;.&lt;/p&gt;</description></item></channel></rss>