Designing the Learning Progression Engine Behind Codervai CP

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. When we were designing its learning progression engine, the core product question was: how do you prevent students from jumping to advanced problems before they've built the foundational skills, without making the platform feel restrictive or condescending?
The answer we arrived at was timed module unlocking with a cohort schedule — not ability gating, which frustrates students who feel artificially held back, but temporal pacing, which mirrors how well-designed university courses work.
Module Unlock Logic
Each course cohort operates on a defined schedule: module 1 is available from day 0, module 2 from day 7, module 3 from day 14, and so on. Students who enroll on any day within the cohort window get access to the modules that have been released as of their enrollment date, and new modules unlock on the cohort's schedule going forward.
interface CohortModule {
moduleIndex: number;
unlockAfterDays: number;
title: string;
problemIds: string[];
}
function getAvailableModules(
cohortStartDate: Date,
modules: CohortModule[]
): CohortModule[] {
const elapsedDays = Math.floor(
(Date.now() - cohortStartDate.getTime()) / (1000 * 60 * 60 * 24)
);
return modules.filter(
module => elapsedDays >= module.unlockAfterDays
);
}
The cohort start date is fixed. All students in the cohort see the same modules on the same calendar days. This creates a shared experience — students are working on the same problems simultaneously, which drives community discussion and makes group study sessions more productive.
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 streak = await db.userStreak.findUnique({ where: { userId } });
const today = new Date().toDateString();
if (streak?.lastActiveDate.toDateString() === today) return;
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const wasActiveYesterday =
streak?.lastActiveDate.toDateString() === yesterday.toDateString();
await db.userStreak.upsert({
where: { userId },
create: { userId, currentStreak: 1, lastActiveDate: new Date() },
update: {
currentStreak: wasActiveYesterday ? streak!.currentStreak + 1 : 1,
lastActiveDate: new Date(),
},
});
}
If two problem submissions from the same user arrive within milliseconds of each other, both threads execute the findUnique call before either has written. Both see the streak as needing an update. Both write. The streak increments by 2 instead of 1.
The correct solution is an atomic upsert at the database level using a raw SQL INSERT ON CONFLICT DO UPDATE with CASE logic:
async function recordActivityAndUpdateStreak(userId: string): Promise<void> {
const today = new Date().toISOString().split("T")[0];
await prisma.$executeRaw`
INSERT INTO "UserStreak" ("userId", "lastActiveDate", "currentStreak", "updatedAt")
VALUES (${userId}, ${today}::date, 1, NOW())
ON CONFLICT ("userId") DO UPDATE SET
"currentStreak" = CASE
WHEN "UserStreak"."lastActiveDate" = (${today}::date - INTERVAL '1 day')
THEN "UserStreak"."currentStreak" + 1
WHEN "UserStreak"."lastActiveDate" = ${today}::date
THEN "UserStreak"."currentStreak"
ELSE 1
END,
"lastActiveDate" = ${today}::date,
"updatedAt" = NOW()
WHERE "UserStreak"."lastActiveDate" < ${today}::date;
`;
}
The entire logic — check yesterday, check today, compute new streak — is a single atomic database operation. No application code reads a value and then writes a derived value. Concurrent calls for the same user will serialize at the database lock level without corrupting the streak count.
Video Walkthrough Quality
Editorial code walkthroughs on a competitive programming platform have a specific quality challenge: the content is code on a dark background. Standard video compression is optimized for natural scenes and photographs, and it performs poorly on text — blurring the fine details in syntax that make or break code legibility.
We encode video content in HLS with multiple quality tiers, but the top-tier encoding profile is configured explicitly for code content: higher quantization parameter limits for text regions, reduced temporal compression, and target bitrate that prioritizes sharp edges over smooth gradients.
The encoding configuration is a single FFmpeg preset that content creators run locally before uploading. The infrastructure side handles HLS segmentation and CDN distribution automatically. The hard part was getting the quality parameters right, which required testing the encoding against several monitors, devices, and network conditions to find settings that were legible under all conditions.
Enjoyed the read?
Have a product idea worth building — let's talk.