snips_interview_guide.md

Snips (URL Shortener) — How to Explain It in the MathWorks Managerial Round


🎙️ The Pitch — Three Levels of Detail

Level 1: 30-Second Version (When they say "give me a brief overview")

"I built a URL shortener — think of it as a self-hosted Bitly. You paste a long URL, get a short shareable link, and can track every click with detailed analytics: countries, devices, browsers, referrers, and a time-series graph. On the security side, it has two-factor authentication using OTP over email, access tokens that refresh automatically, CSRF protection, and rate limiting. Interesting technical parts: every redirect lookup hits Redis first so the database barely gets involved, and click tracking is fully non-blocking — the user is already redirected before any analytics write happens. Stack: React, Node.js/Express, PostgreSQL with Prisma, and Redis."


Level 2: 2-Minute Structured Version (Standard "tell me about your project")

"The problem I was solving: most URL shorteners are SaaS products with per-click pricing and no ownership of your data. I wanted to build one I fully control, with analytics baked in.

So I built a full-stack URL shortener with three main pillars:

Link management: Users create short links from long URLs — either with an auto-generated random slug or a custom one they choose. Each link can have a title, tags, expiry date, active/inactive toggle, and an optional password for gating access.

Redirect engine: When someone clicks a short link, the backend first checks Redis. If the link data is cached, it redirects immediately — no database query at all. Click tracking is done fire-and-forget after the redirect, so the user experience is instant. The analytics pipeline records IP hash (for unique clicks), country and city from a geo-IP lookup, browser, device, OS, and referrer.

Security system: Authentication is two-factor — email + OTP. I use short-lived access tokens (1 hour) stored in httpOnly cookies, a separate refresh token (7 days), and a CSRF token. One device per user is enforced — logging in on a new device invalidates the previous session in Redis. Rate limiting sits at both the global level (express-rate-limit) and per-endpoint level using Redis keys for login and OTP flows.

Tech stack: React + Tailwind on the frontend, Node.js/Express backend, PostgreSQL on Neon with Prisma, Redis for caching and session management.

What I'd change: The click tracking uses a free geo-IP API that adds latency and has no SLA. In production I'd replace it with a self-hosted GeoIP database like MaxMind GeoLite2. I'd also add a proper job queue for analytics writes instead of fire-and-forget."


Level 3: Deep Technical (For follow-up questions)

Use the specific architecture and decisions covered in the question sections below.


📋 Every Managerial Question — Answered Using This Project


Q1: "Tell me about a project you made (overview)"

Use Level 2 pitch above. Always:

  • Start with the problem (SaaS URL shorteners cost money, no data ownership)
  • Name three clear pillars (link management, redirect engine, security)
  • Highlight one impressive technical decision (Redis-first redirect, non-blocking click tracking)
  • End with one thing you'd change (geo-IP dependency)

Q2: "What difficulties did you face in the project?"

Pick 2-3 — the most interesting:

Difficulty 1 — Single-session enforcement (most impressive)

"I wanted to enforce one active session per user — if you log in on a new device, the old session should be killed. The challenge was doing this statelessly with JWTs, since JWTs are self-contained and can't be revoked.

My solution: I store an active-session:<userId> key in Redis pointing to the current session ID. When a new login happens, I delete the old session's Redis keys and overwrite active-session with the new session ID. On every authenticated request, the auth middleware checks whether the JWT's session ID matches the active one in Redis. If it doesn't, the request is rejected with 'Another device logged in.' The old device gets kicked out cleanly even though the JWT hasn't expired yet."

Difficulty 2 — CSRF protection with cookie-based auth

"I'm using httpOnly cookies for JWTs — they're XSS-proof because JavaScript can't read them. But that makes them vulnerable to CSRF — a malicious site could trigger authenticated requests on behalf of the user. I implemented the double-submit cookie pattern: on login, the server generates a CSRF token, stores it in Redis keyed by user ID, and also sends it as a cookie. Sensitive state-changing requests must include this token in the request header (which an attacker's page can't read due to same-origin policy). The server validates the header value against the Redis-stored one. This protects all mutating endpoints without compromising the httpOnly JWT security."

Difficulty 3 — Non-blocking redirect with accurate analytics

"The redirect needs to be near-instant — the user should be bouncing to their destination in milliseconds. But tracking a click involves a geo-IP API call, a database write, and an atomic increment of totalClicks. If I awaited all of that before redirecting, the redirect would take 200-500ms.

The pattern I used: res.redirect() first, then trackClick().catch(console.error) without await. The user is gone before analytics even starts. The transaction atomically creates the ClickEvent row and increments totalClicks in a single Prisma transaction, so the count is always consistent even if multiple clicks arrive concurrently."


Q3: "What did you learn from the project?"

Lesson 1 — Security is layered, not one thing

"This was the first project where I implemented a full security stack — bcrypt for passwords, httpOnly cookies for tokens, CSRF protection, rate limiting at multiple levels, OTP for 2FA. The learning: security is layered defense. Each layer protects against a specific attack vector. Removing any one of them doesn't make the system 'mostly secure' — it creates a gap. CSRF protection only matters because I have httpOnly cookies; rate limiting only matters if someone's trying brute force. The layers work together."

Lesson 2 — Caching forces you to think about consistency

"Adding Redis as a cache was straightforward. The hard part was cache invalidation. When a user updates or toggles a link, the cached slug data in Redis is stale. I had to explicitly call redisClient.del('slug:${slug}') in every update path — createLink, updateLink, deleteLink, and toggleLink. Missing any one of those would serve stale data to people clicking that link. The lesson: a cache isn't free — every write path now has two responsibilities: update the database AND invalidate the cache."

Lesson 3 — Zod schemas as the contract between frontend and backend

"I used Zod for input validation. What surprised me was how it changed my thinking — I started defining the schema first and writing the handler second, rather than the other way around. The schema became the contract: if a field is optional, it's optional everywhere. The structured error output from parsed.error.flatten() also made the frontend error display trivially easy to implement. I'd use this pattern in every future project."


Q4: "Discuss the scalability of the project" / "What are the bottlenecks?"

"The system has a good foundation for scale — Redis-first redirects mean most traffic never touches the database. But there are three bottlenecks:

First: Click analytics — the geo-IP lookup uses a free third-party API (ip-api.com) with rate limits and no SLA. At scale, every redirect would trigger an external HTTP call that can fail or add latency. The fix: import MaxMind GeoLite2 locally — one-time download, instantaneous lookup, no rate limits.

Second: Analytics writes — currently fire-and-forget Prisma transactions. At high click volumes, the database write queue could back up. The fix: a proper job queue like BullMQ with Redis as the broker. Each click enqueues a job; a worker processes them with bounded concurrency and retries on failure.

Third: Single-process architecture — Express, Redis client, and Prisma all run in one Node.js process. A crash takes everything down. The fix: deploy the API server separately from a worker process; use a process manager like PM2 with cluster mode to use all CPU cores.

For horizontal scaling: the API server is stateless — JWT verification happens locally, and session state lives in Redis. You can run multiple API instances behind a load balancer with round-robin routing. The only shared state between instances is Redis, and that's already the design."


Q5: "What improvements would you make in your project?"

"Six specific improvements, in order of impact:

  1. Self-hosted GeoIP — Replace ip-api.com with MaxMind GeoLite2. Eliminates external API dependency, adds zero latency, removes rate limit risk.
  2. Job queue for analytics — BullMQ + Redis instead of fire-and-forget. Gives retries, dead-letter queue, and monitoring for failed click events.
  3. Slug caching at the edge — Currently Redis cache has a 24-hour TTL. At scale, put Cloudflare in front — popular slugs get cached at the CDN edge, eliminating round-trips to the server entirely.
  4. Refresh token rotation — Currently, the refresh token is reused for its entire 7-day lifetime. Rotating it on every use (issue a new one, invalidate the old) closes a token theft window.
  5. Rate limiting on the redirect endpoint — Currently only auth and link management endpoints are rate-limited. The /:slug redirect route has no rate limit — a bot could fire millions of clicks to inflate analytics. Add per-IP rate limiting on the slug route.
  6. Link preview endpoint — Before redirecting, show the user a preview page with the destination domain, especially for password-protected links. Reduces phishing risk."

Q6: "Verification of numbers/metrics on your resume"

Know these cold:

MetricValueHow You Know
Access token expiry1 hour (expiresIn: '1h')generateToken.js line 18
Refresh token expiry7 days (expiresIn: '7d')generateToken.js line 20
Redis cache TTL for slug24 hours (86400s)linkController.js line 175
Redis cache TTL for user1 hourcheckAuth.js line 42
OTP expiry5 minutes (300s)userController.js — redis EX: 300
OTP max attempts5 before OTP is deleteduserController.js — attempts > 5
Verify token expiry5 minutes (300s)userController.js — redis EX: 300
Global rate limit1000 requests per 15 minutes per IPindex.js line 38-43
Resend OTP rate limit60-second cooldown per IP+emailuserController.js line 214
DB models3 (User, Link, ClickEvent)Prisma schema
Analytics dimensions6 (country, city, browser, device, OS, referrer)analytics.js trackClick
Timeseries ranges4 (today by hour, 7d, 30d, all)analyticsController.js
Click uniqueness methodSHA-256 hash of IP addressanalytics.js line 61

[!CAUTION] If asked about traffic numbers or load capacity — be honest: "This is a solo project, so I haven't done formal load testing. Architecturally, the Redis-first redirect means most traffic doesn't hit the database — it can handle significantly more than a database-first approach."


Q7: "Do you prefer to work in a team or individually?"

"This project was individual, and I felt the full weight of every decision — especially security ones. When you're alone, there's no one to ask 'did I miss anything here?' before shipping. I implemented OTP 2FA, CSRF protection, rate limiting, session management — and I'm reasonably confident they're correct, but a code review from someone who's built security systems before would have given me much more confidence.

That experience made me appreciate what team reviews actually do: not just catch bugs, but validate your mental model of the threat landscape. For design-phase decisions — especially security and data architecture — I strongly prefer working in a team. For focused implementation once the design is locked, I work well independently.

MathWorks' design review culture — where you actively seek others to challenge your design — is exactly what I'd have wanted on this project."


Q8: "What opportunity did you take initiative on?"

"The single-session enforcement. Most URL shorteners don't think about this at all — they issue a JWT and leave it at that. I noticed that if someone's credentials were compromised, the attacker could stay logged in indefinitely as long as the JWT was valid, even if the user reset their password.

I took the initiative to design a session tracking layer on top of JWTs using Redis. Each login generates a unique session ID stored in Redis. A new login kicks out the previous session by deleting its Redis keys. The auth middleware on every request validates that the JWT's session ID is still the active one. This wasn't in any tutorial I followed — I designed it by thinking through the attack scenario and working backward to the solution."


Q9: "What criticism did you receive / what went wrong?"

"Two things I'd self-criticize:

First, the geo-IP dependency. I used ip-api.com, a free API with a 45-requests-per-minute limit and no uptime guarantee. In the analytics code, I have a try-catch that silently swallows geo lookup failures — which means some click events get recorded with no country or city. In a real product, silent data loss is never acceptable. I should have either used a reliable paid service or a local GeoLite2 database from the start.

Second, click analytics can still be gamed. I use IP hashing for unique click detection, but a user behind a shared network (like a university WiFi) would look like a single unique visitor even if thousands of people click. And someone with a VPN can rotate IPs to inflate unique click counts. I know the limitations but haven't built around them — a production system would need device fingerprinting or at minimum a cookie-based deduplication layer."


Q10: "Learnings from your project / internship"

"Three things:

First: Every external dependency is a liability. The geo-IP API is the weakest link in the system — it can be rate-limited, go down, or change its response format. Anything external should be treated as unreliable and wrapped in error handling that fails gracefully, not silently.

Second: Redis is a tool, not just a cache. I ended up using Redis for six different purposes: slug caching, session management, OTP storage, verification tokens, rate limiting, and CSRF tokens. Each use case has different TTL, key naming conventions, and invalidation logic. Managing all of this without a coherent naming scheme would have become a mess — I learned to be deliberate about key namespacing (e.g., slug:, session:, otp:, verify-token:, csrf:).

Third: Security decisions are hard to undo. I made the correct choice of httpOnly cookies early on, but CSRF protection came later as an afterthought. If I'd shipped without CSRF protection and then had users, adding it would have required a breaking change to every API client. Security architecture needs to be planned upfront, not retrofitted."


🔗 Connecting Project Decisions to MathWorks Core Values

Project DecisionMaps to MathWorks Value
Single-session enforcement — designed proactively, not requiredContinuous Improvement — "Encourage initiative, creativity, and being leading edge"
Consistent Redis key namespacing (slug:, session:, otp:)Continuous Improvement — "Sweat the details — they make all the difference"
Non-blocking redirect (user first, analytics second)Rational Workplace — "Balance seemingly contradictory goals" (speed and accuracy)
Zod schema-first developmentCredibility and Integrity — "Do what you say you will do" (schema is the contract you commit to)
Honest self-criticism on geo-IP silent failuresCredibility and Integrity — "Be forthright with bad news and difficult issues"
Designing session-revocation from scratch by thinking through attacksLearning and Self-Improvement — "Take initiative and ownership"

⚡ Quick Reference Cheat Sheet

Project: Snips — URL Shortener with Analytics Type: Full-stack web app (solo project) Domain: Developer tools — link management, analytics, security Stack: React 19 + Tailwind | Node.js/Express 5 | PostgreSQL (Neon) Prisma ORM | Redis | Zod | Helmet | express-rate-limit Auth: 2FA (email + OTP) | httpOnly cookies | JWT (access 1h + refresh 7d) CSRF double-submit | Single active session per user Caching: Redis-first redirects (24h TTL) | User profile cache (1h TTL) Key Feature #1: Redis-first redirect engine → Check Redis before touching the database → Click tracking is fire-and-forget (user already redirected) → Analytics: country, city, browser, device, OS, referrer, IP hash Key Feature #2: Session management with single-device enforcement → Redis tracks active-session:<userId> → sessionId mapping → New login kicks out old session automatically → Auth middleware validates session on every request Key Feature #3: Multi-layer rate limiting → Global: 1000 req / 15 min per IP (express-rate-limit) → Login/register: Redis key with 30s TTL per IP+email → OTP resend: 60s cooldown per IP+email → OTP attempts: max 5 per OTP before invalidation Main Bottleneck: ip-api.com geo lookup (external, rate-limited, no SLA) Main Regret: No job queue — analytics are fire-and-forget with no retries Main Learning: Security is layered; each layer covers a specific attack vector