Skip to content

Challenges & Tradeoffs

The most credible answer to "tell me about a challenge" is one with a real before/after and an honest tradeoff — not a story with no downside. These are real ones from this project.

1. The import.meta.glob → sharded-index rewrite

Problem: the original content system globbed every markdown file at build time, producing one JS chunk per question. This worked fine at low question counts and would have silently broken (hit Cloudflare's 20,000-file deploy limit) once the bank scaled toward its target of 10k–20k questions — and it also shipped every question's metadata in the main bundle regardless of whether a user ever visited it.

Fix: rebuilt the pipeline around build-time sharded JSON indexes (metadata) + statically-served markdown bodies fetched on demand (content) + sidecar files for heavy hidden tests fetched only at submit-time. Full writeup: Content Pipeline.

Honest tradeoff: this adds real complexity — a custom build script, a runtime fetch-and-cache layer, cache invalidation to reason about — versus the one-liner import.meta.glob it replaced. That complexity is justified because the failure mode it avoids (a broken production deploy at scale) is worse than the complexity cost, but it would be over-engineering for a project that was never going to grow past a few hundred questions.

2. List rendering at scale (virtualization)

Problem: the CP Solutions list renders 600 rows today; DSA is explicitly designed to scale to 10k–20k. A flat .map() over the full filtered array works fine at 600 rows but would mount tens of thousands of DOM nodes at the target scale — a real performance cliff, not a hypothetical one.

Fix: a dependency-free windowing hook (useWindowedRows) that only mounts rows near the viewport once a list exceeds ~150 items, with measured (not hardcoded) row heights and auto-detected scroll containers so it works identically in standalone routes and embedded tab views.

Honest tradeoff: browser find-in-page (Ctrl+F) can't find off-screen rows in a virtualized list. A dedicated library (react-window, TanStack Virtual) would handle more edge cases, but for this specific case (a single scrollable table, no nested scroll regions) a ~90-line hand-rolled hook avoided an extra dependency for a well-understood problem.

3. GitHub Actions cron unreliability

Problem: OA reminder emails were originally scheduled via a GitHub Actions cron. GitHub explicitly documents that scheduled workflows are best-effort and can be delayed or dropped under load — which showed up as students not receiving reminders reliably.

Fix: moved scheduling to a Cloudflare Worker with a cron trigger running every minute, reading Firestore over the REST API and sending via Resend. The original GitHub Actions workflow is kept, but demoted to manual-trigger-only as a backstop.

Honest tradeoff: this is a second scheduling system to maintain, and both the Worker and the manual fallback write to the same remindersSent/reminderClaims fields with an optimistic lock specifically so a manual catch-up run can never double-send — that de-dupe logic is the part that's easy to get subtly wrong.

4. No dedicated backend

Problem/decision: as a small two-person team, building and operating a separate backend service (even a lightweight one) is real ongoing cost — hosting, auth wiring, API versioning, deployment — for a project without a team to share that load.

Choice: Firebase (Auth + Firestore with security rules) covers state and auth; Cloudflare Workers cover the two things that need actual scheduled/server logic; Judge0 handles compute. There is no general-purpose backend API.

Honest tradeoff: Firestore security rules are a real constraint, not just YAML — modeling access control entirely in rules is harder to reason about than an API layer with explicit authorization code, and some validation that would be trivial server-side (e.g. complex cross-document invariants) is awkward or impossible to express purely in rules. This was an acceptable tradeoff for the project's current scale and team size of one; it's the first thing that would need to change if the product needed, say, admin moderation tooling or complex multi-user permissions.

5. AI grading reliability

Problem: CS Fundamentals concept questions need free-text grading against a rubric, and a single AI provider having an outage or rate-limiting mid-exam is a bad experience during a timed OA.

Fix: a fallback chain across nine AI providers (Gemini, Groq, Cerebras, Mistral, OpenRouter, and others) — the first available/successful one wins, transparently to the user.

Honest tradeoff: different providers/models can grade the same rubric slightly differently, so grading consistency isn't perfectly uniform across a session if the chain falls through to a different provider mid-exam. This is a real limitation of AI-graded free-text answers generally, not something fully solved by the fallback chain — the chain solves availability, not grading consistency across providers.

Ediky Workflow — internal engineering documentation.