<?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>Idempotency Is the System Design Concept That Actually Saves You</title>
      <dc:creator>Mark Flame</dc:creator>
      <pubDate>Sat, 11 Jul 2026 18:26:54 +0000</pubDate>
      <link>https://dev.to/mark_flame_fb0056b1fbe76b/idempotency-is-the-system-design-concept-that-actually-saves-you-4l3b</link>
      <guid>https://dev.to/mark_flame_fb0056b1fbe76b/idempotency-is-the-system-design-concept-that-actually-saves-you-4l3b</guid>
      <description>&lt;h1&gt;
  
  
  Idempotency Is the System Design Concept That Actually Saves You
&lt;/h1&gt;

&lt;p&gt;If you ask backend engineers to name system design concepts, you'll get&lt;br&gt;
"scalability," "load balancing," "caching," "sharding" — the big, glamorous&lt;br&gt;
ones. Idempotency rarely makes the list, and I think that's backwards. It's&lt;br&gt;
one of the few concepts that quietly determines whether your system behaves&lt;br&gt;
correctly the moment anything goes wrong, and things going wrong — networks&lt;br&gt;
dropping, clients retrying, workers crashing mid-job — isn't an edge case in&lt;br&gt;
a real system. It's Tuesday.&lt;/p&gt;
&lt;h2&gt;
  
  
  What idempotency actually means
&lt;/h2&gt;

&lt;p&gt;An operation is idempotent if running it once has the same effect as running&lt;br&gt;
it multiple times. &lt;code&gt;PUT /users/42 { name: "Chika" }&lt;/code&gt; is idempotent — do it&lt;br&gt;
once or five times, the user's name ends up "Chika" either way. &lt;code&gt;POST&lt;br&gt;
/orders&lt;/code&gt; that creates a new order every time it's called is not — call it&lt;br&gt;
twice and you've got two orders, possibly two charges.&lt;/p&gt;

&lt;p&gt;That's the textbook definition. The part that matters in practice is &lt;em&gt;why&lt;/em&gt;&lt;br&gt;
you should care: &lt;strong&gt;the network doesn't give you exactly-once delivery.&lt;/strong&gt; A&lt;br&gt;
client sends a request, the server processes it successfully, and the&lt;br&gt;
response gets lost on the way back — timeout, dropped connection, whatever.&lt;br&gt;
From the client's point of view, it looks exactly like the request never&lt;br&gt;
arrived. The only sane thing to do is retry. Which means every operation&lt;br&gt;
your system exposes to retries needs to survive being executed more than&lt;br&gt;
once for the same logical intent.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where this bites you if you don't design for it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Payment and order creation.&lt;/strong&gt; The classic example for a reason. A client&lt;br&gt;
submits a payment, the request succeeds server-side, the response times out&lt;br&gt;
before the client sees it, the client retries. Without idempotency&lt;br&gt;
protection, that's a double charge — and it's the kind of bug that doesn't&lt;br&gt;
show up in testing, because your test environment doesn't have a flaky&lt;br&gt;
network. It shows up in production, at scale, as a support ticket queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message queue consumers.&lt;/strong&gt; Most queue systems (SQS, RabbitMQ, Kafka&lt;br&gt;
consumer groups) give you &lt;em&gt;at-least-once&lt;/em&gt; delivery, not &lt;em&gt;exactly-once&lt;/em&gt;, as&lt;br&gt;
the default guarantee — a message can be redelivered after a consumer&lt;br&gt;
crashes before acknowledging it, even if the work was actually completed.&lt;br&gt;
If your message handler isn't idempotent, a crash-and-redeliver cycle&lt;br&gt;
silently duplicates whatever side effect that handler causes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed writes across services.&lt;/strong&gt; Anytime one service calls another&lt;br&gt;
and the caller can't be certain whether the callee actually completed the&lt;br&gt;
operation before the connection dropped, you have the same problem in&lt;br&gt;
miniature. Retrying blindly assumes non-idempotent operations are safe to&lt;br&gt;
repeat, which is usually the wrong assumption.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to actually build it in
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Idempotency keys.&lt;/strong&gt; The client generates a unique key per logical&lt;br&gt;
operation (a UUID, typically) and sends it with the request. The server&lt;br&gt;
checks: have I seen this key before? If yes, return the stored result of the&lt;br&gt;
original operation instead of re-executing it. If no, execute it and store&lt;br&gt;
the result against that key. Stripe's API is the reference example most&lt;br&gt;
engineers have actually used as a client — send the same &lt;code&gt;Idempotency-Key&lt;/code&gt;&lt;br&gt;
header twice, get the same charge result twice, not two charges.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;existing&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;IdempotencyRecord&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&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;existing&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&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="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&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;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&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;IdempotencyRecord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&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="nx"&gt;payment&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;&lt;strong&gt;Natural idempotency via upserts.&lt;/strong&gt; Sometimes you don't need a separate&lt;br&gt;
key at all — the operation itself can be made idempotent by its own&lt;br&gt;
structure. Instead of "insert a new record," do "upsert keyed on a natural&lt;br&gt;
unique identifier" (&lt;code&gt;updateOne({ orderId }, { $set: data }, { upsert: true&lt;br&gt;
})&lt;/code&gt;). Run it once or five times, you get the same end state, because the&lt;br&gt;
write is keyed on something that already uniquely identifies the logical&lt;br&gt;
entity rather than creating a new one each call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set-based state transitions instead of increments.&lt;/strong&gt; &lt;code&gt;SET status =&lt;br&gt;
'shipped'&lt;/code&gt; is idempotent. &lt;code&gt;UPDATE inventory SET quantity = quantity - 1&lt;/code&gt; is&lt;br&gt;
not — run it twice and you've decremented twice. Where possible, prefer&lt;br&gt;
writes that assign an absolute value or a range-conditioned transition over&lt;br&gt;
writes that apply a relative change, precisely because the former survives&lt;br&gt;
duplication and the latter doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What idempotency doesn't solve
&lt;/h2&gt;

&lt;p&gt;It's not a substitute for genuine distributed transactions when you need&lt;br&gt;
strict atomicity across multiple resources — it solves "this specific&lt;br&gt;
operation is safe to retry," not "these five operations across three&lt;br&gt;
services either all happen or none do." Sagas and compensating transactions&lt;br&gt;
address that broader problem; idempotency is what makes each individual&lt;br&gt;
step in a saga safe to retry when a step fails partway through, which is&lt;br&gt;
usually why the two show up together in the same system.&lt;/p&gt;

&lt;p&gt;It also doesn't remove the need to think about concurrency. Two &lt;em&gt;different&lt;/em&gt;&lt;br&gt;
clients hitting the same idempotent endpoint at the same time with&lt;br&gt;
&lt;em&gt;different&lt;/em&gt; idempotency keys is a race condition idempotency does nothing&lt;br&gt;
for — that's a locking or optimistic-concurrency problem, a separate&lt;br&gt;
concern that idempotency sometimes gets mistaken for solving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual habit worth building
&lt;/h2&gt;

&lt;p&gt;Before writing any endpoint or message handler that causes a side effect —&lt;br&gt;
a write, a charge, a notification, a state change — ask one question: &lt;em&gt;what&lt;br&gt;
happens if this runs twice for the same logical request?&lt;/em&gt; If the honest&lt;br&gt;
answer is "something bad," that's the signal to add an idempotency key, an&lt;br&gt;
upsert, or a set-based transition before it ships, not after the first&lt;br&gt;
double-charge ticket comes in. It's a five-minute question at design time&lt;br&gt;
and a much longer one to answer after the fact in production.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Atomic Student Creation Across Microservices: Why I Reached for a Compensating Transaction</title>
      <dc:creator>Mark Flame</dc:creator>
      <pubDate>Fri, 10 Jul 2026 15:27:26 +0000</pubDate>
      <link>https://dev.to/mark_flame_fb0056b1fbe76b/atomic-student-creation-across-microservices-why-i-reached-for-a-compensating-transaction-213o</link>
      <guid>https://dev.to/mark_flame_fb0056b1fbe76b/atomic-student-creation-across-microservices-why-i-reached-for-a-compensating-transaction-213o</guid>
      <description>&lt;h1&gt;
  
  
  Atomic Student Creation Across Microservices: Why I Reached for a Compensating Transaction
&lt;/h1&gt;

&lt;p&gt;SkillUp Africa, the EdTech platform I'm building, is a NestJS monorepo split&lt;br&gt;
into an &lt;code&gt;auth-service&lt;/code&gt;, a &lt;code&gt;school-service&lt;/code&gt;, and an API gateway that talks to&lt;br&gt;
both over TCP. Splitting things this way is great for separation of concerns&lt;br&gt;
— until you hit an operation that genuinely needs to touch both services in&lt;br&gt;
one logical unit of work. Creating a student is exactly that operation, and&lt;br&gt;
it's what forced me to actually think about distributed consistency instead&lt;br&gt;
of assuming it for free.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this is harder than it looks
&lt;/h2&gt;

&lt;p&gt;In a monolith, creating a student would be one database transaction:&lt;br&gt;
insert the user account, insert the student record, commit. If anything&lt;br&gt;
fails, the whole thing rolls back automatically and you're left with either&lt;br&gt;
nothing or a fully-created student. Postgres/Mongo transactions give you that&lt;br&gt;
guarantee for free within a single database.&lt;/p&gt;

&lt;p&gt;The moment you split into services with separate databases, that guarantee&lt;br&gt;
disappears. In SkillUp, creating a student (specifically, a school admin&lt;br&gt;
creating a &lt;em&gt;managed&lt;/em&gt; student account) means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;auth-service&lt;/code&gt; creates the user record — hashed credentials, role, a
server-generated username&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;school-service&lt;/code&gt; creates the student entity — linked to a school, a
class, guardian info, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are two separate databases behind two separate services talking over&lt;br&gt;
TCP via &lt;code&gt;@MessagePattern&lt;/code&gt; handlers. There is no single database transaction&lt;br&gt;
that can wrap both. If step 1 succeeds and step 2 fails — school-service is&lt;br&gt;
down, a validation fails, the network hiccups — you're left with a user&lt;br&gt;
account that can log in but has no student record attached to it. That's not&lt;br&gt;
a cosmetic bug. It's an orphaned identity in the system, and depending on&lt;br&gt;
what the gateway does next, it can look to the admin like the creation&lt;br&gt;
"failed" when actually it half-succeeded.&lt;/p&gt;
&lt;h2&gt;
  
  
  The naive fixes, and why I didn't use them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Two-phase commit (2PC).&lt;/strong&gt; The classic distributed-transaction answer. I&lt;br&gt;
considered it and ruled it out fast — it requires a transaction coordinator,&lt;br&gt;
both services need to support prepare/commit phases, and it doesn't play&lt;br&gt;
well with service unavailability (a participant going down mid-transaction&lt;br&gt;
can leave things locked). For a two-service, low-throughput operation like&lt;br&gt;
"admin creates a student," that's a lot of infrastructure for the problem&lt;br&gt;
at hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just hope it doesn't fail.&lt;/strong&gt; Tempting when you're moving fast, genuinely&lt;br&gt;
the wrong call for anything touching account creation. Auth bugs are the&lt;br&gt;
kind that erode trust in a platform fastest.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I actually built: a compensating transaction (saga pattern, simplified)
&lt;/h2&gt;

&lt;p&gt;The pattern I used is a straightforward saga: perform step one, and if step&lt;br&gt;
two fails, explicitly undo step one. No coordinator, no distributed locks —&lt;br&gt;
just the gateway (or an orchestrating service) being responsible for&lt;br&gt;
cleanup when a later step fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// gateway / orchestrating service (simplified)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createManagedStudent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateStudentDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adminSchoolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// schoolId is resolved from the admin's JWT, never trusted from the request body&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schoolId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adminSchoolId&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;authClient&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth.create-managed-user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;student&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// username is always server-generated — never accepted from the client&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateUsername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&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="nf"&gt;toPromise&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&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;student&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;schoolClient&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;school.create-student&lt;/span&gt;&lt;span class="dl"&gt;'&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;dto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schoolId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toPromise&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;student&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// school-service failed — compensate by rolling back the user we just created&lt;/span&gt;
    &lt;span class="k"&gt;await&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;authClient&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth.delete-user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toPromise&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InternalServerErrorException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Student creation failed and was rolled back.&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details here matter as much as the try/catch structure itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;schoolId&lt;/code&gt; is always resolved from the admin's JWT, never from the
request body.&lt;/strong&gt; If it were client-supplied, any authenticated admin could
potentially create a student under a different school just by changing a
field in the payload. This isn't strictly part of the compensating
transaction pattern, but it lives in the same code path and it's the kind
of thing that's easy to get wrong when you're focused on the transaction
logic and lose sight of the trust boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The username is server-generated, not client-supplied.&lt;/strong&gt; This removes
an entire class of bugs and abuse (duplicate usernames, impersonation
attempts, injection through a username field) at the source, rather than
validating it after the fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The compensation step (&lt;code&gt;auth.delete-user&lt;/code&gt;) has to be idempotent and
reliable in its own right.&lt;/strong&gt; If the delete call itself fails, you now have
the orphaned-user problem you were trying to avoid, just one layer deeper.
In practice this means the compensation call needs its own retry handling,
and worst case, a background reconciliation job that periodically checks
for users with no linked student record and flags or cleans them up. I
haven't needed the reconciliation job yet at current scale, but I'm not
pretending the try/catch alone is a complete guarantee — it's the first
line of defense, not the only one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not just make school-service the source of truth and skip auth-service entirely?
&lt;/h2&gt;

&lt;p&gt;I considered this too — collapse the operation into one service so there's&lt;br&gt;
one database and normal ACID transactions apply. It doesn't work here&lt;br&gt;
because &lt;code&gt;auth-service&lt;/code&gt; genuinely owns authentication concerns (password&lt;br&gt;
hashing, token issuance, role management) across &lt;em&gt;all&lt;/em&gt; user types in the&lt;br&gt;
platform, not just students, and I didn't want to duplicate that logic or&lt;br&gt;
fracture where credentials live. The service boundary is doing real work;&lt;br&gt;
the compensating transaction is the cost of keeping that boundary honest&lt;br&gt;
rather than collapsing it for the sake of one operation's convenience.&lt;/p&gt;

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

&lt;p&gt;Splitting a system into services doesn't remove the need for transactional&lt;br&gt;
thinking — it just moves the responsibility from the database to your&lt;br&gt;
application code. Postgres and MongoDB give you strong consistency&lt;br&gt;
guarantees for free within their own boundary; the moment an operation spans&lt;br&gt;
two databases, &lt;em&gt;you&lt;/em&gt; become the transaction coordinator, whether you&lt;br&gt;
planned to or not. The saga pattern isn't a magic fix, it's an honest&lt;br&gt;
acknowledgment of that: define what "undo" means for every step that can&lt;br&gt;
fail after an earlier step already succeeded, and make sure the undo path&lt;br&gt;
is at least as reliable as the thing it's undoing.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>distributedsystems</category>
      <category>microservices</category>
    </item>
    <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>
