<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thibaut Hennau</title>
    <description>The latest articles on DEV Community by Thibaut Hennau (@thibaut_hennau).</description>
    <link>https://dev.to/thibaut_hennau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3933436%2F6dbde2a5-2e04-4d96-9557-43949b38790f.jpg</url>
      <title>DEV Community: Thibaut Hennau</title>
      <link>https://dev.to/thibaut_hennau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thibaut_hennau"/>
    <language>en</language>
    <item>
      <title>Russian has 12 forms per noun. My SRS tracks each one separately</title>
      <dc:creator>Thibaut Hennau</dc:creator>
      <pubDate>Fri, 15 May 2026 15:16:38 +0000</pubDate>
      <link>https://dev.to/thibaut_hennau/russian-has-12-forms-per-noun-my-srs-tracks-each-one-separately-3d3c</link>
      <guid>https://dev.to/thibaut_hennau/russian-has-12-forms-per-noun-my-srs-tracks-each-one-separately-3d3c</guid>
      <description>&lt;p&gt;I built a spaced repetition system for Russian, and the first thing I had to throw out was the assumption that an SRS item is a word.&lt;/p&gt;

&lt;p&gt;For Russian, that doesn't hold.&lt;/p&gt;

&lt;p&gt;Take the word for "house": дом (dom). In a sentence, you might see any of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;дом (dom) - nominative singular&lt;/li&gt;
&lt;li&gt;дома (doma) - genitive singular&lt;/li&gt;
&lt;li&gt;дому (domu) - dative singular&lt;/li&gt;
&lt;li&gt;дом (dom) - accusative singular&lt;/li&gt;
&lt;li&gt;домом (domom) - instrumental singular&lt;/li&gt;
&lt;li&gt;доме (dome) - prepositional singular&lt;/li&gt;
&lt;li&gt;дома (doma) - nominative plural&lt;/li&gt;
&lt;li&gt;домов (domov) - genitive plural&lt;/li&gt;
&lt;li&gt;домам (domam) - dative plural&lt;/li&gt;
&lt;li&gt;дома (doma) - accusative plural&lt;/li&gt;
&lt;li&gt;домами (domami) - instrumental plural&lt;/li&gt;
&lt;li&gt;домах (domakh) - prepositional plural&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;12 forms. Some collide (дом appears twice; дома appears three times across different cases). Some are predictable from a paradigm, some aren't because of stress shift or fleeting vowels. And the catch: a learner who can recite the singular table cold might still freeze in the wild when they need the instrumental plural to say "with friends" (с друзьями, s druzyami).&lt;/p&gt;

&lt;p&gt;Anki gives you the card "дом → house." You answer correctly. The card schedules forward.&lt;/p&gt;

&lt;p&gt;Then a week later you misuse the genitive in a sentence, and your "дом" card is still green because, technically, you remembered the word. The form is what failed. The form is what needs the review.&lt;/p&gt;

&lt;p&gt;So when I was building &lt;a href="https://slova.be" rel="noopener noreferrer"&gt;Slova&lt;/a&gt;, I changed the unit of memory from "lexeme" to "form."&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model
&lt;/h2&gt;

&lt;p&gt;Most SRS implementations look like this (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;front&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;            &lt;span class="c1"&gt;# "дом"
&lt;/span&gt;    &lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;             &lt;span class="c1"&gt;# "house"
&lt;/span&gt;    &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;           &lt;span class="c1"&gt;# 2.5
&lt;/span&gt;    &lt;span class="n"&gt;interval_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;    &lt;span class="c1"&gt;# next review
&lt;/span&gt;    &lt;span class="n"&gt;due&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine for languages where a word is mostly itself. English: "house," "houses," done. Spanish: a few verb conjugations, manageable.&lt;/p&gt;

&lt;p&gt;Russian breaks this with combinatorics. 6 cases × 2 numbers × gender variations × stress-shift patterns for nouns. Verbs add aspect (perfective/imperfective pairs), prefixes, motion-verb directionality, and a separate past-tense system that agrees in gender. The matrix is large enough that one ease score per lexeme throws away most of the signal you have.&lt;/p&gt;

&lt;p&gt;Here's what I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Lexeme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;headword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;         &lt;span class="c1"&gt;# "дом"
&lt;/span&gt;    &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;              &lt;span class="c1"&gt;# "noun"
&lt;/span&gt;    &lt;span class="n"&gt;gender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;           &lt;span class="c1"&gt;# "masc"
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;lexeme_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# "домом"
&lt;/span&gt;    &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;             &lt;span class="c1"&gt;# "instrumental"
&lt;/span&gt;    &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;           &lt;span class="c1"&gt;# "singular"
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;form_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;          &lt;span class="c1"&gt;# the unit of memory
&lt;/span&gt;    &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;interval_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;due&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;last_outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;     &lt;span class="c1"&gt;# "correct" | "missed" | "wrong-case"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unit of scheduling is the form, not the lexeme. Each form carries its own ease and interval. When a learner misses the instrumental of дом, that specific row gets pulled back. The nominative they nailed last week stays scheduled where it was.&lt;/p&gt;

&lt;p&gt;This was the change that took the most rewriting and gave the biggest payoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why per-form actually works in practice
&lt;/h2&gt;

&lt;p&gt;Two reasons.&lt;/p&gt;

&lt;p&gt;First, the failures cluster by case. A learner who's shaky on instrumental tends to be shaky across many nouns, not just one. Per-form tracking makes that visible. I can show a dashboard that says "your instrumental singular is at 60%" instead of "you missed 12 cards this week."&lt;/p&gt;

&lt;p&gt;Second, the wins compound. Once a learner has stabilized the nominative across 200 nouns, those 200 form rows slide into long intervals and stop crowding the daily queue. The queue fills up with the cases they're actively learning. The old "one card per word" model kept pulling the nominative back in any time the genitive failed, which is wasted reps.&lt;/p&gt;

&lt;p&gt;I didn't expect the second effect. The first one was the design goal. The second showed up in the data after a few weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercise selection: the messy part
&lt;/h2&gt;

&lt;p&gt;Per-form scheduling solves "what's due." It doesn't solve "what exercise."&lt;/p&gt;

&lt;p&gt;A form has many ways it can be tested. For домом (domom) you can ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Translate "with the house" into Russian.&lt;/li&gt;
&lt;li&gt;Fill the blank: "Он гордится ___." (He is proud of ___.)&lt;/li&gt;
&lt;li&gt;Pick the right case for the verb's argument from a list.&lt;/li&gt;
&lt;li&gt;Type the form when shown the headword and the case label.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each tests something different. Exercise 1 leans on translation and active recall. Exercise 4 isolates the morphology. Exercise 2 puts the form in semantic context. They aren't interchangeable.&lt;/p&gt;

&lt;p&gt;I pick the exercise type per review based on the form's recent history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick_exercise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Review&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExerciseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;streak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;current_correct_streak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;last_failure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;most_recent_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;streak&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;last_failure&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong-case&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ExerciseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FORM_FROM_LABEL&lt;/span&gt;   &lt;span class="c1"&gt;# isolate the morphology
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;streak&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ExerciseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FILL_IN_CONTEXT&lt;/span&gt;   &lt;span class="c1"&gt;# rebuild in a sentence
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ExerciseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TRANSLATE_PHRASE&lt;/span&gt;      &lt;span class="c1"&gt;# active production
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ramp goes from low cognitive load (produce the form in isolation) to high cognitive load (produce a whole phrase). When something breaks, the next review drops back down. The form has to climb the ladder again. This was probably the second-biggest design change after the per-form unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke when I switched
&lt;/h2&gt;

&lt;p&gt;A few things I didn't predict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue size exploded.&lt;/strong&gt; When the unit was a word, a daily review queue maxed out at maybe 30 items. With forms, it could balloon to 200+ for a learner who'd seen 50 nouns. I had to cap and prioritize: nominative first, then accusative (most common in early Russian input), then genitive. The other cases unlock progressively as the lower ones stabilize. Without that gating, the system felt like a punishment machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spaced repetition math needed adjusting.&lt;/strong&gt; SM-2 (the algorithm Anki uses) assumes independence between cards. Forms of the same lexeme aren't independent. If you can produce домом, you almost always can produce доме next time you see it. I added a small "sibling correctness" boost: when one form of a lexeme is answered correctly, related forms get a tiny ease bump. Not enough to skip review, just enough to stretch their interval. This kept the queue tractable without throwing away review fidelity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong-case errors needed their own bucket.&lt;/strong&gt; A learner who types дома when the prompt wanted домов isn't blanking on the word. They're picking the wrong case. Treating that the same as "didn't know" was too punishing and gave noisy data. So &lt;code&gt;last_outcome&lt;/code&gt; has three states, not two, and wrong-case errors route the form back into the morphology-isolating exercise type next time around. Different failure mode, different remediation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs
&lt;/h2&gt;

&lt;p&gt;The honest answer: more data, more queries, more product surface.&lt;/p&gt;

&lt;p&gt;A noun has 12 form rows. A verb has dozens (aspect pairs, tense, person, gender for past, imperatives, gerunds). For a 1,000-word vocab base you're looking at 15,000+ form rows before any reviews exist. Indexes on &lt;code&gt;(user_id, due)&lt;/code&gt; and &lt;code&gt;(user_id, form_id)&lt;/code&gt; matter. Postgres handles it fine at the scale I'm running; I checked the query plans.&lt;/p&gt;

&lt;p&gt;The UI also has to expose this. A learner needs to understand why they're seeing домом again when they knew дом. The form view shows the case label, the singular/plural, and the recent history. Without that, the system feels arbitrary, and arbitrary kills retention faster than difficulty does.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is overkill
&lt;/h2&gt;

&lt;p&gt;Don't build per-form SRS for English. Don't build it for languages where the morphology is small enough to fold into one card. Spanish verbs are right on the edge; I'd probably do per-tense, not per-form, if I were building for Spanish.&lt;/p&gt;

&lt;p&gt;For Russian (and Polish, Czech, Finnish, any heavily inflected language) the per-form model pays for itself within the first few weeks. The signal you get about which cases are stuck is the actual product. Without it you're guessing about a learner whose curve looks fine on the surface but has a giant blind spot underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you've tried to learn Russian and bounced off Anki because the cards felt wrong, the live version of this is at &lt;a href="https://slova.be" rel="noopener noreferrer"&gt;slova.be&lt;/a&gt;. It's an A1→B1 trainer built around case-aware drilling and verb aspect pairing.&lt;/p&gt;

&lt;p&gt;If you're building an SRS for another inflected language and want to compare scheduler shapes, my email's on the site.&lt;/p&gt;

</description>
      <category>russian</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
