How to Approach System Design Interviews
Every system design question follows the same pattern. The interviewer gives a vague requirement ("design Twitter") and expects you to drive the conversation systematically. Use a four-step framework:
- Requirements — Clarify functional and non-functional requirements before drawing anything
- High-level design — APIs, main components, data flow
- Detailed design — Deep-dive into the 2-3 components the interviewer cares about
- Scale and optimise — Identify bottlenecks and address them
The biggest mistake is skipping step 1. Engineers jump to architecture diagrams before establishing scale, consistency requirements, or which features to prioritise. An interviewer who asks "design a URL shortener" wants to see that you know what questions to ask first.
Question 1: "Design a URL Shortener"
30-Second Answer
A URL shortener maps short codes to long URLs, stored in a database. Short codes are generated by encoding a numeric ID or hashing the URL. Key components: API, database with indexed code lookups, caching for popular URLs, and a CDN for redirect performance.
Strong Answer
Step 1 — Requirements:
- Expected scale: 100M new URLs/day? 10B redirects/day?
- Custom aliases? (e.g.,
bit.ly/my-brand) - URL expiry?
- Analytics (click counts, referrers)?
At 100M new URLs/day: ~1,160 writes/second. At 10B redirects/day: ~116K reads/second. This is read-heavy (100:1 read-to-write ratio). Cache aggressively.
Step 2 — API:
POST /shorten { "longUrl": "...", "customAlias": "my-link" }
GET /{code} → 301 redirect to long URL
GET /analytics/{code} → click stats
Step 3 — Short code generation:
Option A: Auto-increment ID → Base62 encode
public string Encode(long id)
{
const string chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
var sb = new StringBuilder();
while (id > 0) {
sb.Insert(0, chars[(int)(id % 62)]);
id /= 62;
}
return sb.ToString().PadLeft(7, '0'); // 7 chars = 62^7 = 3.5 trillion URLs
}
Option B: MD5/SHA256 hash of URL, take first 7 chars. Simpler, but collision risk requires checking the database.
Auto-increment ID is better: no collisions, predictable distribution.
Step 4 — Data model:
CREATE TABLE ShortUrls (
Code VARCHAR(10) PRIMARY KEY,
LongUrl NVARCHAR(2048) NOT NULL,
CreatedAt DATETIME2 NOT NULL,
ExpiresAt DATETIME2 NULL,
CreatedByUserId INT NULL
);
CREATE INDEX IX_ShortUrls_ExpiresAt ON ShortUrls(ExpiresAt);
Step 5 — Redirect performance:
- Redis cache for hot URLs (LRU eviction, 24h TTL)
- Cache hit rate should be > 90% (Zipf distribution — 20% of URLs get 80% of traffic)
- Use 301 (permanent) for non-expiring URLs: browser caches, reduces server load
- Use 302 (temporary) for analytics URLs: every request hits the server for tracking
Scale:
- Read: Redis cluster + CDN edge caching
- Write: Single SQL Server with auto-increment ID (write throughput ~5K/sec sufficient for 1,160/sec)
- Analytics: Write clicks to Kafka, process asynchronously, store aggregates in ClickHouse
Common Follow-up Questions
- "How do you handle custom aliases?" — Store in the same table, use a separate unique constraint on alias column
- "How do you prevent abuse?" — Rate limiting on POST endpoint (Redis sliding window counter)
- "How does expiry work?" — Background job scans
ExpiresAt < NOW()and deletes or marks as expired
Red Flags (What NOT to Say)
- Jumping to microservices without justifying the scale
- Proposing NoSQL without explaining why SQL falls short at this scale
- Not discussing the 301 vs 302 trade-off for analytics
- Ignoring caching entirely
Question 2: "Design a Notification System"
30-Second Answer
A notification system receives notification requests from services, routes them by channel (email, SMS, push), and delivers them reliably with retries. Key components: notification API, channel routers, channel-specific adapters (email provider, SMS provider), delivery status tracking.
Strong Answer
Requirements to clarify:
- Channels: email, SMS, push, in-app?
- Scale: 1M notifications/day? 100M?
- Delivery guarantees: at-least-once? Exactly-once?
- Templates vs raw content?
- Priority tiers? (transactional vs marketing)
Architecture:
Service A ─────┐
Service B ────→│ Notification API → Message Queue → Channel Workers → Email/SMS/Push providers
Service C ─────┘ │
Delivery Status DB
API:
public record SendNotificationCommand(
Guid UserId,
NotificationChannel Channel, // Email, SMS, Push, InApp
string TemplateName,
Dictionary<string, string> TemplateData,
NotificationPriority Priority); // Transactional, Marketing
User preference service: Users have notification preferences (opt-out of marketing, preferred channel). The notification service checks preferences before routing.
Channel workers: Separate consumers per channel, independently scalable.
public class EmailNotificationConsumer : IConsumer<EmailNotificationMessage>
{
public async Task Consume(ConsumeContext<EmailNotificationMessage> context)
{
// Idempotency check (message may be redelivered)
if (await _tracking.IsAlreadySentAsync(context.Message.NotificationId))
return;
var result = await _emailProvider.SendAsync(
to: context.Message.ToAddress,
subject: context.Message.Subject,
htmlBody: context.Message.Body);
await _tracking.RecordDeliveryAsync(
context.Message.NotificationId,
result.IsSuccess ? DeliveryStatus.Delivered : DeliveryStatus.Failed,
result.ErrorMessage);
}
}
Retry and dead letter: Configure exponential backoff for transient failures (SMTP timeout). After max retries, move to dead letter queue for manual inspection.
Scale considerations:
- Separate queues for transactional (high priority, low volume) and marketing (low priority, high volume)
- Marketing emails can be batched and rate-limited to provider limits (e.g., SendGrid 10K/hour on free tier)
- Push notifications: fan-out requires platform-specific adapters (APNs for iOS, FCM for Android)
Follow-up Questions
- "How do you handle rate limits from email providers?" — Token bucket limiter on the consumer, back-pressure from the queue
- "How do you track delivery status?" — Store delivery records; webhook receivers from providers update status
- "What if the notification service is down?" — Events sit in the queue; services that publish to the queue are unaffected
Red Flags
- Sending notifications synchronously in the order processing flow (blocks the response, single point of failure)
- No idempotency — duplicate notifications are a serious user experience issue
- Not addressing preference management
Question 3: "How Would You Design a Rate Limiter?"
30-Second Answer
A rate limiter rejects requests that exceed a configured threshold. Algorithms: fixed window (count requests per minute), sliding window (more accurate), token bucket (allows bursting), leaky bucket (smooth output rate). For distributed systems, use Redis to store counters so all instances share state.
Strong Answer
Token bucket with Redis (allows controlled bursting):
public class RedisTokenBucketRateLimiter
{
private readonly IConnectionMultiplexer _redis;
public async Task<RateLimitResult> TryAcquireAsync(
string key,
int requestedTokens = 1,
int bucketCapacity = 100,
int refillRatePerSecond = 20)
{
var db = _redis.GetDatabase();
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// Lua script for atomic check-and-decrement
var script = @"
local key = KEYS[1]
local now = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local refill_rate = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1]) or capacity
local last_refill = tonumber(data[2]) or now
-- Refill tokens based on elapsed time
local elapsed_seconds = (now - last_refill) / 1000
tokens = math.min(capacity, tokens + elapsed_seconds * refill_rate)
if tokens >= requested then
tokens = tokens - requested
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600)
return 1 -- Allow
else
return 0 -- Deny
end";
var result = (int)await db.ScriptEvaluateAsync(
script,
new RedisKey[] { $"ratelimit:{key}" },
new RedisValue[] { now, bucketCapacity, refillRatePerSecond, requestedTokens });
return result == 1 ? RateLimitResult.Allow : RateLimitResult.Deny;
}
}
ASP.NET Core integration:
// Built-in rate limiting (NET 7+)
builder.Services.AddRateLimiter(options =>
{
options.AddSlidingWindowLimiter("per-user", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 6; // 10-second segments
opt.QueueLimit = 0; // Reject immediately, no queue
});
});
Response when rate limited:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000030
Question 4: "How Would You Scale a .NET API from 100 to 1M Users?"
30-Second Answer
Scaling happens in layers: first make the service stateless (sessions in Redis, files in blob storage), add a load balancer, scale horizontally. Then add caching (Redis) for hot data, read replicas for database reads, and CDN for static assets. At extreme scale, consider sharding or moving hot data to appropriate stores.
Strong Answer
Level 1 (100 → 10K users): Make it stateless, deploy multiple instances behind a load balancer.
- Move sessions to Redis
- Move file uploads to Azure Blob Storage
- Use IDistributedCache for shared state
Level 2 (10K → 100K users): Cache hot data.
- Cache-aside pattern for read-heavy data (product catalogue, user profiles)
- Database read replica for reporting queries
- CDN for static assets
Level 3 (100K → 1M users): Database scaling.
- Read replicas for read-heavy load
- Database partitioning for time-series tables
- Consider NoSQL for specific workloads (document storage, user sessions)
What the interviewer wants to hear: You know the order of operations. Premature sharding is a mistake. Caching and read replicas solve most scaling problems. Know when NOT to shard.
General Red Flags in System Design Interviews
- Not asking clarifying questions before designing
- Jumping to exotic technology without justifying why simple solutions fail
- Ignoring failure modes (what happens when the cache is down? what if the message queue is full?)
- No discussion of monitoring and observability
- Treating CAP theorem as theoretical — interviewers want to know what it means for your design
- Forgetting about security (authentication, rate limiting, input validation)