<?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: Mark Flame</title>
    <description>The latest articles on DEV Community by Mark Flame (@mark_flame_fb0056b1fbe76b).</description>
    <link>https://dev.to/mark_flame_fb0056b1fbe76b</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1781433%2Ff76fb0eb-6ff1-4f92-93da-4fea242e2cf1.jpg</url>
      <title>DEV Community: Mark Flame</title>
      <link>https://dev.to/mark_flame_fb0056b1fbe76b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mark_flame_fb0056b1fbe76b"/>
    <language>en</language>
    <item>
      <title>The Mongoose Bug That Taught Me to Stop Trusting findByIdAndUpdate</title>
      <dc:creator>Mark Flame</dc:creator>
      <pubDate>Thu, 09 Jul 2026 16:02:04 +0000</pubDate>
      <link>https://dev.to/mark_flame_fb0056b1fbe76b/the-mongoose-bug-that-taught-me-to-stop-trusting-findbyidandupdate-54gj</link>
      <guid>https://dev.to/mark_flame_fb0056b1fbe76b/the-mongoose-bug-that-taught-me-to-stop-trusting-findbyidandupdate-54gj</guid>
      <description>&lt;h1&gt;
  
  
  The Mongoose Bug That Taught Me to Stop Trusting findByIdAndUpdate
&lt;/h1&gt;

&lt;p&gt;I want to talk about a bug that cost me a few hours, because I think the&lt;br&gt;
&lt;em&gt;reason&lt;/em&gt; it happened is more useful than the fix itself. It's the kind of bug&lt;br&gt;
that doesn't throw an error, doesn't crash anything, and just quietly does&lt;br&gt;
the wrong thing — which is the worst kind.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I was working on the Result module for KDC, a school management system I'm&lt;br&gt;
building. Part of the flow involves updating a student's result document —&lt;br&gt;
recalculating a final score, and by extension, a grade — whenever a score&lt;br&gt;
field changes. I had a Mongoose &lt;code&gt;pre('save')&lt;/code&gt; hook on the &lt;code&gt;Result&lt;/code&gt; model to&lt;br&gt;
handle exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;resultSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finalScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cbtScore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writtenScore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateGrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finalScore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Straightforward. Whenever a result document gets saved, recompute the score&lt;br&gt;
and grade from whatever the current values are. I tested it against a&lt;br&gt;
document I created and saved directly, and it worked exactly as expected.&lt;/p&gt;

&lt;p&gt;Then I wired up the actual update endpoint using the pattern I'd used&lt;br&gt;
everywhere else in the codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByIdAndUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;resultId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;writtenScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newScore&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the grade just... didn't update. &lt;code&gt;finalScore&lt;/code&gt; stayed stale. No error, no&lt;br&gt;
warning, nothing in the logs. The document saved fine, the &lt;code&gt;writtenScore&lt;/code&gt;&lt;br&gt;
field updated fine — just the derived fields silently didn't.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;Here's the thing I didn't fully internalize until I hit this: &lt;code&gt;findByIdAndUpdate&lt;/code&gt;&lt;br&gt;
(and &lt;code&gt;findOneAndUpdate&lt;/code&gt;) talk to MongoDB directly through the driver's update&lt;br&gt;
operators. They don't load a document into memory, mutate it, and call&lt;br&gt;
&lt;code&gt;.save()&lt;/code&gt; on it. Which means Mongoose's &lt;code&gt;pre('save')&lt;/code&gt; and &lt;code&gt;post('save')&lt;/code&gt;&lt;br&gt;
middleware — the hooks tied specifically to the &lt;code&gt;save&lt;/code&gt; lifecycle — never&lt;br&gt;
fire. There's a separate &lt;code&gt;pre('findOneAndUpdate')&lt;/code&gt; hook if you want middleware&lt;br&gt;
on that path, but it's a genuinely different hook, and if you only defined&lt;br&gt;
&lt;code&gt;pre('save')&lt;/code&gt; (like I had), it's simply skipped.&lt;/p&gt;

&lt;p&gt;This isn't a Mongoose bug. It's Mongoose behaving exactly as documented. The&lt;br&gt;
bug was mine — I'd built a mental model where "updating a document" always&lt;br&gt;
meant "the save hooks will run," and that assumption was wrong for one of the&lt;br&gt;
two most common ways to update a document in Mongoose.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: fetch, mutate, save
&lt;/h2&gt;

&lt;p&gt;The reliable fix, when you need &lt;code&gt;save&lt;/code&gt;-hook logic to actually run, is to stop&lt;br&gt;
using the shortcut and go back to the explicit pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writtenScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newScore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// pre('save') hook fires here, as expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines instead of one, and it costs you an extra round trip to the&lt;br&gt;
database. But it means your derived fields, validation, and any other&lt;br&gt;
&lt;code&gt;save&lt;/code&gt;-hook logic actually run every time, instead of only running for the&lt;br&gt;
code paths that happen to call &lt;code&gt;.save()&lt;/code&gt; directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually changed after this
&lt;/h2&gt;

&lt;p&gt;It wasn't just this one endpoint. I went back through the codebase and&lt;br&gt;
audited every place I was using &lt;code&gt;findByIdAndUpdate&lt;/code&gt; or &lt;code&gt;findOneAndUpdate&lt;/code&gt; on&lt;br&gt;
a model that had &lt;code&gt;pre('save')&lt;/code&gt; middleware, because the bug is silent by&lt;br&gt;
nature — it doesn't announce itself, it just produces stale derived data that&lt;br&gt;
looks fine until someone notices the numbers are wrong.&lt;/p&gt;

&lt;p&gt;The rule I settled on: if a schema has meaningful &lt;code&gt;pre('save')&lt;/code&gt; logic&lt;br&gt;
(derived fields, validation that depends on multiple fields, anything beyond&lt;br&gt;
basic schema-level validation), don't reach for &lt;code&gt;findByIdAndUpdate&lt;/code&gt; on it by&lt;br&gt;
default. Use the fetch-mutate-save pattern, and reserve the &lt;code&gt;findAndUpdate&lt;/code&gt;&lt;br&gt;
shortcuts for simple field updates where nothing downstream depends on&lt;br&gt;
&lt;code&gt;save&lt;/code&gt;-hook side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;The Mongoose docs cover this distinction clearly — it wasn't undocumented&lt;br&gt;
behavior. The real lesson wasn't "read the docs more carefully," though&lt;br&gt;
that's true. It was: &lt;strong&gt;when you reach for a shortcut method, know what it&lt;br&gt;
skips, not just what it does.&lt;/strong&gt; &lt;code&gt;findByIdAndUpdate&lt;/code&gt; is genuinely useful and&lt;br&gt;
I still use it constantly — just not on models where I need save-lifecycle&lt;br&gt;
guarantees. The bug taught me to ask that question before writing the update&lt;br&gt;
logic, not after debugging why a grade wasn't recalculating.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>javascript</category>
      <category>mongodb</category>
      <category>node</category>
    </item>
    <item>
      <title>"Building Network-Resilient Exam Sessions for a CBT System in Nigeria"</title>
      <dc:creator>Mark Flame</dc:creator>
      <pubDate>Thu, 09 Jul 2026 15:55:40 +0000</pubDate>
      <link>https://dev.to/mark_flame_fb0056b1fbe76b/building-network-resilient-exam-sessions-for-a-cbt-system-in-nigeria-378p</link>
      <guid>https://dev.to/mark_flame_fb0056b1fbe76b/building-network-resilient-exam-sessions-for-a-cbt-system-in-nigeria-378p</guid>
      <description>&lt;h1&gt;
  
  
  Building Network-Resilient Exam Sessions for a CBT System in Nigeria
&lt;/h1&gt;

&lt;p&gt;A few months ago I started building KDC, a school management system for an NGO&lt;br&gt;
school here in Abuja. One of the modules is a CBT (computer-based testing)&lt;br&gt;
engine — students sit exams on shared school computers, timed, timetable-gated,&lt;br&gt;
auto-graded.&lt;/p&gt;

&lt;p&gt;It sounds like a standard CRUD problem until you remember where it's running.&lt;br&gt;
Nigerian schools deal with unstable power and inconsistent network connections.&lt;br&gt;
A student can be 12 minutes into a 40-minute exam and the connection drops —&lt;br&gt;
NEPA takes the light, the router reboots, whatever. If the exam session just&lt;br&gt;
dies at that point, you've got a genuinely upset student, a teacher who has to&lt;br&gt;
manually intervene, and a system nobody trusts.&lt;/p&gt;

&lt;p&gt;So "what happens when the network drops mid-exam" wasn't an edge case I could&lt;br&gt;
shrug off. It had to be a first-class part of the design.&lt;/p&gt;
&lt;h2&gt;
  
  
  The naive approach (and why it breaks)
&lt;/h2&gt;

&lt;p&gt;The obvious first design is: student starts exam → session created → student&lt;br&gt;
submits answers as they go → session closes on submit or timeout.&lt;/p&gt;

&lt;p&gt;The problem is state. If you don't explicitly track &lt;em&gt;where&lt;/em&gt; a student is in&lt;br&gt;
an exam — which questions they've seen, what they've answered so far, how&lt;br&gt;
much time is actually left — then a disconnect forces you into one of two bad&lt;br&gt;
options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Restart the exam.&lt;/strong&gt; Unacceptable. The student loses their timer, their
progress, possibly the trust of the whole system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust the client.&lt;/strong&gt; Let the frontend hold all the state and just resubmit
everything on reconnect. Fragile — browser tabs get closed, local storage
gets cleared, and now you have no server-side source of truth for a
&lt;em&gt;graded academic exam&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither works. The session itself needed to be the source of truth, not the&lt;br&gt;
client.&lt;/p&gt;
&lt;h2&gt;
  
  
  Designing the ExamSession as a resumable entity
&lt;/h2&gt;

&lt;p&gt;The fix was to make &lt;code&gt;ExamSession&lt;/code&gt; a persistent, resumable entity rather than a&lt;br&gt;
transient in-memory thing tied to a socket or a request lifecycle. Every&lt;br&gt;
session tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which exam and student it belongs to&lt;/li&gt;
&lt;li&gt;a server-authoritative start timestamp (so remaining time is always
calculated server-side, never trusted from the client)&lt;/li&gt;
&lt;li&gt;current status (&lt;code&gt;in-progress&lt;/code&gt;, &lt;code&gt;submitted&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;answers submitted so far, keyed by question ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a student reconnects — new tab, refreshed page, different device on the&lt;br&gt;
same network — the client doesn't start a new session. It asks the server:&lt;br&gt;
&lt;em&gt;"does this student have an active session for this exam?"&lt;/em&gt; If yes, the&lt;br&gt;
server returns the existing session state: time remaining (calculated from&lt;br&gt;
the stored start time, not a client clock), and everything already answered.&lt;br&gt;
The frontend just re-renders from that state. From the student's point of&lt;br&gt;
view, it looks like nothing happened.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;exam&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="nx"&gt;examId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;xxxx&lt;/span&gt;

&lt;span class="c1"&gt;// server logic (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ExamSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;student&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;studentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;exam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;examId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in-progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;existingSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;exam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;existingSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;existingSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;autoSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingSession&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existingSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decision here: &lt;strong&gt;time is never trusted from the client.&lt;/strong&gt;&lt;br&gt;
Whatever the frontend timer shows is just a UI convenience. The moment a&lt;br&gt;
session is fetched or an answer is submitted, the server recalculates&lt;br&gt;
remaining time from the stored &lt;code&gt;startedAt&lt;/code&gt;. That's what makes a disconnect&lt;br&gt;
safe — the clock doesn't stop just because the connection did.&lt;/p&gt;
&lt;h2&gt;
  
  
  Batch-processing answers with a Map for O(1) lookup
&lt;/h2&gt;

&lt;p&gt;The other piece of this was answer submission. Early on I was updating one&lt;br&gt;
answer at a time — student answers a question, client fires a request,&lt;br&gt;
server updates that one field in the session document. That's a lot of&lt;br&gt;
round trips for a 40-question exam, and it's exactly the kind of pattern&lt;br&gt;
that falls apart on a flaky connection: one failed request out of forty&lt;br&gt;
and now the session state is silently incomplete.&lt;/p&gt;

&lt;p&gt;I switched to batching. The client periodically syncs the &lt;em&gt;entire&lt;/em&gt; current&lt;br&gt;
answer set, and on the server I process it against a &lt;code&gt;Map&lt;/code&gt; keyed by question&lt;br&gt;
ID rather than looping through an array with &lt;code&gt;.find()&lt;/code&gt; on every question&lt;br&gt;
(which would be O(n) per lookup, O(n²) for a full batch):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingAnswers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;questionId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;incoming&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;submittedAnswers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;existingAnswers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;questionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;questionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;questionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;selectedOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedOption&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;answeredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingAnswers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does two things: it makes each sync idempotent (resubmitting the same&lt;br&gt;
batch after a dropped connection doesn't create duplicates or corrupt state),&lt;br&gt;
and it keeps the update fast regardless of how many questions are in the&lt;br&gt;
exam.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone building this from scratch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server time, always.&lt;/strong&gt; Any timed, gradeable system where you let the
client be the source of truth for time is a system you don't actually
control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency isn't optional on flaky networks.&lt;/strong&gt; If a retry can happen
(and on unreliable connections, it will), your write operations need to be
safe to repeat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for the network you'll actually be deployed on&lt;/strong&gt;, not the one on
your dev machine. A lot of backend tutorials assume stable, low-latency
connections. That assumption doesn't hold for a lot of the world, including
where I'm building this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;KDC is still in active development — the exam module is done, results and&lt;br&gt;
analytics are next. If you're building anything for an environment with&lt;br&gt;
unreliable infrastructure, I'd genuinely rather over-engineer for&lt;br&gt;
disconnects up front than field angry calls from a school on exam day.&lt;/p&gt;

</description>
      <category>education</category>
      <category>networking</category>
      <category>showdev</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
