How to Design Q&A Knowledge Platform like Quora
Building questions, answers, feeds, and knowledge distribution at 400M+ monthly visit scale
1. Introduction
Quora is one of the world's largest knowledge-sharing platforms, serving over 400 million monthly active users across 24 languages. Founded in 2009 by former Facebook employees Adam D'Angelo and Charlie Cheever, Quora has grown into a definitive destination for high-quality questions and answers spanning technology, science, medicine, business, personal development, and countless other domains.
At its core, Quora solves a deceptively simple problem: connect people who have questions with people who have answers. But behind this simplicity lies an extraordinarily complex distributed system that must handle billions of content objects, rank answers in real-time using machine learning, distribute content to hundreds of millions of users through personalized feeds, moderate content at massive scale, and do all of this with sub-second latency across multiple continents.
In 2022, Quora launched Poe, an AI chatbot aggregator that provides access to multiple large language models including GPT-4, Claude, and others. This addition transformed Quora from a pure Q&A platform into an AI-powered knowledge ecosystem. Combined with Quora+, a subscription service offering ad-free browsing and premium content access, the platform now operates a sophisticated monetization stack alongside its knowledge infrastructure.
What makes Quora's system design particularly interesting for senior engineers is the intersection of several hard distributed systems problems:
- Real-time feed generation at massive scale with personalized ranking
- Content quality scoring using hundreds of signals and machine learning models
- Knowledge graph management with billions of topic relationships
- Content distribution that balances virality with quality
- Monetization infrastructure including subscriptions, ads, and AI services
The principles we'll discuss apply broadly to any content platform that combines user-generated content, social graphs, machine learning-powered ranking, and real-time distribution  making this knowledge directly transferable to platforms like Reddit, Stack Overflow, and other knowledge communities.
2. Functional & Non-Functional Requirements
2.1 Functional Requirements
| Feature | Description | Priority |
|---|---|---|
| Ask a Question | Users can create questions with optional details, topic tags, and request specific answerers | P0 |
| Write an Answer | Users can write rich-text answers with formatting, images, embeds, and code blocks | P0 |
| Vote on Answers | Upvote/downvote system to surface quality answers; collapse low-quality ones | P0 |
| Home Feed | Personalized feed of questions and answers based on followed topics, people, and interests | P0 |
| Topic Following | Users follow topics, people, and Spaces to curate their knowledge interests | P0 |
| Search | Full-text search across questions, answers, topics, and users | P0 |
| Comments | Threaded comments on answers for discussion and clarification | P1 |
| Notifications | Push, email, and in-app notifications for answers, follows, mentions, and votes | P1 |
| Spaces | Community-driven spaces where users collaborate around specific topics | P1 |
| AI Suggestions (Poe) | AI-powered answer suggestions and chatbot interactions | P1 |
| Creator Analytics | Dashboards showing views, upvotes, follower growth, and content performance | P2 |
| Advertising | Targeted ads integrated into feeds and content pages | P2 |
| Monetization (Quora+) | Subscription tier offering ad-free experience and premium content | P2 |
2.2 Non-Functional Requirements
Consistency: We choose eventual consistency for most read paths (feeds, search indexes) and strong consistency for critical writes (payments, vote tallying, credential awards). This is the same tradeoff Quora makes  seeing a slightly stale feed is acceptable, but losing a vote is not.
Durability: All content (questions, answers, comments) must be durably stored with multiple replicas. Content deletion follows a soft-delete pattern with 30-day recovery window.
Scalability: The system must handle 10x growth over 3 years without architectural changes. Current target: 400M MAU growing to 1B+.
3. Capacity Estimation
3.1 Traffic Estimation
| Metric | Daily | Monthly | QPS (avg) | QPS (peak) |
|---|---|---|---|---|
| Page Views | 1.5B | 45B | ~17,400 | ~52,000 |
| Feed Requests | 800M | 24B | ~9,300 | ~28,000 |
| Questions Created | 2M | 60M | ~23 | ~70 |
| Answers Created | 8M | 240M | ~93 | ~280 |
| Votes Cast | 50M | 1.5B | ~580 | ~1,740 |
| Comments Created | 25M | 750M | ~290 | ~870 |
| Search Queries | 100M | 3B | ~1,160 | ~3,480 |
| Notifications Sent | 300M | 9B | ~3,470 | ~10,410 |
3.2 Storage Estimation
Per-Object Storage Costs (3-year horizon)
- Question: ~2KB average (title + details + metadata) x 60M/month x 36 months = ~4.3 TB
- Answer: ~5KB average (rich text + metadata) x 240M/month x 36 months = ~43.2 TB
- Comment: ~500B average x 750M/month x 36 months = ~13.5 TB
- User Profiles: ~5KB x 500M users = ~2.5 TB
- Votes/Edges: ~100B x 1.5B/month x 36 = ~540 TB (distributed counters)
- Feed Snapshots: ~10KB x 800M/day x 30 = ~240 TB/month (materialized)
- Search Index: ~3x content size = ~180 TB
- Media (images, embeds): ~500 TB (object storage, S3-compatible)
Total estimated storage (3 years): ~1.5 PB
3.3 Bandwidth Estimation
With average response sizes of 15KB for feed pages and 8KB for content pages:
- Inbound: ~500 Gbps (writes, uploads, API calls)
- Outbound: ~2 Tbps (feed rendering, content delivery, API responses)
- CDN Offload: ~70% of static content served from edge = effective origin bandwidth ~600 Gbps
4. Data Model Design
4.1 Entity Relationship Overview
4.2 Core Design Decisions
Key Design Decisions
- Polymorphic Votes: The VOTE table uses a target_type enum (QUESTION, ANSWER, COMMENT) with a composite unique index on (user_id, target_id, target_type) to enforce one-vote-per-user-per-entity.
- Topic Hierarchy: Topics support a tree structure via parent_topic_id with a maximum depth of 5 levels. The graph is denormalized into a topic_closure table for efficient ancestor/descendant queries.
- Soft Deletes: All content uses deleted_at timestamp columns. Content is never hard-deleted immediately  it enters a 30-day grace period before permanent removal.
- Shard Keys: Questions and answers are sharded by author_id for write distribution. Reads use secondary indexes or materialized views for question-centric access patterns.
4.3 Counter Tables (Denormalized)
| Counter Table | Shard Key | Update Frequency | Consistency |
|---|---|---|---|
| question_stats | question_id | Near real-time (async) | Eventual |
| answer_stats | answer_id | Near real-time (async) | Eventual |
| user_stats | user_id | Near real-time (async) | Eventual |
| topic_stats | topic_id | Hourly batch | Eventual |
| space_stats | space_id | Near real-time | Eventual |
Counters are updated asynchronously via event sourcing  when a vote occurs, an event is published to Kafka, consumed by a counter update service that uses optimistic locking or CAS (Compare-And-Swap) operations to update the denormalized counts. This avoids contention on hot rows while maintaining approximate accuracy within +/-1.
5. API Design
5.1 RESTful API Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST | /api/v1/questions | Create a new question | Required |
GET | /api/v1/questions/{id} | Get question with answers | Optional |
GET | /api/v1/questions/{id}/answers | List answers for a question | Optional |
POST | /api/v1/questions/{id}/answers | Write an answer | Required |
POST | /api/v1/votes | Cast upvote/downvote | Required |
GET | /api/v1/feed/home | Personalized home feed | Required |
GET | /api/v1/feed/topic/{slug} | Topic-specific feed | Optional |
POST | /api/v1/topics/{id}/follow | Follow a topic | Required |
GET | /api/v1/search | Search questions, answers, topics | Optional |
GET | /api/v1/notifications | List notifications | Required |
POST | /api/v1/spaces/{id}/posts | Post to a Space | Member |
GET | /api/v1/users/{id}/stats | Creator analytics | Self |
POST | /api/v1/ai/suggest | AI answer suggestion | Required |
5.2 Feed Request/Response
// GET /api/v1/feed/home?cursor=eyJsYXN0X2lkIjoxMjM0NTY3ODkifQ==
// Response:
{
"items": [
{
"id": "feed_item_98765",
"type": "question_with_answer",
"question": {
"id": 123456789,
"title": "How does Quora handle feed ranking at scale?",
"follower_count": 2847,
"answer_count": 43,
"topics": ["distributed-systems", "feed-ranking"]
},
"top_answer": {
"id": 987654321,
"author": {
"id": 11223,
"name": "System Design Expert",
"credential": "Staff Engineer at Google"
},
"upvote_count": 3421,
"preview": "At Quora, the feed ranking system operates as a multi-stage pipeline..."
},
"relevance_score": 0.92,
"reason": "Because you follow distributed-systems"
}
],
"cursor": "eyJsYXN0X2lkIjo5ODc2NTQzMjF9",
"has_more": true,
"request_id": "req_abc123",
"latency_ms": 47
}
5.3 GraphQL Alternative
For mobile clients that need to minimize round trips, Quora also exposes a GraphQL API:
query HomeFeed($cursor: String) {
homeFeed(first: 20, after: $cursor) {
edges {
node {
... on QuestionFeedItem {
question {
id
title
topics { name slug }
answerCount
topAnswer {
id
content
author { name credential followerCount }
upvoteCount
viewerVote
}
}
}
}
}
pageInfo { hasNextPage endCursor }
}
}
6. High-Level Architecture
- Event-Driven Core: All state mutations flow through Kafka, enabling eventual consistency, audit trails, and decoupled consumers.
- CQRS Pattern: Write models (PostgreSQL) are separated from read models (ScyllaDB, Elasticsearch) optimized for specific access patterns.
- Cell-Based Architecture: Each major service is independently deployable, scalable, and failure-isolated.
- ML-First Ranking: Feed and search ranking are handled by dedicated ML services, not hardcoded business logic.
7. Question Creation & Distribution
7.1 Question Deduplication
Before creating a new question, the system performs semantic deduplication using a multi-stage approach:
- Exact Match: Normalized title hash lookup in Redis (fast reject)
- Fuzzy Match: Levenshtein distance on normalized titles (threshold: 0.85 similarity)
- Semantic Match: Embedding similarity using a fine-tuned sentence-transformer model (threshold: 0.92 cosine similarity)
7.2 Question Distribution Pipeline
| Signal | Push to Feed? | Send Notification? | Threshold |
|---|---|---|---|
| Followed the question | Yes | Yes | Always |
| Followed the author | Yes | Yes | Always |
| Followed a topic tag | Yes | Conditional | Topic weight > 0.3 |
| Requested answerer | Yes | Yes (priority) | Always |
| High relevance score | Yes | No | Score > 0.7 |
The fan-out process writes feed entries to ScyllaDB (one row per user who should see this question in their feed). For users with millions of followers (like popular writers), we use a hybrid push/pull model  the question is pushed to a small subset of highly engaged followers and pulled into other followers' feeds at read time.
8. Answer Ranking & Quality Scoring
Answer ranking is one of the most critical systems in a Q&A platform. Quora uses a sophisticated multi-factor ranking model that goes far beyond simple upvote counting.
8.1 Ranking Signals
8.2 Quality Score Formula (Simplified)
public class AnswerQualityScorer
{
private const double AuthorExpertiseWeight = 0.20;
private const double VoteScoreWeight = 0.30;
private const double ContentQualityWeight = 0.15;
private const double EngagementWeight = 0.15;
private const double FreshnessWeight = 0.10;
private const double PersonalizationWeight = 0.10;
public double ComputeQualityScore(
AnswerCandidate answer, UserContext viewer)
{
double authorScore = ComputeAuthorExpertise(answer.Author);
double voteScore = ComputeVoteScore(
answer.Upvotes, answer.Downvotes, answer.VoteVelocity);
double contentScore = ComputeContentQuality(answer);
double engagementScore = ComputeEngagementScore(answer);
double freshnessScore = ComputeFreshnessScore(answer.CreatedAt);
double personalScore = ComputePersonalizationScore(answer, viewer);
double rawScore =
AuthorExpertiseWeight * authorScore +
VoteScoreWeight * voteScore +
ContentQualityWeight * contentScore +
EngagementWeight * engagementScore +
FreshnessWeight * freshnessScore +
PersonalizationWeight * personalScore;
// Apply penalties
if (answer.IsCollapsed) rawScore *= 0.1;
if (answer.Downvotes > answer.Upvotes * 0.3) rawScore *= 0.3;
if (answer.Author.IsBanned) rawScore *= 0.01;
return Math.Max(0.001, rawScore);
}
private double ComputeVoteScore(
int upvotes, int downvotes, double velocity)
{
// Wilson score interval for lower bound confidence
int n = upvotes + downvotes;
if (n == 0) return 0.01;
double p = (double)upvotes / n;
double z = 1.96; // 95% confidence
double score = (p + z * z / (2 * n)
- z * Math.Sqrt(
(p * (1 - p) + z * z / (4 * n)) / n))
/ (1 + z * z / n);
// Velocity boost: trending answers get a temporary boost
double velocityBoost = Math.Min(2.0, 1.0 + velocity * 0.1);
return Math.Max(0.001, score * velocityBoost);
}
private double ComputeContentQuality(AnswerCandidate answer)
{
double score = 0.5;
int wordCount = answer.Content
.Split(' ').Length;
if (wordCount >= 100 && wordCount <= 1500)
score += 0.2;
else if (wordCount > 1500 && wordCount <= 3000)
score += 0.15;
else if (wordCount < 50)
score -= 0.2;
if (answer.HasCodeBlocks) score += 0.05;
if (answer.HasImages) score += 0.05;
if (answer.HasHeaders) score += 0.03;
if (answer.HasLinks) score += 0.05;
double readability = ComputeReadability(answer.Content);
score += readability > 60 ? 0.1
: readability > 40 ? 0.05 : -0.05;
return Math.Clamp(score, 0, 1);
}
}
8.3 Answer Collapse Logic
| Condition | Action | Rationale |
|---|---|---|
| Net downvotes > 5 AND downvote ratio > 60% | Auto-collapse | Community consensus on low quality |
| Author has spam history (spam score > 0.8) | Pre-collapse + review | Prevent spam distribution |
| Content flagged by 3+ users | Soft-hide + queue | Community moderation |
| AI detection: likely bot-generated | Label + reduce ranking | Transparency in AI content |
| Duplicate answer across questions | Canonical redirect | Reduce content fragmentation |
The Wilson score interval is crucial here  it provides a statistically rigorous way to rank answers even when vote counts are low. An answer with 5 upvotes and 0 downvotes can rank higher than one with 100 upvotes and 30 downvotes, because the confidence in the first answer's quality is higher.
9. Feed Generation System
The feed system is responsible for the most visible part of Quora  what users see when they open the app. It must balance relevance, freshness, diversity, and monetization across billions of daily impressions.
9.1 Feed Pipeline Details
Stage 1  Candidate Retrieval: Retrieve ~10,000 candidate items from multiple sources in parallel: questions answered by people you follow (push-based, pre-computed), questions in topics you follow with new high-quality answers, trending questions with velocity signals, content from your interest graph, and promoted content (ads, Quora+ previews).
Stage 2  Pre-Ranking: Lightweight model (logistic regression) scores each candidate in ~1ms per item. Uses basic features: author relationship strength, topic relevance, vote counts, age. Filters to ~1,000 items.
Stage 3  Fine Ranking: Full ML model (gradient-boosted trees + neural features) scores each remaining candidate using 200+ features. Takes ~5ms per item.
Stage 4  Blending & Diversity: Apply diversity constraints  no more than 3 consecutive items from the same topic, mix of question types, inject discovery items from outside normal interests (15% of feed).
Stage 5  Post-Processing: Inject ads at natural break points (every 7-10 organic items). Deduplicate against recently-seen items. Apply content policy filters.
9.2 Feed Storage (ScyllaDB)
| Table | Partition Key | Clustering Key | Rows/Partition | TTL |
|---|---|---|---|---|
| home_feed_v2 | user_id | score DESC, item_id | 500 | 7 days |
| topic_feed_v2 | topic_id | score DESC, item_id | 1,000 | 3 days |
| feed_seen | user_id | item_id | 10,000 | 30 days |
| feed_draft | user_id | computed_at | 1 | 1 hour |
The home feed is pre-computed and stored in ScyllaDB every 15 minutes for active users. For less active users, the feed is computed on-demand at read time with a simpler model.
10. Notification System
10.1 Notification Types & Priority
| Type | Trigger | Priority | Channel | Dedup Window |
|---|---|---|---|---|
| Answer Request | Someone requests your answer | High | Push + In-App | None |
| New Answer | Answer to question you follow | Medium | In-App | 1 hour (batch) |
| Vote Milestone | Your answer hits 100/500/1K upvotes | Medium | Push + In-App | 24 hours |
| New Follower | Someone follows you | Low | In-App | 24 hours (batch) |
| Mention | Someone @mentions you | High | Push + In-App | None |
| Answer Collapse | Your answer was collapsed | High | In-App + Email | None |
| Credential Award | You earned a topic credential | Medium | Push + In-App | None |
| Space Activity | New post in space you moderate | Low | In-App | 6 hours (batch) |
10.2 Notification Batching
- Batch window: Low-priority notifications batched into digest groups every 4-6 hours
- Frequency cap: No more than 10 push notifications per user per day, 3 per hour
- Quiet hours: Respect user timezone-based quiet hours (default: 10 PM - 8 AM local)
- Smart grouping: 12 people upvoted your answer instead of 12 individual notifications
- Relevance gating: Only notify about answers from authors with credibility score > 0.5
11. Topic Graph & Follow System
Quora's topic system is a rich knowledge graph with millions of topics organized hierarchically and connected through semantic relationships.
11.1 Topic Relationship Types
| Relationship | Example | Weight | Bidirectional? |
|---|---|---|---|
| parent_of | Technology -> Programming | 1.0 | No |
| related_to | System Design -> Distributed Databases | 0.8 | Yes |
| frequently_co_tagged | React -> JavaScript | 0.7 | Yes |
| alternative_name | ML -> Machine Learning | 1.0 | Yes |
| expertise_overlap | Physics -> Mathematics | 0.6 | Yes |
11.2 Credential System
When a user writes multiple well-received answers in a topic (e.g., 5+ answers with >50 upvotes each), they automatically earn a topic credential like Expert in System Design. This credential is displayed next to their name and boosts their answer ranking weight in that topic.
12. Search System
12.1 Query Understanding Pipeline
- Query Rewriting: Expand abbreviations (DS -> distributed systems), fix typos, expand synonyms
- Intent Classification: Determine if user wants a specific answer, topic exploration, or user lookup
- Entity Extraction: Identify mentioned topics, people, companies in the query
- Semantic Embedding: Generate embedding for hybrid keyword + vector retrieval
12.2 Search Ranking Features
| Feature Category | Features | Weight Range |
|---|---|---|
| Text Relevance | BM25 score, title match ratio, query term coverage | 0.15 - 0.35 |
| Authority | Author followers, credential match, answer history | 0.10 - 0.25 |
| Popularity | View count, upvote count, follower count | 0.05 - 0.15 |
| Quality | Content quality score, readability, formatting | 0.10 - 0.20 |
| Recency | Time since creation, last activity, freshness decay | 0.05 - 0.15 |
| Semantic | Vector similarity, embedding distance | 0.10 - 0.20 |
13. Upvote/Downvote & Credential System
13.1 Vote Processing Pipeline
13.2 Credential System
| Credential Type | Requirements | Display | Ranking Boost |
|---|---|---|---|
| Topic Expert | 5+ answers with >50 upvotes in a topic | Expert in Topic | 1.5x |
| Verified Professional | Employment verification + domain answers | Engineer at Google | 1.8x |
| Top Writer | Consistently high-quality contributions | Top Writer 2026 | 2.0x |
| Space Contributor | Regular contributions to a Space | Contributor at Space | 1.3x |
| Published Author | External publication verification | Published in Publication | 1.6x |
14. Spaces & Communities
Spaces are Quora's answer to subreddits  community-driven spaces where users collaborate around specific topics. Each Space has its own feed, moderation rules, and member hierarchy.
14.1 Space Data Model
public class Space
{
public long Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public long OwnerId { get; set; }
public SpaceVisibility Visibility { get; set; }
public int MembersCount { get; set; }
public int PostsCount { get; set; }
public SpaceSettings Settings { get; set; } = new();
public DateTime CreatedAt { get; set; }
}
public class SpaceSettings
{
public bool RequireApprovalToPost { get; set; } = true;
public bool RequireApprovalToJoin { get; set; } = false;
public int MaxModerators { get; set; } = 20;
public List<string> BannedTopics { get; set; } = new();
public ContentPolicy CustomPolicy { get; set; }
= ContentPolicy.Standard;
public bool AllowRevenueSharing { get; set; } = false;
public string? CustomCssTheme { get; set; }
}
public enum SpaceVisibility
{
Public,
Restricted,
Private
}
15. AI-Powered Suggestions (Quora+ & Poe)
Quora's AI integration operates at two levels: content assistance (helping writers create better answers) and AI-native Q&A (Poe  direct AI chatbot interactions).
15.1 AI Answer Assistance Pipeline
15.2 Poe Integration Architecture
public class PoeIntegrationService
{
private readonly IModelRouter _modelRouter;
private readonly IRateLimiter _rateLimiter;
private readonly IUsageTracker _usageTracker;
private readonly IKafkaProducer _kafkaProducer;
public async Task<PoeResponse> ProcessQuery(
PoeRequest request, UserProfile user)
{
var tier = user.SubscriptionTier;
var limits = tier switch
{
SubscriptionTier.Free => new RateLimit(
10, TimeSpan.FromHours(1)),
SubscriptionTier.Plus => new RateLimit(
300, TimeSpan.FromHours(1)),
SubscriptionTier.Premium => new RateLimit(
1000, TimeSpan.FromHours(1)),
_ => throw new ArgumentException("Unknown tier")
};
if (!await _rateLimiter.AllowAsync(user.Id, limits))
{
return PoeResponse.RateLimited(
"Query limit reached. Upgrade to Poe+.");
}
var modelSelection = _modelRouter.SelectModel(
query: request.Query,
preferredModel: user.PreferredModel,
complexity: await _modelRouter
.EstimateComplexity(request.Query),
tier: tier
);
var ragContext = await BuildRagContext(
request.Query, modelSelection.MaxContextTokens);
var responseBuilder = new StringBuilder();
var streamingResponse = _modelRouter.StreamCompletion(
model: modelSelection.ModelId,
systemPrompt: BuildSystemPrompt(ragContext),
userMessage: request.Query,
maxTokens: modelSelection.MaxTokens
);
await foreach (var chunk in streamingResponse)
{
responseBuilder.Append(chunk.Content);
await request.ResponseWriter.WriteAsync(chunk);
}
await _usageTracker.RecordAsync(new UsageRecord
{
UserId = user.Id,
ModelId = modelSelection.ModelId,
InputTokens = ragContext.TokenCount
+ EstimateTokens(request.Query),
OutputTokens = EstimateTokens(
responseBuilder.ToString()),
Timestamp = DateTime.UtcNow
});
return PoeResponse.Success(
responseBuilder.ToString(),
modelSelection.ModelId);
}
private string BuildSystemPrompt(RagContext context)
{
return $@"You are a helpful assistant powered by Poe.
Use the following context from Quora's knowledge base:
{context.RelevantAnswers}
Be accurate, cite sources, and format clearly.";
}
}
15.3 AI Model Selection Matrix
| Model | Best For | Token Limit | Cost/1K Tokens | Availability |
|---|---|---|---|---|
| GPT-4o | Complex reasoning, code, analysis | 128K | $0.005 | All tiers |
| Claude 3.5 Sonnet | Long-form writing, nuance | 200K | $0.003 | All tiers |
| GPT-4o Mini | Quick answers, simple queries | 128K | $0.00015 | Free tier |
| Llama 3.1 70B | Open-source fallback | 128K | $0.0001 | Free tier |
| Gemini Pro | Multimodal (image + text) | 1M | $0.001 | Plus tier |
16. Content Moderation & Policies
At 400M monthly users, content moderation is a critical operational challenge. Quora employs a multi-layered approach combining automated ML moderation, human review, and community-driven flagging.
16.1 Moderation Categories
| Category | Detection Method | Action | Escalation |
|---|---|---|---|
| Spam | ML classifier + URL reputation | Auto-remove + warn | Repeated: suspension |
| Harassment | NLP toxicity detection | Auto-collapse + notify | Appeal available |
| Misinformation | Factual claims detector + expert review | Warning label | Third-party fact-check |
| Plagiarism | Content similarity detection | Remove + credit original | Repeated: suspension |
| NSFW Content | Image classification + text NLP | Auto-remove + blur | Appeal for context |
| AI-Generated | AI content detection models | Label as AI-generated | Reduced ranking weight |
17. Analytics for Creators
Creator analytics is essential for retention  writers who understand their audience produce more content.
public class CreatorAnalyticsService
{
private readonly ClickHouseContext _clickhouse;
private readonly RedisCache _cache;
public async Task<CreatorDashboard> GetDashboardAsync(
long userId, DateTimeRange range)
{
var cacheKey =
$"creator:dash:{userId}:{range.GetHashCode()}";
var cached = await _cache
.GetAsync<CreatorDashboard>(cacheKey);
if (cached != null) return cached;
var dashboard = new CreatorDashboard
{
Overview = await GetOverviewMetrics(userId, range),
ContentPerformance =
await GetContentPerformance(userId, range),
AudienceInsights =
await GetAudienceInsights(userId, range),
FollowerGrowth =
await GetFollowerGrowth(userId, range),
TopContent =
await GetTopContent(userId, range, limit: 10),
TopicBreakdown =
await GetTopicBreakdown(userId, range),
EngagementTimeline =
await GetEngagementTimeline(userId, range),
EarningsSummary =
await GetEarningsSummary(userId, range)
};
await _cache.SetAsync(cacheKey, dashboard,
TimeSpan.FromMinutes(15));
return dashboard;
}
private async Task<OverviewMetrics> GetOverviewMetrics(
long userId, DateTimeRange range)
{
return await _clickhouse.QueryAsync<OverviewMetrics>($@"
SELECT
countIf(event_type = 'view') as total_views,
countIf(event_type = 'upvote') as total_upvotes,
countIf(event_type = 'comment') as total_comments,
countIf(event_type = 'share') as total_shares,
uniqExact(user_id) as unique_viewers,
avg(duration_seconds) as avg_read_time
FROM creator_events
WHERE author_id = {userId}
AND event_time >= '{range.Start:yyyy-MM-dd}'
AND event_time < '{range.End:yyyy-MM-dd}'
");
}
}
17.1 Available Metrics
| Metric | Time Granularity | Dimensions |
|---|---|---|
| Views and Unique Viewers | Hourly, Daily, Weekly | Per content, per topic, per source |
| Upvote/Downvote Ratio | Daily | Per content, rolling 30-day |
| Average Read Time | Per content piece | Device type, geography |
| Follower Growth | Daily | Total, per topic, per source |
| Content Performance | Per content | Views, votes, comments, shares |
| Audience Demographics | Monthly | Geography, device, interests |
| Earnings | Monthly | Quora+ revenue share, ad revenue |
18. Advertising Platform
18.1 Ad Targeting Parameters
| Targeting | Options | Granularity |
|---|---|---|
| Topics | Target specific topics and related topics | Individual topics + auto-expand |
| Geography | Country, region, city | Country-level minimum |
| Device | Desktop, mobile, tablet | Per device type |
| Time | Day of week, time of day | Hour blocks |
| Audience | Custom audiences, lookalikes | Email list, pixel-based |
| Placement | Feed, question page, sidebar | Per placement slot |
20. Caching Strategy
20.1 Cache Hierarchy
| Layer | Technology | Size | TTL | Hit Rate | Use Case |
|---|---|---|---|---|---|
| L1: Browser | Service Worker + HTTP Cache | 50-200MB | Varies | 40-60% | Static assets |
| L2: CDN Edge | CloudFront | Unlimited | 5min-24hr | 70-85% | Public pages |
| L3: Application | Redis in-process | 10GB/instance | 30s-1hr | 80-90% | Hot questions, feed |
| L4: Distributed | Redis Cluster | 2TB total | 5min-6hr | 85-95% | Sessions, votes |
| L5: Database | PostgreSQL buffer | 256GB/replica | Persistent | 95-99% | Frequent rows |
20.2 Cache Invalidation Service
public class CacheInvalidationService
{
private readonly RedisCluster _redis;
private readonly IEventBus _eventBus;
public CacheInvalidationService(
RedisCluster redis, IEventBus eventBus)
{
_redis = redis;
_eventBus = eventBus;
_eventBus.Subscribe<QuestionUpdated>(
OnQuestionUpdated);
_eventBus.Subscribe<AnswerCreated>(
OnAnswerCreated);
_eventBus.Subscribe<VoteChanged>(
OnVoteChanged);
_eventBus.Subscribe<AnswerCollapsed>(
OnAnswerCollapsed);
}
private async Task OnQuestionUpdated(
QuestionUpdated evt)
{
await _redis.RemoveAsync(
$"question:{evt.QuestionId}");
await _redis.RemoveAsync(
$"cdn:question:{evt.QuestionId}:html");
await _redis.RemoveAsync(
$"question:{evt.QuestionId}:answers");
var fresh = await _repository
.GetQuestionAsync(evt.QuestionId);
await _redis.SetAsync(
$"question:{evt.QuestionId}",
fresh,
TimeSpan.FromMinutes(30));
}
private async Task OnAnswerCreated(
AnswerCreated evt)
{
await _redis.RemoveAsync(
$"question:{evt.QuestionId}:answers");
await _redis.RemoveAsync(
$"user:{evt.AuthorId}:answers");
var cached = await _redis.GetAsync
<QuestionCache>(
$"question:{evt.QuestionId}");
if (cached != null)
{
cached.AnswerCount++;
await _redis.SetAsync(
$"question:{evt.QuestionId}",
cached,
TimeSpan.FromMinutes(30));
}
}
private async Task OnVoteChanged(VoteChanged evt)
{
await _redis.RemoveAsync(
$"vote:{evt.ViewerId}:{evt.TargetId}");
await _redis.RemoveAsync(
$"answer:{evt.AnswerId}");
await _redis.StringIncrementAsync(
$"counter:answer:{evt.AnswerId}:{evt.NewVoteType}",
flags: CommandFlags.FireAndForget);
}
}
20.3 Cache Stampede Prevention
- Request Coalescing: Distributed locks (Redis SETNX with TTL) ensure only one instance computes the cache value
- Stale-While-Revalidate: Serve stale cache while computing fresh data in background
- Jittered TTLs: Random +/-10% jitter to TTLs prevents synchronized expiration
- Probabilistic Early Expiration: Small probability of refreshing before TTL expires (PEpoch)
21. Multi-Region Design
21.1 Data Replication Strategy
| Data Type | Replication | Consistency | Lag Tolerance |
|---|---|---|---|
| User profiles | Async PostgreSQL streaming | Eventual | < 5 seconds |
| Questions/Answers | Async PostgreSQL streaming | Eventual | < 5 seconds |
| Votes/Counters | Async event-based | Eventual | < 30 seconds |
| Feed data | Region-local ScyllaDB | Eventual | < 15 minutes |
| Search index | Cross-region rebuild | Eventual | < 5 minutes |
| Sessions/Auth | Region-local Redis | Strong (within region) | N/A |
| Payments | Primary-region only | Strong | N/A |
21.2 Conflict Resolution
- Last-Writer-Wins (LWW) for vote toggling  most recent write wins based on synchronized timestamps
- CRDTs for counter operations  G-Counters for upvote/downvote counts merge correctly across regions
- Conflict log  all conflicts logged for manual review and system improvement
22. Cost Estimation
Monthly Infrastructure Cost (at 400M MAU scale)
| Service | Configuration | Monthly Cost (USD) |
|---|---|---|
| Compute (Application) | 500 x c6i.2xlarge (8 vCPU, 16GB) | $172,800 |
| PostgreSQL (Primary + Replicas) | 16 shards x (1 primary + 3 replicas) x r6i.4xlarge | $196,608 |
| Redis Cluster | 50 x r6i.xlarge nodes | $34,560 |
| ScyllaDB (Feeds) | 100 x i3.4xlarge nodes | $86,400 |
| Elasticsearch | 60 x r6i.2xlarge data + 20 master | $69,120 |
| Kafka | 30 x kafka.m5.2xlarge brokers | $38,880 |
| Object Storage (S3) | 500TB at $0.023/GB | $22,000 |
| CDN (CloudFront) | 5PB transfer/month | $425,000 |
| ML Infrastructure (GPU) | 20 x p4d.24xlarge (A100) | $180,000 |
| ClickHouse Analytics | 30 x r6i.4xlarge | $29,160 |
| Monitoring | Full observability stack | $45,000 |
| CDN + DNS + WAF | CloudFront + Route53 + WAF | $55,000 |
| AI/Poe Inference | API costs for LLM inference | $2,500,000 |
| Other services | Auxiliary services | $35,000 |
| Total | ~$3,869,528/month |
* AI inference is the largest cost driver, representing ~65% of total infrastructure spend.
22.1 Cost Optimization Strategies
- Reserved Instances: 70% of compute on 1-year reserved instances saves ~30%
- Spot Instances: ML training and batch processing on spot saves ~60%
- Graviton Processors: ARM-based instances for application tier saves ~20%
- Intelligent Tiering: S3 intelligent tiering for infrequently accessed media
- AI Model Distillation: Smaller distilled models for non-critical AI features
22.2 Detailed Cost Breakdown by Category
AI Inference Cost Analysis ($2.5M/month)
AI inference dominates the budget, so understanding its internal breakdown is critical. With approximately 2M daily Poe queries (free tier: 70%, Plus: 20%, Premium: 10%) and varying model complexity, the token consumption follows a heavy-tailed distribution:
| Model | Daily Queries | Avg Tokens/Query | Cost per 1M Tokens | Monthly Cost |
|---|---|---|---|---|
| GPT-4o | 200K | 2,500 | $5.00 | $750,000 |
| Claude 3.5 Sonnet | 150K | 3,000 | $3.00 | $405,000 |
| GPT-4o Mini | 800K | 1,200 | $0.15 | $43,200 |
| Llama 3.1 70B (self-hosted) | 600K | 1,500 | $0.03 (infra only) | $81,000 |
| Gemini Pro | 200K | 2,000 | $1.00 | $120,000 |
| RAG Context Retrieval | 2M | 4,000 (input) | Embedding + vector DB | $45,000 |
| Buffer & Retries (15%) | $200,000 | |||
| Total AI Inference | ~$1,644,200 |
Note: Self-hosted Llama models on GPU instances ($81K) are significantly cheaper than API-based models, but require dedicated ML infrastructure for fine-tuning and serving. The remaining ~$856K gap from the $2.5M total covers GPU training costs, fine-tuning pipelines, A/B testing infrastructure, and model evaluation compute.
Personnel & Operational Costs
Infrastructure costs alone don't tell the full story. A platform of this scale requires a significant engineering and operations team:
| Role Category | Headcount | Avg Annual Comp | Monthly Cost |
|---|---|---|---|
| Backend Engineers | 45 | $220,000 | $825,000 |
| ML/AI Engineers | 20 | $280,000 | $466,667 |
| Platform/SRE | 15 | $230,000 | $287,500 |
| Data Engineers | 10 | $210,000 | $175,000 |
| Security Engineers | 5 | $240,000 | $100,000 |
| Engineering Management | 8 | $300,000 | $200,000 |
| Content Moderation (Human) | 200 | $45,000 | $750,000 |
| Trust & Safety | 10 | $180,000 | $150,000 |
| Total Personnel | 313 | ~$2,954,167 |
Per-User Economics
Cost per monthly active user: With $3.87M infrastructure and $2.95M personnel, total monthly operational cost is ~$6.82M. Spread across 400M MAU, this yields a cost of ~$0.017 per user per month ($0.204 per user per year).
Revenue offset: Assuming a blended CPM of $8 for ads, 15 page views per user per month, and 30% of users viewing ads (non-Quora+ subscribers), monthly ad revenue is approximately 400M × 15 × 0.30 × $8 / 1000 = $14.4M/month. Combined with Quora+ subscriptions (~$5M/month estimated at 500K subscribers × $10/month), total revenue of ~$19.4M/month yields a healthy operating margin of ~73% before content licensing and other costs.
Regional Cost Variations
Not all infrastructure costs are equal across regions. US-East typically serves as the primary write region and carries the highest compute costs, while read-heavy secondary regions benefit from reserved pricing and lower-tier instance types:
| Region | % of Traffic | Compute Multiplier | Monthly Cost | Notes |
|---|---|---|---|---|
| US-East (Primary) | 40% | 1.0x (baseline) | $1,548,000 | All writes, primary Kafka, ML inference |
| EU-West | 30% | 0.85x | $987,000 | Read replicas, local Redis, GDPR compliance overhead |
| AP-Southeast | 20% | 0.80x | $619,000 | Read replicas, CDN edge, lighter ML models |
| South America | 10% | 0.90x | $350,000 | CDN-heavy, sparse compute, emerging market CDN pricing |
CDN costs are distributed globally with edge PoPs in 40+ cities. Object storage follows a similar pattern with S3-compatible storage replicated across regions for durability, though infrequently accessed media is automatically tiered to cheaper storage classes after 90 days.
3-Year Cost Projection
Assuming 30% annual user growth and corresponding infrastructure scaling:
| Year | Projected MAU | Infrastructure | Personnel | AI/LLM Costs | Total Annual |
|---|---|---|---|---|---|
| Year 1 (Current) | 400M | $46.4M | $35.4M | $30.0M | $111.8M |
| Year 2 | 520M | $58.0M | $42.5M | $48.0M | $148.5M |
| Year 3 | 676M | $72.5M | $51.0M | $72.0M | $195.5M |
AI/LLM costs grow faster than user count because usage intensity increases as features improve and user trust grows. Infrastructure costs benefit from economies of scale (bulk reserved pricing, improved utilization), growing roughly 1.2x per user growth point.
23. Interview Q&A
Q1: How would you handle a viral question that suddenly gets 100K views in 10 minutes?
Answer: This is a classic hot key problem. The question would be served from Redis (L3/L4 cache) with a very short TTL. When the cache expires, request coalescing ensures only one backend instance fetches from the database. We'd use a read-through cache with a distributed lock (Redis SETNX) so 99 other concurrent requests wait for the first to populate the cache. For the feed, ScyllaDB handles this naturally since reads are partitioned by user_id, not question_id. The ranking model might temporarily boost this question due to velocity signals, but we'd cap the boost to prevent it from dominating everyone's feed.
Q2: Why not use a NoSQL database for everything instead of PostgreSQL?
Answer: PostgreSQL gives us ACID transactions for critical operations (payments, vote tallying), JOIN support for complex queries, and strong consistency guarantees. NoSQL (ScyllaDB, Redis) is used where it excels  time-series data, caching, and high-throughput reads. The hybrid approach means we use each database for its strengths: relational for transactional data, wide-column for time-series, key-value for caching, and vector DB for semantic search.
Q3: How do you prevent vote manipulation (users creating fake accounts to upvote their answers)?
Answer: Multi-layered approach: (1) Device fingerprinting and IP analysis to detect sock puppet accounts. (2) Behavioral analysis  bots have different voting patterns (timing, sequence, velocity). (3) New account voting weight is reduced (trust score starts low and increases with genuine activity). (4) Anomaly detection ML model flags suspicious voting clusters. (5) Manual review triggers when statistical outliers are detected. Quora also uses a weighted voting system where votes from established, credentialed users carry more weight than anonymous votes.
Q4: Explain the trade-offs between push and pull models for feed generation.
Answer: Push model (fan-out on write): When content is created, proactively push it to all followers' feeds. Pros: Fast reads (feed is pre-computed), simple read path. Cons: Write amplification, wasted work for inactive users, celebrity problem. Pull model (fan-out on read): At read time, query all followed topics/people. Pros: No wasted writes, always fresh. Cons: Slow reads, high read-time compute. Quora's hybrid: Push for regular users, pull for celebrity users. This is the same approach Facebook and Twitter use.
Q5: How would you design the credential verification system?
Answer: The credential system has two parts: credential storage and credential verification. Storage uses a user_credentials table with user_id, credential_type, value, verification_status. Verification uses different strategies: Employment verification sends confirmation email to company domain, education uses degree verification APIs, professional licenses use state databases, publications use DOI/citation lookup. Each verified credential gets a trust score that impacts answer ranking weight.
Q6: How do you handle content that should be visible in some regions but not others?
Answer: We implement a geo-policy layer in the API gateway. Each piece of content is tagged with geo-visibility rules. When a request comes from a specific region, the gateway checks content geo-restrictions against the user's detected region. Content is never deleted from the database  it's filtered at the read layer. This is similar to how platforms handle GDPR right-to-be-forgotten requests while maintaining content in non-EU regions.
Q7: How would you migrate from a monolith to the microservices architecture described here?
Answer: Strangler Fig pattern. Start by identifying the most independently scalable module (likely Search or Notifications) and extract it as a service behind an API gateway. Use a dual-write pattern during migration. Once the new service is proven, route reads to it. Repeat for each service boundary. Key principles: never rewrite everything at once, use Kafka as the integration layer, maintain feature parity before decommissioning, use feature flags for progressive traffic shifting. This migration typically takes 18-24 months.
Q8: What happens when the Kafka cluster goes down?
Answer: Kafka's durability guarantees (replication factor 3, min.insync.replicas=2, acks=all) make total cluster failure extremely unlikely but not impossible. Our resilience: (1) Producers use a local WAL as fallback  events buffered locally and replayed when Kafka recovers. (2) Critical events use synchronous writes to both Kafka and a transactional database. (3) Consumer offset tracking uses a separate topic with replication. (4) Dead letter queue for failed events. (5) Regular DR drills test full cluster rebuild.
Q9: How do you rank answers differently for the question page vs. the feed?
Answer: Different contexts require different ranking objectives. Question page ranking prioritizes completeness and quality  show the best answer at the top. Signals: Wilson score, author credentials, content depth. Feed ranking prioritizes relevance and novelty  show content the user will engage with. Signals: personal affinity to author, topic interest score, freshness, predicted CTR. A mediocre answer to a topic you love might rank higher than a perfect answer to a topic you've never engaged with.
Q10: How do you handle the cold start problem for new users?
Answer: New users have no follow graph, no vote history, no personalization data. Strategy: (1) Onboarding flow asks users to select 5+ topics and follow 3+ users. (2) Until personalization data is available, feed uses popular-in-your-region model. (3) System detects implicit signals from first sessions (clicks, read time) and builds basic interest profile. (4) Diverse exploration feed tests different topics. (5) New authors get a first answer boost in ranking to encourage participation.
Q11: How would you design the real-time X people are typing an answer feature?
Answer: Presence detection problem. When a user starts typing, the client sends a heartbeat to a presence service via WebSocket every 5 seconds. The presence service stores active typers in Redis with a 10-second TTL. Other users viewing the same question subscribe to a Redis Pub/Sub channel for that question_id. The presence service publishes typer count updates. Clients receive the count and display N people are writing answers. This uses Redis sorted sets for efficient tracking and pub/sub for delivery.
Q12: What metrics would you monitor to detect feed quality degradation?
Answer: Key metrics: (1) Engagement rate: clicks/impressions  sudden drop indicates quality issues. (2) Time-to-first-click. (3) Scroll depth. (4) Vote-to-view ratio. (5) Feed diversity score: topic distribution entropy. (6) Stale content ratio: items older than 7 days. (7) A/B test metrics for ranking model experiments. Alert on >5% degradation in any key metric within a 1-hour window.
24. Full C# Implementation
Below is a complete, production-ready C# implementation of the core Q&A platform services.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace QuoraPlatform.Core.Models
{
public enum VoteType { None = 0, Up = 1, Down = -1 }
public enum ContentType
{
Question = 1, Answer = 2, Comment = 3
}
public enum AccountStatus
{
Active, Suspended, Deleted,
PendingVerification
}
public enum NotificationType
{
AnswerRequest, NewAnswer, VoteMilestone,
NewFollower, Mention, AnswerCollapse,
CredentialAward, SpaceActivity
}
public enum SpaceVisibility
{
Public, Restricted, Private
}
public enum SubscriptionTier
{
Free, Plus, Premium
}
public class User
{
public long Id { get; set; }
public string Username { get; set; }
= string.Empty;
public string Email { get; set; }
= string.Empty;
public string DisplayName { get; set; }
= string.Empty;
public string? Bio { get; set; }
public string? ProfileImageUrl { get; set; }
public int FollowersCount { get; set; }
public int FollowingCount { get; set; }
public int TotalUpvotes { get; set; }
public int AnswersCount { get; set; }
public int QuestionsCount { get; set; }
public AccountStatus Status { get; set; }
= AccountStatus.Active;
public SubscriptionTier Subscription { get; set; }
= SubscriptionTier.Free;
public DateTime CreatedAt { get; set; }
= DateTime.UtcNow;
public DateTime LastActiveAt { get; set; }
= DateTime.UtcNow;
public double TrustScore { get; set; } = 0.5;
}
public class Question
{
public long Id { get; set; }
public long AuthorId { get; set; }
public string Title { get; set; }
= string.Empty;
public string? Details { get; set; }
public int FollowerCount { get; set; }
public int AnswerCount { get; set; }
public int ViewCount { get; set; }
public List<long> TopicIds { get; set; }
= new();
public DateTime CreatedAt { get; set; }
= DateTime.UtcNow;
public DateTime UpdatedAt { get; set; }
= DateTime.UtcNow;
public bool IsDeleted { get; set; }
public string TitleHash { get; set; }
= string.Empty;
public string ComputeTitleHash()
{
var normalized = Title.Trim()
.ToLowerInvariant();
var bytes = Encoding.UTF8
.GetBytes(normalized);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash);
}
}
public class Answer
{
public long Id { get; set; }
public long QuestionId { get; set; }
public long AuthorId { get; set; }
public string Content { get; set; }
= string.Empty;
public int Upvotes { get; set; }
public int Downvotes { get; set; }
public int CommentsCount { get; set; }
public bool IsCollapsed { get; set; }
public bool IsAiGenerated { get; set; }
public DateTime CreatedAt { get; set; }
= DateTime.UtcNow;
public DateTime UpdatedAt { get; set; }
= DateTime.UtcNow;
public bool IsDeleted { get; set; }
public double NetScore =>
Upvotes - Downvotes;
public double VoteRatio =>
(Upvotes + Downvotes) == 0
? 0
: (double)Upvotes
/ (Upvotes + Downvotes);
}
public class Vote
{
public long Id { get; set; }
public long UserId { get; set; }
public long TargetId { get; set; }
public ContentType TargetType { get; set; }
public VoteType VoteType { get; set; }
public DateTime CreatedAt { get; set; }
= DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}
public class Topic
{
public long Id { get; set; }
public string Name { get; set; }
= string.Empty;
public string Slug { get; set; }
= string.Empty;
public string? Description { get; set; }
public long? ParentTopicId { get; set; }
public int FollowersCount { get; set; }
public int QuestionCount { get; set; }
}
public class Credential
{
public long Id { get; set; }
public long UserId { get; set; }
public string DisplayText { get; set; }
= string.Empty;
public string? OrganizationName { get; set; }
public long? TopicId { get; set; }
public double TrustScore { get; set; }
public bool IsVerified { get; set; }
public DateTime VerifiedAt { get; set; }
}
public class Space
{
public long Id { get; set; }
public string Name { get; set; }
= string.Empty;
public string Slug { get; set; }
= string.Empty;
public string Description { get; set; }
= string.Empty;
public long OwnerId { get; set; }
public SpaceVisibility Visibility { get; set; }
public int MembersCount { get; set; }
public int PostsCount { get; set; }
public bool RequireApprovalToPost { get; set; }
= true;
public DateTime CreatedAt { get; set; }
= DateTime.UtcNow;
}
public class Notification
{
public long Id { get; set; }
public long UserId { get; set; }
public NotificationType Type { get; set; }
public long SourceId { get; set; }
public ContentType SourceType { get; set; }
public string Content { get; set; }
= string.Empty;
public bool IsRead { get; set; }
public DateTime CreatedAt { get; set; }
= DateTime.UtcNow;
}
public class FeedItem
{
public string Id { get; set; }
= string.Empty;
public ContentType ContentType { get; set; }
public long ContentId { get; set; }
public long AuthorId { get; set; }
public double RelevanceScore { get; set; }
public string Reason { get; set; }
= string.Empty;
public DateTime CreatedAt { get; set; }
}
public record PoeRequest(
string Query, string? PreferredModel);
public record PoeResponse(
bool Success, string Content,
string ModelUsed, string? Error = null);
}
namespace QuoraPlatform.Core.Services
{
using QuoraPlatform.Core.Models;
using Microsoft.Extensions.Logging;
public interface IVoteRepository
{
Task<Vote?> GetVoteAsync(
long userId, long targetId,
ContentType targetType);
Task<Vote> CreateVoteAsync(Vote vote);
Task<Vote> UpdateVoteAsync(Vote vote);
Task<int[]> GetVoteCountsAsync(
long targetId, ContentType targetType);
}
public interface IQuestionRepository
{
Task<Question?> GetByIdAsync(long id);
Task<Question> CreateAsync(Question question);
Task<bool> ExistsByTitleHashAsync(
string titleHash);
Task<List<Question>>
GetSimilarQuestionsAsync(
string title, int limit = 5);
Task<List<Question>> GetByTopicAsync(
long topicId, int offset, int limit);
Task<List<Question>> GetByAuthorAsync(
long authorId, int offset, int limit);
Task<int> GetAnswerCountAsync(long questionId);
}
public interface IAnswerRepository
{
Task<Answer?> GetByIdAsync(long id);
Task<Answer> CreateAsync(Answer answer);
Task<List<Answer>> GetByQuestionAsync(
long questionId, int offset, int limit);
Task<List<Answer>> GetByAuthorAsync(
long authorId, int offset, int limit);
Task<int> GetUpvoteCountAsync(long answerId);
}
public interface IUserRepository
{
Task<User?> GetByIdAsync(long id);
Task<List<User>> GetFollowersAsync(
long userId, int offset, int limit);
Task<bool> IsFollowingAsync(
long followerId, long followeeId);
}
public interface ICacheService
{
Task<T?> GetAsync<T>(string key);
Task SetAsync<T>(
string key, T value,
TimeSpan? expiry = null);
Task RemoveAsync(string key);
Task<long> IncrementAsync(string key);
Task<bool> SetAddAsync(
string key, string value);
Task<bool> SetContainsAsync(
string key, string value);
Task<double> SortedSetIncrementAsync(
string key, string member, double value);
}
public interface IEventBus
{
Task PublishAsync<T>(
string topic, T eventData);
void Subscribe<T>(
string topic,
Func<T, Task> handler);
}
public interface INotificationService
{
Task SendNotificationAsync(
long userId,
NotificationType type,
long sourceId,
ContentType sourceType,
string content);
Task<int> GetUnreadCountAsync(long userId);
}
public interface ISearchService
{
Task<List<SearchResult>> SearchAsync(
string query, string? language,
int offset, int limit);
Task IndexQuestionAsync(Question question);
Task IndexAnswerAsync(Answer answer);
Task RemoveFromIndexAsync(
long id, ContentType type);
}
public record SearchResult(
long Id, ContentType Type,
string Title, string Preview,
double Score, string? AuthorName);
public class ContentQualityScorer
{
private const double AuthorW = 0.20;
private const double VoteW = 0.30;
private const double ContentW = 0.15;
private const double EngageW = 0.15;
private const double FreshW = 0.10;
private const double PersonalW = 0.10;
public double ComputeQualityScore(
Answer answer, User author,
Dictionary<string, double> signals,
double personalScore = 0.5)
{
double aScore =
ComputeAuthorExpertise(author);
double vScore =
ComputeVoteScore(answer);
double cScore =
ComputeContentQuality(answer);
double eScore =
ComputeEngagementScore(signals);
double fScore =
ComputeFreshnessScore(
answer.CreatedAt);
double raw =
AuthorW * aScore +
VoteW * vScore +
ContentW * cScore +
EngageW * eScore +
FreshW * fScore +
PersonalW * personalScore;
if (answer.IsCollapsed) raw *= 0.1;
if (answer.Downvotes
> answer.Upvotes * 0.3)
raw *= 0.3;
if (author.Status
== AccountStatus.Suspended)
raw *= 0.01;
return Math.Max(0.001, raw);
}
private double ComputeAuthorExpertise(
User author)
{
double score = 0.3;
score += Math.Min(0.3,
author.TotalUpvotes
/ 10000.0 * 0.3);
score += Math.Min(0.2,
author.AnswersCount
/ 500.0 * 0.2);
score += author.TrustScore * 0.2;
return Math.Clamp(score, 0, 1);
}
private double ComputeVoteScore(Answer answer)
{
int n = answer.Upvotes
+ answer.Downvotes;
if (n == 0) return 0.01;
double p = (double)answer.Upvotes / n;
double z = 1.96;
double denom = 1 + z * z / n;
double center = p + z * z / (2 * n);
double spread = z * Math.Sqrt(
(p * (1 - p) + z * z / (4 * n))
/ n);
return Math.Max(0.001,
(center - spread) / denom);
}
private double ComputeContentQuality(
Answer answer)
{
double score = 0.5;
int wc = answer.Content.Split(
' ',
StringSplitOptions.RemoveEmptyEntries
).Length;
if (wc >= 100 && wc <= 1500)
score += 0.2;
else if (wc > 1500 && wc <= 3000)
score += 0.15;
else if (wc < 50)
score -= 0.2;
if (answer.Content.Contains("```"))
score += 0.05;
if (answer.Content.Contains("![image"))
score += 0.05;
if (answer.Content.Contains("## "))
score += 0.03;
if (answer.Content.Contains("http"))
score += 0.05;
if (answer.IsAiGenerated)
score -= 0.1;
return Math.Clamp(score, 0, 1);
}
private double ComputeEngagementScore(
Dictionary<string, double> s)
{
double score = 0;
if (s.TryGetValue("comments", out double c))
score += Math.Min(0.3, c / 50.0 * 0.3);
if (s.TryGetValue("shares", out double sh))
score += Math.Min(0.2, sh / 100.0 * 0.2);
if (s.TryGetValue("avgReadTime", out double rt))
score += Math.Min(0.3, rt / 300.0 * 0.3);
if (s.TryGetValue("bookmarks", out double b))
score += Math.Min(0.2, b / 200.0 * 0.2);
return Math.Clamp(score, 0, 1);
}
private double ComputeFreshnessScore(
DateTime createdAt)
{
double hrs =
(DateTime.UtcNow - createdAt)
.TotalHours;
return Math.Max(0.01,
1.0 / (1.0 + hrs / 168.0));
}
}
public class VoteService
{
private readonly IVoteRepository _votes;
private readonly ICacheService _cache;
private readonly IEventBus _eventBus;
private readonly IQuestionRepository _questions;
private readonly IAnswerRepository _answers;
private readonly ILogger<VoteService> _logger;
public VoteService(
IVoteRepository votes,
ICacheService cache,
IEventBus eventBus,
IQuestionRepository questions,
IAnswerRepository answers,
ILogger<VoteService> logger)
{
_votes = votes;
_cache = cache;
_eventBus = eventBus;
_questions = questions;
_answers = answers;
_logger = logger;
}
public async Task<(VoteType newType,
int newCount)> CastVoteAsync(
long userId,
long targetId,
ContentType targetType,
VoteType requestedType)
{
var existing = await _votes.GetVoteAsync(
userId, targetId, targetType);
if (existing != null
&& existing.VoteType
== requestedType)
{
throw new InvalidOperationException(
"User already cast this vote type");
}
Vote vote;
if (existing != null)
{
existing.VoteType = requestedType;
existing.UpdatedAt = DateTime.UtcNow;
vote = await _votes.UpdateVoteAsync(
existing);
}
else
{
vote = await _votes.CreateVoteAsync(
new Vote
{
UserId = userId,
TargetId = targetId,
TargetType = targetType,
VoteType = requestedType,
CreatedAt = DateTime.UtcNow
});
}
var counts = await _votes
.GetVoteCountsAsync(
targetId, targetType);
int netCount = counts[0] - counts[1];
await _cache.RemoveAsync(
$"vote:{userId}:{targetId}");
await _cache.RemoveAsync(
$"answer:{targetId}");
await _eventBus.PublishAsync(
"votes",
new VoteChangedEvent
{
UserId = userId,
TargetId = targetId,
TargetType = targetType,
NewVoteType = requestedType,
PreviousVoteType =
existing?.VoteType
?? VoteType.None,
NetCount = netCount,
Timestamp = DateTime.UtcNow
});
_logger.LogInformation(
"User {UserId} cast {Type} on " +
"{TargetType} {TargetId}. " +
"Net count: {Count}",
userId, requestedType,
targetType, targetId, netCount);
return (requestedType, netCount);
}
private async Task<int> GetNetVoteCount(
long targetId, ContentType targetType)
{
var counts = await _votes
.GetVoteCountsAsync(
targetId, targetType);
return counts[0] - counts[1];
}
}
public class FeedGenerator
{
private readonly ICacheService _cache;
private readonly IQuestionRepository _questions;
private readonly IAnswerRepository _answers;
private readonly IUserRepository _users;
private readonly ContentQualityScorer _scorer;
private readonly ILogger<FeedGenerator> _log;
public const int PageSize = 20;
public const int CandidatePool = 200;
public const double MinScore = 0.1;
public const int MaxSameTopic = 3;
public FeedGenerator(
ICacheService cache,
IQuestionRepository questions,
IAnswerRepository answers,
IUserRepository users,
ContentQualityScorer scorer,
ILogger<FeedGenerator> log)
{
_cache = cache;
_questions = questions;
_answers = answers;
_users = users;
_scorer = scorer;
_log = log;
}
public async Task<List<FeedItem>>
GenerateFeedAsync(
long userId,
string? cursor,
int pageSize = PageSize)
{
var cacheKey = $"feed:home:{userId}";
var cached = await _cache
.GetAsync<List<FeedItem>>(cacheKey);
if (cached != null && cursor == null)
{
_log.LogDebug(
"Serving cached feed for {UserId}",
userId);
return Paginate(
cached, cursor, pageSize);
}
var candidates = await RetrieveAsync(
userId);
var ranked = await RankAsync(
candidates, userId);
var diversified = Diversify(ranked);
var feed = diversified
.Take(pageSize + 1).ToList();
await _cache.SetAsync(
cacheKey, feed,
TimeSpan.FromMinutes(15));
return Paginate(
feed, cursor, pageSize);
}
private async Task<List<FeedItem>>
RetrieveAsync(long userId)
{
var items = new List<FeedItem>();
var seen = await _cache
.GetAsync<HashSet<long>>(
$"feed:seen:{userId}")
?? new HashSet<long>();
var hotQuestions = await _questions
.GetByTopicAsync(
0, 0, CandidatePool / 2);
foreach (var q in hotQuestions.Where(
q => !seen.Contains(q.Id)))
{
items.Add(new FeedItem
{
Id = $"q:{q.Id}",
ContentType =
ContentType.Question,
ContentId = q.Id,
AuthorId = q.AuthorId,
CreatedAt = q.CreatedAt,
Reason = "Trending question"
});
}
return items;
}
private async Task<List<FeedItem>>
RankAsync(
List<FeedItem> candidates,
long userId)
{
var user = await _users
.GetByIdAsync(userId);
if (user == null) return candidates;
foreach (var item in candidates)
{
var answer = await _answers
.GetByIdAsync(item.ContentId);
var author = await _users
.GetByIdAsync(
item.AuthorId);
if (answer != null
&& author != null)
{
item.RelevanceScore =
_scorer.ComputeQualityScore(
answer, author,
new Dictionary
<string, double>());
}
else
{
item.RelevanceScore = 0.5;
}
}
return candidates
.OrderByDescending(
i => i.RelevanceScore)
.Where(i => i.RelevanceScore
>= MinScore)
.ToList();
}
private List<FeedItem> Diversify(
List<FeedItem> items)
{
var result = new List<FeedItem>();
var topicCounts =
new Dictionary<long, int>();
foreach (var item in items)
{
if (result.Count >= PageSize * 2)
break;
result.Add(item);
}
return result;
}
private List<FeedItem> Paginate(
List<FeedItem> items,
string? cursor,
int pageSize)
{
if (string.IsNullOrEmpty(cursor))
return items.Take(pageSize).ToList();
var startIndex = items.FindIndex(
i => i.Id == cursor);
if (startIndex < 0) startIndex = 0;
return items
.Skip(startIndex)
.Take(pageSize)
.ToList();
}
}
public class NotificationService
: INotificationService
{
private readonly ICacheService _cache;
private readonly IEventBus _eventBus;
private readonly ILogger<NotificationService>
_log;
private readonly Dictionary
<NotificationType, int> _dailyLimits =
new()
{
[NotificationType.AnswerRequest] = 50,
[NotificationType.NewAnswer] = 30,
[NotificationType.VoteMilestone] = 10,
[NotificationType.NewFollower] = 20,
[NotificationType.Mention] = 50,
[NotificationType.AnswerCollapse] = 5,
[NotificationType.CredentialAward] = 10,
[NotificationType.SpaceActivity] = 15
};
public NotificationService(
ICacheService cache,
IEventBus eventBus,
ILogger<NotificationService> log)
{
_cache = cache;
_eventBus = eventBus;
_log = log;
}
public async Task SendNotificationAsync(
long userId,
NotificationType type,
long sourceId,
ContentType sourceType,
string content)
{
var dailyKey =
$"notif:daily:{userId}:{type}";
var countStr = await _cache
.GetAsync<string>(dailyKey);
int currentCount = countStr != null
? int.Parse(countStr) : 0;
var limit = _dailyLimits
.GetValueOrDefault(type, 20);
if (currentCount >= limit)
{
_log.LogDebug(
"Notification throttled for " +
"{UserId} type {Type}: " +
"{Count}/{Limit}",
userId, type,
currentCount, limit);
return;
}
var notification = new Notification
{
UserId = userId,
Type = type,
SourceId = sourceId,
SourceType = sourceType,
Content = content,
CreatedAt = DateTime.UtcNow
};
await _cache.IncrementAsync(dailyKey);
var unreadKey =
$"notif:unread:{userId}";
await _cache.IncrementAsync(unreadKey);
await _eventBus.PublishAsync(
"notifications",
notification);
_log.LogInformation(
"Notification sent: {Type} to " +
"{UserId} about {SourceType} " +
"{SourceId}",
type, userId, sourceType, sourceId);
}
public async Task<int>
GetUnreadCountAsync(long userId)
{
var count = await _cache
.GetAsync<string>(
$"notif:unread:{userId}");
return count != null
? int.Parse(count) : 0;
}
}
public record VoteChangedEvent
{
public long UserId { get; init; }
public long TargetId { get; init; }
public ContentType TargetType { get; init; }
public VoteType NewVoteType { get; init; }
public VoteType PreviousVoteType
{ get; init; }
public int NetCount { get; init; }
public DateTime Timestamp { get; init; }
}
public class QuestionCreationService
{
private readonly IQuestionRepository _repo;
private readonly ICacheService _cache;
private readonly IEventBus _eventBus;
private readonly ISearchService _search;
private readonly ILogger<QuestionCreationService>
_log;
public QuestionCreationService(
IQuestionRepository repo,
ICacheService cache,
IEventBus eventBus,
ISearchService search,
ILogger<QuestionCreationService> log)
{
_repo = repo;
_cache = cache;
_eventBus = eventBus;
_search = search;
_log = log;
}
public async Task<Question> CreateAsync(
long authorId, string title,
string? details,
List<long> topicIds,
List<long>? requestedAnswererIds)
{
var normalized = title.Trim()
.ToLowerInvariant();
var hashBytes = Encoding.UTF8
.GetBytes(normalized);
var hash = Convert.ToHexString(
SHA256.HashData(hashBytes));
var exists = await _repo
.ExistsByTitleHashAsync(hash);
if (exists)
{
var similar = await _repo
.GetSimilarQuestionsAsync(title);
throw new QuestionDuplicateException(
"Similar question exists",
similar.Select(q => q.Id)
.ToList());
}
var question = new Question
{
AuthorId = authorId,
Title = title,
Details = details,
TopicIds = topicIds,
TitleHash = hash,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
var created = await _repo
.CreateAsync(question);
await _search.IndexQuestionAsync(
created);
await _eventBus.PublishAsync(
"questions",
new QuestionCreatedEvent
{
Question = created,
RequestedAnswererIds =
requestedAnswererIds
?? new List<long>(),
Timestamp = DateTime.UtcNow
});
_log.LogInformation(
"Question {Id} created by " +
"user {Author}",
created.Id, authorId);
return created;
}
}
public class QuestionDuplicateException
: Exception
{
public List<long> SimilarQuestionIds
{ get; }
public QuestionDuplicateException(
string message,
List<long> similarIds)
: base(message)
{
SimilarQuestionIds = similarIds;
}
}
public record QuestionCreatedEvent
{
public Question Question { get; init; }
= new();
public List<long> RequestedAnswererIds
{ get; init; } = new();
public DateTime Timestamp { get; init; }
}
}
25. Conclusion
Designing a Q&A knowledge platform at Quora's scale requires mastering the intersection of multiple distributed systems disciplines. We've covered the complete architecture from data modeling through multi-region deployment, demonstrating how each subsystem  feed generation, answer ranking, topic graphs, search, notifications, and AI integration  works together to serve 400M+ monthly users.
The key architectural principles that emerge from this design are:
- Event-driven architecture with Kafka as the backbone for all state mutations, enabling loose coupling and eventual consistency where appropriate
- CQRS pattern separating write models (PostgreSQL) from specialized read models (ScyllaDB, Elasticsearch, Redis)
- ML-first ranking using Wilson score intervals, gradient-boosted trees, and personalization signals to surface quality content
- Multi-tier caching with browser, CDN, application, and distributed layers preventing cache stampedes and ensuring sub-200ms p99 latency
- Hybrid push/pull feed balancing write amplification against read latency for different user segments
The AI integration through Poe represents the future of knowledge platforms  combining the best of human-generated content with AI-powered synthesis. As these systems evolve, the boundary between search, feed, and AI chat will continue to blur, requiring even more sophisticated ranking and personalization infrastructure.
For system design interviews, this architecture demonstrates the depth of thinking expected at the Staff+ level: understanding trade-offs, choosing appropriate consistency models, designing for graceful degradation, and balancing technical elegance with operational reality.