Snips β Juspay Technical Interview Mastery Guide
Purpose: A comprehensive, production-grade guide to defending the Snips project in high-intensity technical interviews (specifically calibrated for Juspay's focus on low latency, concurrency, database internals, system design, and security).
π Table of Contents
- 30-Second Elevator Pitch (The Hook)
- High-Level System Architecture
- The Hottest Path: Redirect Engine (
GET /:slug) - Database Design & Indexing Strategy
- Analytics Pipeline: Ingestion, Processing & Aggregation
- Slug Generation Algorithm & Collision Math
- Auth, Sessions & Security Architecture
- Frontend Resilience & Axios Interceptors
- Production Scaling & Architectural Evolution (The Senior Upgrade)
- Top 15 Juspay "Grill" Questions & Model Answers
1. 30-Second Elevator Pitch
How to say it in the interview: *"Snips is a high-performance URL shortening and link analytics platform engineered with a strict latency budget for redirects and privacy-centric telemetry. The core challenge in URL shorteners is asymmetry: reads outnumber writes by 100 to 1, and redirect latency directly degrades user experience.
To solve this, I designed a sub-15ms redirect path leveraging Redis as a Cache-Aside layer ahead of PostgreSQL, decoupled click telemetry asynchronously so users aren't blocked on tracking writes, enforced SHA-256 IP anonymization for GDPR-compliant analytics, and built a dual-token JWT authentication engine backed by Redis session control to support active device tracking and instant revocation."*
2. High-Level System Architecture
Architectural Overview Diagram
ββββββββββββββββββββββββββββββββββββββββββ
β Client / Browser β
βββββββββ¬βββββββββββββββββββββββββ¬ββββββββ
β β
Redirect Traffic β β API / Dashboard Traffic
(GET /:slug) β β (CRUD, Auth, Analytics)
βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Express.js Engine β
β (Helmet, Global RateLimit, CookieParser) β
βββββββββ¬ββββββββββββββββββββββββββββββββ¬ββββββββ
β β
Cache Lookup β β Session/CSRF Validation
βββββββββββββββββββ΄ββββββββββββββ β & Cache-Aside Queries
β βΌ βΌ
β ββββββββββββββββββββββββββββββββββββββββββ
β β Redis 7.x β
β β - `slug:{slug}` (TTL 24h) β
β β - `active-session:{userId}` β
β β - `csrf:{userId}` (TTL 1h) β
β β - `otp:{email}`, Rate-limit counters β
β ββββββββββββββββββ¬ββββββββββββββββββββββββ
β β
β Cache Miss / β Cache Miss /
β DB Fallback β Persisted Writes
βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL Database β
β - User Table (Credentials, Verification, Roles) β
β - Link Table (Slugs, Passwords, Expiry, B-Tree Indexes) β
β - ClickEvent Table (Composite Indexed [linkId, clickedAt]) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββArchitectural Separation
- The Edge/Redirect Path (
GET /:slug): Ultra-low latency, public, read-heavy, cached in Redis. - The Analytics & Admin Path (
/api/v1/...): Authenticated, tenant-scoped, analytical SQL queries, write operations.
3. The Hottest Path: Redirect Engine (GET /:slug)
1. The Latency Budget & Philosophy
The redirect route is the most critical path in the entire application. Every extra millisecond added to a redirect degrades user conversion rates.
- Target Internal Latency:
< 15ms(excluding client network RTT). - Golden Rule: Never make a synchronous/blocking call in the redirect path.
2. Request Lifecycle Step-by-Step
Client Request: GET /:slug
β
βΌ
1. Query Redis: GET slug:{slug}
βββ HIT βββββββββ
βββ MISS ββββββββΌββ> Query PostgreSQL: prisma.link.findUnique({ slug })
β Populate Redis: SET slug:{slug} EX 86400 (24h)
βΌ
2. Evaluate Status & Constraints:
βββ Not found? ββββββββββββ> Return 404 (Link Not Found)
βββ isActive == false? ββββ> Return 410 (Link Inactive/Paused)
βββ expiresAt < now()? ββββ> Return 410 (Link Expired)
βββ hasPassword? ββββββββββ> Redirect (302) to Frontend /unlock/:slug
β
βΌ
3. Send HTTP 302 Redirect to `linkData.originalUrl` (Immediately ends HTTP response)
β
βΌ (Asynchronously / Out of Band)
4. Trigger `trackClick(linkId, req)` without `await`3. Key Interview Nuances
Why HTTP 302 (Found) instead of HTTP 301 (Moved Permanently)?
How to say it: "If we send a 301, the browser caches the target URL in its disk cache indefinitely. Subsequent clicks bypass our server completely! This means we would lose all analytics tracking, and if the user updates the destination URL, expires the link, or pauses it, existing visitors would continue going to the old URL. A 302 forces the browser to check our server on every click while redirecting immediately."
Why Cache-Aside with 24-hour TTL?
- Pattern: Cache-Aside (Lazy loading). The cache is populated on the first miss and kept hot.
- Invalidation Strategy: Any mutating action on a link (
updateLink,deleteLink,toggleLink) issues an explicitredisClient.del("slug:" + slug)to prevent stale reads.
4. Database Design & Indexing Strategy
Schema Overview (PostgreSQL via Prisma)
model User {
id String @id @default(uuid())
name String @db.VarChar(255)
email String @unique
password String @db.VarChar(60) // Exact length for bcrypt hash
verified Boolean @default(false)
role String @default("user")
createdAt DateTime @default(now())
links Link[]
}
model Link {
id String @id @default(uuid())
userId String
originalUrl String
slug String @unique // Generates Unique B-Tree Index
title String?
description String?
tags String[] @default([])
password String? // Bcrypt hash if protected
expiresAt DateTime?
isActive Boolean @default(true)
totalClicks Int @default(0) // Denormalized counter for fast dashboard reads
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
clicks ClickEvent[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([slug])
}
model ClickEvent {
id String @id @default(uuid())
linkId String
clickedAt DateTime @default(now())
ipHash String? // SHA-256 hashed IP for unique visitor counting
country String?
city String?
referrer String?
browser String?
device String?
os String?
link Link @relation(fields: [linkId], references: [id], onDelete: Cascade)
@@index([linkId])
@@index([clickedAt])
@@index([linkId, clickedAt]) // Crucial Composite Index for Timeseries
}Deep-Dive: Index Design Rationale
1. Why @@index([linkId, clickedAt]) (Composite Index)?
How to say it: "Our analytics dashboard frequently queries timeseries data:
SELECT ... FROM ClickEvent WHERE linkId = ? AND clickedAt >= ?. If we only had separate single-column indexes onlinkIdandclickedAt, Postgres would have to do an Index Bitmap Scan, intersect both index results in memory, and then fetch heap pages. By creating a composite B-Tree index on(linkId, clickedAt), the B-Tree is ordered first bylinkId, then chronologically byclickedAt. Postgres does a direct B-Tree index range scan on the exact slice of time for that specific link. This turns an $O(N)$ table scan into an $O(\log N + K)$ index lookup, keeping query times under 5ms even with millions of rows."
2. Why Denormalize totalClicks in the Link Table?
- Running
SELECT COUNT(*) FROM ClickEvent WHERE linkId = ?every time a user views their dashboard list (e.g. 50 links) requires 50 aggregate count operations. In PostgreSQL,COUNT(*)must scan the index/MVCC visibility maps. - Denormalizing
totalClicksgives $O(1)$ reads on the link overview list.
3. Cascade Deletions & Referential Integrity
onDelete: CascadeonUser -> LinkandLink -> ClickEvent.- If a user deletes a link, Postgres cleans up all associated historical telemetry in an ACID transaction without leaving orphan records.
5. Analytics Pipeline
1. Ingestion & Telemetry Extraction
When trackClick(linkId, req) is invoked:
- IP Extraction: Safely checks
req.headers["x-forwarded-for"]?.split(",")[0](to handle reverse proxies/CDNs) fallback toreq.socket.remoteAddress. - User-Agent Parsing: Extracted via regex heuristics:
- Device:
mobile,tablet, ordesktop. - Browser: Chrome, Firefox, Safari, Edge, Opera, Other.
- OS: Windows, macOS, Linux, Android, iOS, Other.
- Device:
- Referrer Sanitization: Uses
new URL(referrer).hostnameto clean tracking queries and normalize sources (e.g.,https://l.instagram.com/?utm_source=...->l.instagram.com). If blank, defaults to"direct". - Privacy-Preserving Visitor Identification (
ipHash):- GDPR strictly regulates storing raw IPv4/IPv6 addresses.
- We hash the IP with SHA-256:
const ipHash = ip ? crypto.createHash("sha256").update(ip).digest("hex") : null; - This produces a 64-character deterministic pseudo-identifier. It allows us to compute unique visitors (
COUNT(DISTINCT ipHash)) without storing personally identifiable information (PII).
2. Time-Zone Aware Analytics Aggregation
In analyticsController.js, queries support custom ranges (today, 7d, 30d, all) and user-selected time zones (tz, e.g., "Asia/Kolkata" or "America/New_York").
The SQL Query for Today (Hourly Breakdown):
SELECT
TO_CHAR(DATE_TRUNC('hour', "clickedAt" AT TIME ZONE 'UTC' AT TIME ZONE $tz), 'HH24:MI') as date,
COUNT(*)::int as clicks
FROM "ClickEvent"
WHERE "linkId" = $linkId
AND DATE("clickedAt" AT TIME ZONE 'UTC' AT TIME ZONE $tz) = DATE(NOW() AT TIME ZONE $tz)
GROUP BY 1
ORDER BY 1 ASC;Why double AT TIME ZONE?
How to say it: "Postgres stores timestamps in UTC. The syntax
\"clickedAt\" AT TIME ZONE 'UTC' AT TIME ZONE $tzfirst interprets the UTC timestamp, then shifts it into the user's local timezone. Without this, a click at 1:00 AM IST on Sept 17th would be grouped into 7:30 PM Sept 16th (UTC date), corrupting daily and hourly aggregates for non-UTC users."
3. Categorical Breakdowns (groupBy)
For Countries, Referrers, Devices, Browsers, and OS, we use Prisma's native groupBy (compiles to SQL GROUP BY dimension ORDER BY count DESC LIMIT 10):
const rows = await prisma.clickEvent.groupBy({
by: ["referrer"],
where: { linkId: link.id },
_count: { referrer: true },
orderBy: { _count: { referrer: "desc" } },
take: 10,
});6. Slug Generation Algorithm & Collision Math
1. Base62 Character Set
Alphabet: [a-z, A-Z, 0-9] = 26 + 26 + 10 = 62 characters.
Length: 7 characters.
2. The Combinatorics & Collision Probability
- Total Unique Keys: $$62^7 = 3,521,614,606,208 \approx 3.52 \text{ Trillion combinations}$$
- The Birthday Paradox Math:
In a collision problem with $N$ possible outcomes and $k$ generated keys, the collision probability is approximately:
$$P(\text{collision}) \approx 1 - e^{-\frac{k^2}{2N}}$$
- Even after generating 1,000,000 links, the probability of a collision on the next random generation is: $$\frac{10^6}{3.52 \times 10^{12}} \approx 0.000028%$$
3. Collision Resolution in Code
const generateSlug = async () => {
while (true) {
const slug = Array.from(crypto.randomBytes(7))
.map((b) => CHARS[b % 62])
.join("");
const exists = await prisma.link.findUnique({ where: { slug } });
if (!exists) return slug;
}
};- Uses
crypto.randomBytes(7)for cryptographically secure pseudo-random numbers (CSPRNG) rather thanMath.random(). - Uses a retry loop with database verification.
4. Custom Slugs & Reserved Routing Protection
To prevent users from hijacking application routes, custom slugs are filtered against a blacklist:
const RESERVED = new Set([
"api", "login", "register", "dashboard", "settings", "admin",
"logout", "verify", "reset", "password", "help", "about",
"pricing", "terms", "privacy", "404"
]);If a user tries to create snips.io/login or snips.io/api, the request is rejected with 400 Bad Request.
7. Auth, Sessions & Security Architecture
1. Dual-Token JWT with Session State
- Access Token: Short-lived (1 Hour), contains
{ id, sessionId }. Signed withJWT_SECRET. - Refresh Token: Long-lived (7 Days), contains
{ id, sessionId }. Signed withREFRESH_SECRET. - Cookie Security:
httpOnly: true(JavaScript cannot read the cookie; protects against XSS token theft).secure: true(Transmitted only over HTTPS).sameSite: "none"(Allows cross-domain credentials for decoupled frontend-backend architectures).
2. Single-Device Session Enforcement via Redis
Many basic apps create stateless JWTs with no way to invalidate them before expiry. In Snips:
- When a user logs in, we generate a cryptographically random
sessionId(16 bytes hex). - We store
active-session:{userId} -> sessionIdin Redis. - If the user logs in from another browser or device:
const existingSession = await redisClient.get(activeSessionKey); if (existingSession) { await redisClient.del(`session:${existingSession}`); await redisClient.del(`refresh-token:${id}`); } - On every authenticated request,
checkAuthmiddleware checks:const sessionActive = await isSessionActive(decoded.id, decoded.sessionId); if (!sessionActive) { res.clearCookie("accessToken"); res.clearCookie("refreshToken"); return res.status(401).json({ message: "Session not active. Another device logged in." }); }
How to say it: "This gives us the scalability of JWTs combined with the instant revocation capabilities of stateful sessions. If an account is compromised, we can invalidate all active sessions in Redis in $O(1)$ time."
3. Redis-Backed CSRF Protection
- Why CSRF protection if tokens are in cookies?
Cookies sent with
sameSite: "none"andwithCredentials: trueare vulnerable to Cross-Site Request Forgery. - Implementation:
- A 32-byte cryptographic CSRF token is stored in Redis:
csrf:{userId}(TTL 1h). - Sent to the browser as a non-httpOnly cookie.
- The client must read this cookie and attach it as the
x-csrf-tokenheader on all mutating calls (POST,PUT,PATCH,DELETE). - The backend
verifyCSRFTokenmiddleware validates: $$\text{req.headers['x-csrf-token']} === \text{redis.get('csrf:' + userId)}$$
- A 32-byte cryptographic CSRF token is stored in Redis:
4. Rate Limiting & Abuse Prevention
- Global Ingress:
express-rate-limit(1000 requests per 15 minutes per IP). - Registration:
register-rate-limit:${ip}:${email}(30s cooldown). - Login:
login-rate-limit:${ip}:${email}(30s cooldown). - OTP Verification:
- 6-digit numeric OTP generated via
crypto.randomInt(100000, 999999). - Max 5 attempts tracked in Redis with atomic
INCRonotp-attempts:${email}. - If attempts > 5, OTP is destroyed, thwarting brute-force attacks.
- 6-digit numeric OTP generated via
5. Password-Protected Links
- Password hash stored via
bcrypt.hash(password, 10). - Handled via
POST /api/v1/unlock/:slug. - Verified using
bcrypt.compare()(constant-time execution to prevent timing attacks). - Upon success, the client receives the destination URL.
8. Frontend Resilience & Axios Interceptors
One of the most technically impressive parts of Snips is the frontend request pipeline (apiInterceptor.js).
The Thundering Herd Problem on Token Expiration
Imagine a dashboard loading 5 API calls in parallel (/links, /overview, /timeseries, /countries, /devices).
If the user's access token expires:
- All 5 requests return
401 Unauthorizedat the exact same moment. - A naive client would fire 5 parallel
/refreshrequests, causing race conditions, multiple token invalidations, and UI crashes.
The Solution: Double-Queue Mutex Interceptor
Request 1 (401) ββ> isRefreshing = false ββ> Set isRefreshing = true ββ> Call /refresh
Request 2 (401) ββ> isRefreshing = true ββ> Enqueue into failedQueue βββββββββ
Request 3 (401) ββ> isRefreshing = true ββ> Enqueue into failedQueue βββββββββ€
β
/refresh succeeds! β
Execute processQueue() <ββββββββββββββββββββ
Replay Requests 2 & 3 with new sessionIn frontend/src/apiInterceptor.js:
- Maintains
isRefreshingboolean andfailedQueue = []. - Maintains
isRefreshingCSRFTokenboolean andcsrfFailedQueue = []. - The first failed request triggers the refresh; all subsequent concurrent requests return a
new Promisewhoseresolveandrejecthandlers are stored in the queue. - Once the refresh completes, all pending promises are resolved and replayed automatically.
- Prevents infinite retry loops by checking
if (originalRequest.url.includes("/refresh")) return Promise.reject(error).
9. Production Scaling & Architectural Evolution
CRITICAL FOR JUSPAY: Interviewers will ask: "What are the bottlenecks of this system, and how would you scale it to 100,000 clicks per second?" Being able to critique your own project and explain the next architectural step is the hallmark of a Senior engineer.
The 3 Bottlenecks in the Baseline Implementation
| Bottleneck | Problem in Baseline Code | Production Solution |
|---|---|---|
| 1. Unawaited Async Execution | trackClick is called as trackClick(...).catch(...) without await in the Express request lifecycle. If 5,000 clicks hit at once, 5,000 asynchronous promises run concurrently in Node's event loop, competing for CPU and DB connections. | Message Queue (BullMQ / Redis Streams): Enqueue a lightweight job { linkId, ip, ua, referer, timestamp } and return immediately. |
| 2. Remote GeoIP Network Call | Baseline calls fetch("http://ip-api.com/json/..."). This introduces external network latency (100β300ms), risk of 429 rate-limiting, and network failure points. | Local MaxMind GeoLite2 (.mmdb): Query the MaxMind binary database locally in memory. Zero network overhead, < 5 microseconds latency. |
| 3. Database Write Contention | Each click executes prisma.$transaction([ clickEvent.create, link.update({ increment: 1 }) ]). Writing a row and updating a counter for every single click creates massive write locks on Postgres. | Batching & Redis Buffer: Buffer click counts in Redis using HINCRBY link:clicks {linkId} 1, then flush batches to Postgres every 5 seconds. |
The Production Target Architecture
Client (Click)
β
βΌ
[ Redirect Handler ] ββ> Read Redis (slug:xyz) ββ> 302 Found (Redirect < 10ms)
β
βΌ (BullMQ.add / Redis Stream XADD)
[ Redis Ingestion Queue ]
β
βΌ (Worker Process / Consumer Pool)
[ BullMQ Worker ]
βββ 1. Parse UA (in-memory)
βββ 2. MaxMind GeoLite2 MMDB lookup (local in-memory binary)
βββ 3. SHA-256 IP anonymization
βββ 4. Batch Insert into PostgreSQL (every 100 events or 1 second)10. Top 15 Juspay "Grill" Questions & Model Answers
Q1: Why did you choose PostgreSQL over MongoDB?
Answer: *"URL shorteners and analytics require strict relational integrity and structured aggregation.
- Relational Integrity: Links belong to Users, and ClickEvents belong to Links. We need ACID guarantees and cascade deletions so that deleting a user or link cleanly purges millions of associated telemetry records without orphan documents.
- Analytical Queries: PostgreSQL has superior analytical supportβ
DATE_TRUNC, window functions, and rich composite B-Tree indexing. MongoDB's document model either leads to unbounded array growth (embedding clicks inside the link document hits the 16MB document limit) or normalized collections that require slow$lookupaggregation pipelines."*
Q2: What happens if Redis crashes? Is your system completely down?
Answer: *"No, the system degrades gracefully:
- On the redirect path, if Redis fails, the code falls back to querying PostgreSQL directly via Prisma (
findUnique({ slug })). Latency increases from ~2ms to ~15ms, but redirects do not fail.- For write resilience in production, Redis can be deployed in a Sentinel or Cluster configuration with Primary-Replica replication and AOF (Append Only File) persistence enabled every second, ensuring durability."*
Q3: How do you prevent a Cache Stampede (Thundering Herd) when a viral link's cache expires?
Answer: *"When a viral link with thousands of QPS expires in Redis, thousands of concurrent requests would miss the cache simultaneously and slam PostgreSQL with the same query. To prevent this:
- Probabilistic Early Expiration (XFetch Algorithm): Recompute and refresh the cache background worker slightly before the TTL actually expires.
- Mutex Locking in Redis: On a cache miss, the first worker acquires a short-lived Redis lock (
SET lock:slug:xyz 1 NX EX 5). Only the lock holder queries the database and populates the cache; all other requests wait 50ms and retry the cache read."*
Q4: How do you prevent Cache Penetration (queries for non-existent slugs)?
Answer: *"If an attacker floods the server with random, non-existent slugs (e.g.,
/:random_string), every request misses Redis and hits PostgreSQL. We solve this in two ways:
- Cache Null Values: If Postgres returns
null, storeslug:xyz -> nullin Redis with a short TTL (e.g. 60 seconds).- Bloom Filter: Maintain a Bloom Filter in Redis of all existing slugs. If the Bloom filter says the slug does not exist, reject the request immediately at the edge without querying Redis key storage or PostgreSQL."*
Q5: Why Base62 instead of Base64 or UUID?
Answer: "1. URL Safety: Base64 includes
+and/, which are reserved characters in URLs and require percent-encoding (%2B,%2F), making the link ugly and longer. Base64 also uses=, which is a padding character. Base62 only uses alphanumeric characters[a-zA-Z0-9], which are 100% URL-safe. 2. Length & Human Friendliness: A UUID is 36 characters long, which defeats the purpose of a URL shortener. 7 characters of Base62 gives 3.52 trillion combinations while fitting easily into SMS, tweets, and QR codes."
Q6: Can someone guess another user's links? How do you prevent link enumeration?
Answer: "Auto-incrementing integer IDs (e.g.
1001,1002) are vulnerable to sequential enumeration attacks. An attacker can crawl every link sequentially. We generate slugs using cryptographically secure random bytes (crypto.randomBytes(7)). With 3.52 trillion possibilities, the key space is sparse. Random probing has a near-zero success rate, preventing link scraping."
Q7: Why did you put the composite index on (linkId, clickedAt) instead of (clickedAt, linkId)?
Answer: "Order of columns in a composite B-Tree index matters strictly due to the Leftmost Prefix Rule. All our queries filter by equality on
linkIdand range onclickedAt:WHERE linkId = 'abc' AND clickedAt >= '2026-01-01'With(linkId, clickedAt), the index is organized first into buckets bylinkId, and within each bucket, rows are strictly sorted by time. Postgres seeks directly to thelinkIdbucket and scans only the required time range. If the index were(clickedAt, linkId), Postgres would have to scan all clicks across all links within that time window and filter out other links, which is significantly slower."
Q8: How does your password protection protect against timing attacks?
Answer: "If you compare strings using standard equality operators (
input === storedPassword), the comparison returnsfalseon the very first mismatched character. An attacker can measure the response time in microseconds to deduce character-by-character correct passwords. We usebcrypt.compare(), which uses a constant-time comparison algorithm under the hood, ensuring the execution time is independent of how many characters match."
Q9: How do you prevent cross-tenant data leakage in analytics queries?
Answer: *"We enforce ownership scoping at the database query level. We have a shared helper:
const getOwnedLink = async (linkId, userId) => { const link = await prisma.link.findUnique({ where: { id: linkId } }); if (!link) throw new NotFoundError(); if (link.userId !== userId) throw new ForbiddenError(); return link; };In overview endpoints, queries are scoped directly with
where: { userId: req.user.id }. Even if a malicious user guesses another user'slinkId, they cannot access the analytics."*
Q10: How do you handle Time Zones accurately in your charts?
Answer: "Clicks occur globally and are stored in UTC (
clickedAt DateTime @default(now())). If a user in India (UTC+5:30) views their dashboard, their 'today' starts 5.5 hours before UTC 'today'. We accept the client's timezone (e.g.,Intl.DateTimeFormat().resolvedOptions().timeZone) and apply double conversion in raw SQL:\"clickedAt\" AT TIME ZONE 'UTC' AT TIME ZONE ${tz}. This shifts the UTC timestamp into the user's local wall-clock time before grouping by hour or day."
Q11: Explain your CSRF defense and why standard JWTs in localStorage are not enough.
Answer: "Storing tokens in
localStoragemakes them accessible to any JavaScript running on the page. If any 3rd-party npm package or CDN script has an XSS vulnerability, the tokens can be stolen. To prevent this, we store tokens inhttpOnlycookies, which JavaScript cannot read. However,httpOnlycookies are automatically attached by the browser on cross-origin requests, opening up CSRF vulnerabilities. To neutralize CSRF, we use a Redis-backed Synchronizer Token Pattern: on login, we generate a random 32-byte CSRF token, store it in Redis undercsrf:{userId}, and require it in thex-csrf-tokenheader for all state-changing HTTP methods. Attackers on external malicious sites cannot read or forge this custom header due to Same-Origin Policy."
Q12: Why did you use crypto.createHash('sha256') on IP addresses instead of storing raw IPs?
Answer: "Under GDPR and CCPA, IP addresses are classified as Personally Identifiable Information (PII). Storing raw IP addresses requires user consent banners, data retention policies, and 'Right to be Forgotten' deletion flows. By hashing the IP address with SHA-256 immediately upon arrival, we anonymize the data. The hash is deterministic, allowing us to execute
COUNT(DISTINCT ipHash)to calculate unique visitors accurately without storing PII."
Q13: If unique clicks grow to 100 million rows, COUNT(DISTINCT ipHash) will become slow. How would you scale it?
Answer: *"At high scale, exact distinct counting in SQL requires sorting or maintaining large in-memory hash tables, which scales $O(N)$ in memory. The industry standard solution is HyperLogLog (HLL), supported natively by Redis (
PFADDandPFCOUNT):
- When a click occurs, execute
PFADD link:unique:{linkId} {ipHash}.- To get unique visitors, execute
PFCOUNT link:unique:{linkId}. HyperLogLog uses a fixed 12 KB of memory per link regardless of whether there are 100 clicks or 1 billion clicks, with a standard error rate of< 0.81%. This is how companies like Reddit and Bitly calculate real-time unique counts."*
Q14: Explain the difference between Node.js event loop behavior for network I/O vs bcrypt password hashing.
Answer: "Network requests (like database queries or Redis calls) use non-blocking asynchronous sockets managed by the OS kernel (via
epollon Linux orIOCPon Windows) through Libuv. They do not block worker threads. In contrast,bcrypt.hash()andbcrypt.compare()are CPU-intensive cryptographic operations. Because JavaScript is single-threaded, running bcrypt synchronously would freeze the entire event loop, stopping all incoming HTTP requests. Thebcryptnpm package delegates the key derivation work to Libuv's background thread pool (default 4 threads). By usingawait bcrypt.compare(), Node executes the computation off the main thread, keeping the event loop responsive to incoming redirects."
Q15: If you were given 2 weeks to rewrite Snips for a 100x traffic spike, what would you do?
Answer: *"I would implement three high-impact optimizations:
- Edge Redirects via Cloudflare Workers / Fastly Compute: Move the
GET /:slugredirect logic from Express to Cloudflare Workers using Cloudflare KV. The redirect would resolve at the CDN edge within 5ms worldwide without ever touching our origin server.- Redis Streams Ingestion Queue: Decouple analytics ingestion completely from the redirect engine. The edge worker emits a fire-and-forget event to a Redis Stream or Kafka topic.
- ClickHouse or TimescaleDB for Analytics: Migrate
ClickEventfrom PostgreSQL to ClickHouse. Columnar databases compress timeseries analytics by 80% and executeGROUP BYaggregations over 100 million rows in milliseconds."*
π‘ Quick Recall Mental Map for the Interview
When asked about:
- Architecture: Think Dual Path (Hot Redirect Path vs Admin/Analytics Path).
- Latency: Think Redis Cache-Aside + Non-blocking Telemetry + MaxMind MMDB.
- Database: Think Composite Index
(linkId, clickedAt)+ DenormalizedtotalClicks+ Cascade Constraints. - Security: Think HttpOnly Dual-Token JWT + Redis Session Revocation + Redis CSRF + Bcrypt Constant-Time.
- Analytics: Think SHA-256 IP Hash + Double
AT TIME ZONEin SQL + HyperLogLog for scale. - Frontend: Think Axios Interceptor double-queue mutex for 401 & CSRF.