Designing the Learning Progression Engine Behind Codervai CP

작성자

카테고리:

← 피드로
DEV Community · Parvej Shah · 2026-08-27 개발(SW)
Cover image for Streaks Without Race Conditions: What Actually Runs Behind Codervai CP

Parvej Shah

Originally published at parvejshah.com/blog/scaling-competitive-programming-lms-architectures by Parvej Shah.

Competitive programming requires building a specific kind of knowledge: algorithms and data structures that compose with each other. You can’t understand dynamic programming without first being solid on recursion. You can’t reason about graph traversal without understanding how to implement a queue. The dependency tree is real, and the order in which concepts are introduced matters.

Codervai CP is a structured competitive programming learning platform. Separately, its daily-streak mechanic has to survive a predictable concurrency problem: a burst of submissions near midnight, all racing to protect a streak before the day resets.

Publishing, Not Pacing

The original plan for content release was a cohort calendar — module 1 from day 0, module 2 from day 7, and so on, mirroring how a university course paces itself. That’s not what’s actually running. The real mechanism is simpler: an instructor flips a chapter’s is_live flag from the admin CMS, and every enrolled student is notified the moment it happens.

That ended up being the better call, not a fallback. A publish button doesn’t have timezone edge cases, doesn’t need a scheduler that has to stay correct forever, and gives instructors a real escape hatch — a chapter that isn’t ready yet just doesn’t get published, instead of unlocking on schedule whether it’s ready or not.

The Streak Concurrency Problem

Daily streaks are one of the most effective engagement mechanics in learning platforms. At Codervai CP, streaks are awarded for solving at least one problem per day. A student who maintains a 30-day streak has real motivation to protect it.

The concurrency issue is predictable: a significant fraction of streak activity happens near midnight, as students rush to maintain their streak before the day resets. This creates a burst of simultaneous database writes, and the naive implementation of streak tracking breaks under concurrent load.

Consider the naive approach:

// BROKEN: race condition when two submissions arrive simultaneously
async function updateStreak(userId: string): Promise<void> {
  const { rows } = await pool.query(
    'SELECT * FROM "UserStreak" WHERE "userId" = $1',
    [userId]
  );
  const streak = rows[0];
  const today = new Date().toDateString();

  if (streak && new Date(streak.lastActiveDate).toDateString() === today) return;

  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() - 1);
  const wasActiveYesterday =
    streak && new Date(streak.lastActiveDate).toDateString() === yesterday.toDateString();

  const newStreak = wasActiveYesterday ? streak.currentStreak + 1 : 1;
  await pool.query(
    `INSERT INTO "UserStreak" ("userId", "currentStreak", "lastActiveDate")
     VALUES ($1, $2, NOW())
     ON CONFLICT ("userId") DO UPDATE SET "currentStreak" = $2, "lastActiveDate" = NOW()`,
    [userId, newStreak]
  );
}

Enter fullscreen mode Exit fullscreen mode

If two problem submissions from the same user arrive within milliseconds of each other, both queries execute the SELECT before either has written. Both see the streak as needing an update. Both write. The streak increments by 2 instead of 1.

The actual fix is an atomic upsert at the database level, in one parameterized query, with no application-code read-then-write step at all:

async function recordActivityAndUpdateStreak(userId: string): Promise<void> {
  const today = new Date().toISOString().split("T")[0];

  await pool.query(
    `INSERT INTO "UserStreak" ("userId", "lastActiveDate", "currentStreak", "updatedAt")
     VALUES ($1, $2::date, 1, NOW())
     ON CONFLICT ("userId") DO UPDATE SET
       "currentStreak" = CASE
         WHEN "UserStreak"."lastActiveDate" = ($2::date - INTERVAL '1 day')
           THEN "UserStreak"."currentStreak" + 1
         WHEN "UserStreak"."lastActiveDate" = $2::date
           THEN "UserStreak"."currentStreak"
         ELSE 1
       END,
       "lastActiveDate" = $2::date,
       "updatedAt" = NOW()
     WHERE "UserStreak"."lastActiveDate" < $2::date OR "UserStreak"."userId" IS NULL`,
    [userId, today]
  );
}

Enter fullscreen mode Exit fullscreen mode

The entire logic — check yesterday, check today, compute new streak — is a single atomic database operation, guarded by the trailing WHERE clause: it doubles as same-day idempotency (a second submission the same day is a no-op, not a second increment) and as anti-backdating (a write can’t apply against a date older than what’s already stored). No application code reads a value and then writes a derived value. Concurrent calls for the same user serialize at the database lock level without corrupting the streak count.

Video: Buy, Don’t Build

Editorial code walkthroughs on a competitive programming platform have a specific quality challenge: the content is code on a dark background, and standard video compression optimized for natural scenes tends to blur the fine syntax details that make code legible.

The instinct is to reach for a custom encoding profile — tuned quantization, reduced temporal compression, all the FFmpeg knobs. We didn’t build that. Walkthroughs are delivered through BunnyCDN Stream, which handles HLS segmentation and adaptive bitrate on its own, or a plain YouTube embed where that’s simpler. Legible video-of-code at reasonable cost is a solved problem one layer up the stack; building a custom transcoding pipeline would have meant maintaining infrastructure that mostly re-implements what a CDN already does well, for a marginal quality gain that never got prioritized against actual product work.

What Held Up, What Didn’t

The streak upsert design held up exactly as built — it’s still the atomic, single-round-trip operation described above, running unmodified under real midnight traffic. The “cohort pacing” idea didn’t survive contact with actual instructors using the platform; a manual publish flow turned out to be both simpler to build and easier for content creators to reason about than a scheduler would have been.

Parvej Shah is a Lead Full-Stack Web Developer & Platform Architect based in Dhaka, Bangladesh. Explore full architecture case studies and production code at parvejshah.com.

원문에서 계속 ↗