system-design48 min read

How to Design Q&A Knowledge Platform like Quora — A Senior+ Guide | Ayodhyya

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
What you'll learn: This article walks through the complete system design of a Quora-like platform, from data modeling to multi-region deployment, covering every major subsystem with Mermaid diagrams, C# implementations, capacity estimates, and the kind of depth expected in a Staff+ engineering interview.

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

FeatureDescriptionPriority
Ask a QuestionUsers can create questions with optional details, topic tags, and request specific answerersP0
Write an AnswerUsers can write rich-text answers with formatting, images, embeds, and code blocksP0
Vote on AnswersUpvote/downvote system to surface quality answers; collapse low-quality onesP0
Home FeedPersonalized feed of questions and answers based on followed topics, people, and interestsP0
Topic FollowingUsers follow topics, people, and Spaces to curate their knowledge interestsP0
SearchFull-text search across questions, answers, topics, and usersP0
CommentsThreaded comments on answers for discussion and clarificationP1
NotificationsPush, email, and in-app notifications for answers, follows, mentions, and votesP1
SpacesCommunity-driven spaces where users collaborate around specific topicsP1
AI Suggestions (Poe)AI-powered answer suggestions and chatbot interactionsP1
Creator AnalyticsDashboards showing views, upvotes, follower growth, and content performanceP2
AdvertisingTargeted ads integrated into feeds and content pagesP2
Monetization (Quora+)Subscription tier offering ad-free experience and premium contentP2

2.2 Non-Functional Requirements

99.99%
Availability SLA
<200ms
p99 Feed Latency
<100ms
p99 Read Latency
50K
QPS Reads
5K
QPS Writes
500PB
Total Data

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

MetricDailyMonthlyQPS (avg)QPS (peak)
Page Views1.5B45B~17,400~52,000
Feed Requests800M24B~9,300~28,000
Questions Created2M60M~23~70
Answers Created8M240M~93~280
Votes Cast50M1.5B~580~1,740
Comments Created25M750M~290~870
Search Queries100M3B~1,160~3,480
Notifications Sent300M9B~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

erDiagram USER ||--o{ QUESTION : creates USER ||--o{ ANSWER : writes USER ||--o{ COMMENT : posts USER ||--o{ VOTE : casts USER ||--o{ TOPIC_FOLLOW : follows USER ||--o{ USER_FOLLOW : follows USER ||--o{ NOTIFICATION : receives QUESTION ||--o{ ANSWER : has QUESTION ||--o{ TOPIC_TAG : tagged QUESTION ||--o{ COMMENT : has ANSWER ||--o{ VOTE : receives ANSWER ||--o{ COMMENT : has TOPIC ||--o{ QUESTION : categorizes TOPIC ||--o{ ANSWER : categorizes TOPIC }o--o{ TOPIC : parent_child SPACE ||--o{ SPACE_MEMBER : contains SPACE ||--o{ SPACE_POST : features USER ||--o{ SPACE_MEMBER : belongs_to USER { bigint id PK string username string email text bio string profile_image_url int followers_count int credits datetime created_at enum account_status } QUESTION { bigint id PK bigint author_id FK string title text details int followers_count int answer_count int view_count enum status datetime created_at } ANSWER { bigint id PK bigint question_id FK bigint author_id FK text content int upvotes int downvotes bool is_collapsed bool is_ai_generated datetime created_at } TOPIC { bigint id PK string name string slug text description bigint parent_topic_id FK int followers_count } VOTE { bigint id PK bigint user_id FK bigint target_id FK enum target_type enum vote_type datetime created_at } COMMENT { bigint id PK bigint parent_id FK bigint user_id FK bigint target_id FK text content int upvotes datetime created_at } SPACE { bigint id PK string name string slug text description bigint owner_id FK enum visibility int members_count }

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 TableShard KeyUpdate FrequencyConsistency
question_statsquestion_idNear real-time (async)Eventual
answer_statsanswer_idNear real-time (async)Eventual
user_statsuser_idNear real-time (async)Eventual
topic_statstopic_idHourly batchEventual
space_statsspace_idNear real-timeEventual

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

MethodEndpointDescriptionAuth
POST/api/v1/questionsCreate a new questionRequired
GET/api/v1/questions/{id}Get question with answersOptional
GET/api/v1/questions/{id}/answersList answers for a questionOptional
POST/api/v1/questions/{id}/answersWrite an answerRequired
POST/api/v1/votesCast upvote/downvoteRequired
GET/api/v1/feed/homePersonalized home feedRequired
GET/api/v1/feed/topic/{slug}Topic-specific feedOptional
POST/api/v1/topics/{id}/followFollow a topicRequired
GET/api/v1/searchSearch questions, answers, topicsOptional
GET/api/v1/notificationsList notificationsRequired
POST/api/v1/spaces/{id}/postsPost to a SpaceMember
GET/api/v1/users/{id}/statsCreator analyticsSelf
POST/api/v1/ai/suggestAI answer suggestionRequired

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

graph TB subgraph Clients[Client Layer] Web[Web App React] iOS[iOS App] Android[Android App] API_Ext[External API] end subgraph Edge[Edge Layer] CDN[CDN - CloudFront] LB[Load Balancer - Envoy] WAF[WAF - Rate Limiter] end subgraph Gateway[API Gateway] GW[API Gateway - Auth Routing] GraphQL[GraphQL Federation] end subgraph Services[Application Services] QS[Question Service] AS[Answer Service] FS[Feed Service] NS[Notification Service] VS[Vote Service] SS[Search Service] TS[Topic Service] SpS[Space Service] AIS[AI Service Poe] ModS[Moderation Service] AdsS[Ads Service] AnS[Analytics Service] US[User Service] end subgraph Async[Async Processing] Kafka[Apache Kafka] Workers[Background Workers] Scheduler[Cron Scheduler] end subgraph ML[ML Pipeline] Ranker[Ranking Model] RecSys[Recommendation Engine] NLP[NLP Pipeline] ModML[Moderation ML] end subgraph Data[Data Layer] PG[PostgreSQL Users Q and A] Redis[Redis Cluster Cache] ES[Elasticsearch Search] S3[Object Storage Media] Scylla[ScyllaDB Feeds] Neo4j[Neo4j Topic Graph] ClickHouse[ClickHouse Analytics] end Web --> CDN --> LB iOS --> LB Android --> LB API_Ext --> LB LB --> WAF --> GW GW --> GraphQL GraphQL --> QS GraphQL --> AS GraphQL --> FS GraphQL --> NS GraphQL --> VS GraphQL --> SS GraphQL --> TS GraphQL --> SpS GraphQL --> AIS GraphQL --> US QS --> Kafka AS --> Kafka VS --> Kafka NS --> Kafka Kafka --> Workers Workers --> FS Workers --> NS Workers --> ModS FS --> Ranker FS --> RecSys ModS --> ModML AIS --> NLP QS --> PG AS --> PG US --> PG QS --> Redis AS --> Redis VS --> Redis FS --> Scylla SS --> ES TS --> Neo4j AnS --> ClickHouse AS --> S3
  • 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

sequenceDiagram participant User participant GW as API Gateway participant QS as Question Service participant Kafka participant TS as Topic Service participant NS as Notification Service participant FS as Feed Service participant SI as Search Index participant MS as Moderation Service User->>GW: POST /api/v1/questions GW->>QS: Create Question QS->>QS: Validate and deduplicate QS->>QS: Check similar questions via Elasticsearch alt Similar question exists QS-->>User: 409 Conflict with similar question IDs else New question QS->>QS: Store in PostgreSQL QS->>Kafka: Publish QuestionCreated event QS-->>User: 201 Created with question_id end Kafka->>TS: Index question-topic associations Kafka->>SI: Index question text and metadata Kafka->>MS: Run automated content moderation Kafka->>FS: Fan-out to followers feeds Kafka->>NS: Notify requested answerers

7.1 Question Deduplication

Before creating a new question, the system performs semantic deduplication using a multi-stage approach:

  1. Exact Match: Normalized title hash lookup in Redis (fast reject)
  2. Fuzzy Match: Levenshtein distance on normalized titles (threshold: 0.85 similarity)
  3. Semantic Match: Embedding similarity using a fine-tuned sentence-transformer model (threshold: 0.92 cosine similarity)

7.2 Question Distribution Pipeline

SignalPush to Feed?Send Notification?Threshold
Followed the questionYesYesAlways
Followed the authorYesYesAlways
Followed a topic tagYesConditionalTopic weight > 0.3
Requested answererYesYes (priority)Always
High relevance scoreYesNoScore > 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

graph LR subgraph Signals[Ranking Signals] V[Vote Signals Upvotes Downvotes Velocity] A[Author Signals Credentials History Expertise] Q[Quality Signals Length Formatting Readability] E[Engagement Signals Comments Shares Time-on-Read] T[Temporal Signals Recency Decay Trending] P[Personalization User Interests Follow Graph] end subgraph Model[Ranking Model] GBM[Gradient Boosted Decision Tree] NN[Neural Ranker] Blending[Score Blending] end V --> GBM A --> GBM Q --> GBM E --> GBM T --> NN P --> NN GBM --> Blending NN --> Blending

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

ConditionActionRationale
Net downvotes > 5 AND downvote ratio > 60%Auto-collapseCommunity consensus on low quality
Author has spam history (spam score > 0.8)Pre-collapse + reviewPrevent spam distribution
Content flagged by 3+ usersSoft-hide + queueCommunity moderation
AI detection: likely bot-generatedLabel + reduce rankingTransparency in AI content
Duplicate answer across questionsCanonical redirectReduce 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.

graph TB subgraph Input[Feed Inputs] FollowGraph[Follow Graph] ContentPool[Content Pool] UserPrefs[User Preferences] end subgraph Pipeline[Feed Pipeline] Retrieval[1. Candidate Retrieval ~10000] PreRank[2. Pre-Ranking ~1000] Ranking[3. Fine Ranking ~200] Blending[4. Blending and Diversity] PostProc[5. Post-Processing Ads Dedup] end subgraph Output[Feed Output] HomeFeed[Home Feed] TopicFeed[Topic Feed] NotifFeed[Notification Feed] end FollowGraph --> Retrieval ContentPool --> Retrieval UserPrefs --> Retrieval Retrieval --> PreRank --> Ranking --> Blending --> PostProc PostProc --> HomeFeed PostProc --> TopicFeed PostProc --> NotifFeed

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)

TablePartition KeyClustering KeyRows/PartitionTTL
home_feed_v2user_idscore DESC, item_id5007 days
topic_feed_v2topic_idscore DESC, item_id1,0003 days
feed_seenuser_iditem_id10,00030 days
feed_draftuser_idcomputed_at11 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

sequenceDiagram participant Event as Event Source participant Kafka participant NP as Notification Policy participant NS as Notification Store participant Push as Push Service participant Email as Email Service participant Device as User Device Event->>Kafka: AnswerCreated or VoteCast Kafka->>NP: Process notification event NP->>NP: Check user preferences NP->>NP: Check frequency caps NP->>NP: Batch similar notifications alt User Online NP->>NS: Store notification NS-->>Device: Real-time WebSocket push else User Offline NP->>NS: Store notification NP->>Push: Send push notification Push-->>Device: APNs or FCM push end

10.1 Notification Types & Priority

TypeTriggerPriorityChannelDedup Window
Answer RequestSomeone requests your answerHighPush + In-AppNone
New AnswerAnswer to question you followMediumIn-App1 hour (batch)
Vote MilestoneYour answer hits 100/500/1K upvotesMediumPush + In-App24 hours
New FollowerSomeone follows youLowIn-App24 hours (batch)
MentionSomeone @mentions youHighPush + In-AppNone
Answer CollapseYour answer was collapsedHighIn-App + EmailNone
Credential AwardYou earned a topic credentialMediumPush + In-AppNone
Space ActivityNew post in space you moderateLowIn-App6 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.

graph TB subgraph TopicGraph[Topic Knowledge Graph] Tech[Technology] Prog[Programming] SysD[System Design] DistrDB[Distributed Databases] Cache2[Caching] WebDev[Web Development] Science[Science] Physics[Physics] Quantum[Quantum Computing] Math[Mathematics] end Tech --> Prog Tech --> SysD Tech --> WebDev SysD --> DistrDB SysD --> Cache2 Science --> Physics Physics --> Quantum Science --> Math Tech --> Science

11.1 Topic Relationship Types

RelationshipExampleWeightBidirectional?
parent_ofTechnology -> Programming1.0No
related_toSystem Design -> Distributed Databases0.8Yes
frequently_co_taggedReact -> JavaScript0.7Yes
alternative_nameML -> Machine Learning1.0Yes
expertise_overlapPhysics -> Mathematics0.6Yes

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.



13. Upvote/Downvote & Credential System

13.1 Vote Processing Pipeline

sequenceDiagram participant User participant VS as Vote Service participant Redis participant Kafka participant CS as Counter Service participant FS as Feed Service participant RS as Ranking Service User->>VS: POST /api/v1/votes answer_id type UP VS->>Redis: Check if user already voted via SETNX alt Already voted same type VS-->>User: 409 Already voted else Already voted different type VS->>VS: Update vote type swap VS->>Kafka: VoteChanged event else New vote VS->>VS: Insert vote record VS->>Kafka: VoteCast event end VS-->>User: 200 OK new_vote_count Kafka->>CS: Update answer counters Kafka->>FS: Recalculate feed scores Kafka->>RS: Trigger answer re-ranking

13.2 Credential System

Credential TypeRequirementsDisplayRanking Boost
Topic Expert5+ answers with >50 upvotes in a topicExpert in Topic1.5x
Verified ProfessionalEmployment verification + domain answersEngineer at Google1.8x
Top WriterConsistently high-quality contributionsTop Writer 20262.0x
Space ContributorRegular contributions to a SpaceContributor at Space1.3x
Published AuthorExternal publication verificationPublished in Publication1.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.

graph TB subgraph SpaceA[Space: System Design Practice] SA_Owner[Owner] SA_Mods[Moderators] SA_Contrib[Contributors] SA_Readers[Readers] end subgraph Features[Space Features] Feed2[Curated Feed] Rules[Custom Rules] Analytics2[Space Analytics] Moderation2[Content Moderation] Invite[Invite System] Revenue[Revenue Sharing] end SA_Owner --> SA_Mods SA_Mods --> SA_Contrib SA_Contrib --> SA_Readers SpaceA --> Features

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

graph LR subgraph AISystem[AI Suggestion System] Question[Question Input] Context[Context Retrieval] LLM[LLM Generation] Quality2[Quality Filter] Output[Suggested Answer] end Question --> Context --> LLM --> Quality2 --> Output Context --> RAG[RAG Pipeline] Context --> UserHist[User Writing History] Context --> WebSearch[Web Search]

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

ModelBest ForToken LimitCost/1K TokensAvailability
GPT-4oComplex reasoning, code, analysis128K$0.005All tiers
Claude 3.5 SonnetLong-form writing, nuance200K$0.003All tiers
GPT-4o MiniQuick answers, simple queries128K$0.00015Free tier
Llama 3.1 70BOpen-source fallback128K$0.0001Free tier
Gemini ProMultimodal (image + text)1M$0.001Plus 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.

graph TB subgraph Layers[Multi-Layer Moderation] L1[Layer 1: Pre-Publish ML Filter] L2[Layer 2: Post-Publish Batch ML Scan] L3[Layer 3: Community Flagging] L4[Layer 4: Human Review Queue] L5[Layer 5: Appeals Process] end Content[New Content] --> L1 L1 -->|Pass| Published[Published] L1 -->|Flag| L2 L2 -->|Clean| Published L2 -->|Suspect| L3 L3 -->|Confirmed| L4 L4 -->|Violation| Removed[Removed] L4 -->|False Positive| Published L4 -->|Appeal| L5 L5 -->|Upheld| Removed L5 -->|Overturned| Published

16.1 Moderation Categories

CategoryDetection MethodActionEscalation
SpamML classifier + URL reputationAuto-remove + warnRepeated: suspension
HarassmentNLP toxicity detectionAuto-collapse + notifyAppeal available
MisinformationFactual claims detector + expert reviewWarning labelThird-party fact-check
PlagiarismContent similarity detectionRemove + credit originalRepeated: suspension
NSFW ContentImage classification + text NLPAuto-remove + blurAppeal for context
AI-GeneratedAI content detection modelsLabel as AI-generatedReduced 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

MetricTime GranularityDimensions
Views and Unique ViewersHourly, Daily, WeeklyPer content, per topic, per source
Upvote/Downvote RatioDailyPer content, rolling 30-day
Average Read TimePer content pieceDevice type, geography
Follower GrowthDailyTotal, per topic, per source
Content PerformancePer contentViews, votes, comments, shares
Audience DemographicsMonthlyGeography, device, interests
EarningsMonthlyQuora+ revenue share, ad revenue

18. Advertising Platform

sequenceDiagram participant User participant FS as Feed Service participant AdS as Ad Service participant Auction as Auction Service participant AdStore as Ad Store User->>FS: Request home feed FS->>FS: Generate organic feed 15 items FS->>AdS: Request ads 3 slots user context AdS->>AdS: Match ads to user targeting AdS->>Auction: Run real-time auction Auction->>AdStore: Fetch bid amounts and budgets Auction->>Auction: Calculate eCPM for each ad Auction-->>AdS: Winner ads with pricing AdS-->>FS: 3 ads with tracking IDs FS->>FS: Interleave ads into feed FS-->>User: 18 items 15 organic plus 3 ads

18.1 Ad Targeting Parameters

TargetingOptionsGranularity
TopicsTarget specific topics and related topicsIndividual topics + auto-expand
GeographyCountry, region, cityCountry-level minimum
DeviceDesktop, mobile, tabletPer device type
TimeDay of week, time of dayHour blocks
AudienceCustom audiences, lookalikesEmail list, pixel-based
PlacementFeed, question page, sidebarPer placement slot

19. Database Sharding Strategy

graph TB subgraph ShardLayer[Shard Router] Router[Consistent Hashing Router] end subgraph Shards[PostgreSQL Shards - 16 total] S0[Shard 0] S1[Shard 1] S2[Shard 2] S3[Shard 3] SN[Shard N] S15[Shard 15] end subgraph Replicas[Read Replicas x3 per shard] R0R[Shard 0 Replicas] R1R[Shard 1 Replicas] RNR[Shard N Replicas] end Router --> S0 Router --> S1 Router --> S2 Router --> S3 Router --> SN Router --> S15 S0 --> R0R S1 --> R1R SN --> RNR

19.1 Shard Key Selection

EntityShard KeyShard CountRationale
Usersuser_id16Primary entity, evenly distributed
Questionsauthor_idCo-located with usersWrite locality
Answersauthor_idCo-located with usersWrite locality
Votestarget_id256High volume, separate cluster
Commentstarget_id64Co-located with parent content
Notificationsuser_idCo-located with usersRead locality
Feeduser_idScyllaDBTime-series optimized

19.2 Cross-Shard Query Handling

  1. Materialized Views: Question-centric views pre-computed and stored in a separate shard
  2. Scatter-Gather: Fan out to all shards and merge (used sparingly due to latency)
  3. Denormalized Reads: Answer data denormalized into question shard for read-heavy access
public class ShardRouter
{
    private const int ShardCount = 16;
    private readonly Dictionary<int, DatabaseConnection>
        _shardConnections;

    public ShardRouter(IConfiguration config)
    {
        _shardConnections =
            new Dictionary<int, DatabaseConnection>();
        for (int i = 0; i < ShardCount; i++)
        {
            var connString = config[
                $"Shards:Shard_{i}:ConnectionString"];
            _shardConnections[i] =
                new DatabaseConnection(connString!);
        }
    }

    public int GetShardId(long entityId)
    {
        return (int)(entityId % ShardCount);
    }

    public DatabaseConnection GetShard(long entityId)
    {
        var shardId = GetShardId(entityId);
        return _shardConnections[shardId];
    }

    public async Task<T> ExecuteOnShard<T>(
        long shardKey,
        Func<DatabaseConnection, Task<T>> operation)
    {
        var shard = GetShard(shardKey);
        return await operation(shard);
    }

    public async Task<List<T>> ScatterGather<T>(
        Func<DatabaseConnection, Task<List<T>>> operation)
    {
        var tasks = _shardConnections.Values
            .Select(connection => operation(connection))
            .ToList();
        var results = await Task.WhenAll(tasks);
        return results
            .SelectMany(r => r).ToList();
    }

    public async Task<Dictionary<long, T>>
        ExecuteOnSpecificShards<T>(
            IEnumerable<long> shardKeys,
            Func<DatabaseConnection, IEnumerable<long>,
                Task<Dictionary<long, T>>> operation)
    {
        var grouped = shardKeys
            .GroupBy(GetShardId)
            .ToDictionary(
                g => g.Key, g => g.ToList());
        var tasks = grouped.Select(async kvp =>
        {
            var shard = _shardConnections[kvp.Key];
            return await operation(shard, kvp.Value);
        });
        var results = await Task.WhenAll(tasks);
        return results
            .SelectMany(r => r)
            .ToDictionary(
                r => r.Key, r => r.Value);
    }
}

20. Caching Strategy

20.1 Cache Hierarchy

LayerTechnologySizeTTLHit RateUse Case
L1: BrowserService Worker + HTTP Cache50-200MBVaries40-60%Static assets
L2: CDN EdgeCloudFrontUnlimited5min-24hr70-85%Public pages
L3: ApplicationRedis in-process10GB/instance30s-1hr80-90%Hot questions, feed
L4: DistributedRedis Cluster2TB total5min-6hr85-95%Sessions, votes
L5: DatabasePostgreSQL buffer256GB/replicaPersistent95-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

graph TB subgraph Global[Global Infrastructure] GSLB[Global Load Balancer Route53] end subgraph US[US-East Primary] US_App[Application Cluster] US_DB[PostgreSQL Primary Write] US_Redis[Redis Cluster] US_Kafka[Kafka Cluster] end subgraph EU[EU-West] EU_App[Application Cluster] EU_DB[PostgreSQL Read Replica] EU_Redis[Redis Cluster] EU_Kafka[Kafka MirrorMaker] end subgraph APAC[AP-Southeast] APAC_App[Application Cluster] APAC_DB[PostgreSQL Read Replica] APAC_Redis[Redis Cluster] APAC_Kafka[Kafka MirrorMaker] end GSLB --> US_App GSLB --> EU_App GSLB --> APAC_App US_DB -.->|Async Replication| EU_DB US_DB -.->|Async Replication| APAC_DB US_Kafka -.->|MirrorMaker 2| EU_Kafka US_Kafka -.->|MirrorMaker 2| APAC_Kafka

21.1 Data Replication Strategy

Data TypeReplicationConsistencyLag Tolerance
User profilesAsync PostgreSQL streamingEventual< 5 seconds
Questions/AnswersAsync PostgreSQL streamingEventual< 5 seconds
Votes/CountersAsync event-basedEventual< 30 seconds
Feed dataRegion-local ScyllaDBEventual< 15 minutes
Search indexCross-region rebuildEventual< 5 minutes
Sessions/AuthRegion-local RedisStrong (within region)N/A
PaymentsPrimary-region onlyStrongN/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)

ServiceConfigurationMonthly 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 Cluster50 x r6i.xlarge nodes$34,560
ScyllaDB (Feeds)100 x i3.4xlarge nodes$86,400
Elasticsearch60 x r6i.2xlarge data + 20 master$69,120
Kafka30 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 Analytics30 x r6i.4xlarge$29,160
MonitoringFull observability stack$45,000
CDN + DNS + WAFCloudFront + Route53 + WAF$55,000
AI/Poe InferenceAPI costs for LLM inference$2,500,000
Other servicesAuxiliary 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:

ModelDaily QueriesAvg Tokens/QueryCost per 1M TokensMonthly Cost
GPT-4o200K2,500$5.00$750,000
Claude 3.5 Sonnet150K3,000$3.00$405,000
GPT-4o Mini800K1,200$0.15$43,200
Llama 3.1 70B (self-hosted)600K1,500$0.03 (infra only)$81,000
Gemini Pro200K2,000$1.00$120,000
RAG Context Retrieval2M4,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 CategoryHeadcountAvg Annual CompMonthly Cost
Backend Engineers45$220,000$825,000
ML/AI Engineers20$280,000$466,667
Platform/SRE15$230,000$287,500
Data Engineers10$210,000$175,000
Security Engineers5$240,000$100,000
Engineering Management8$300,000$200,000
Content Moderation (Human)200$45,000$750,000
Trust & Safety10$180,000$150,000
Total Personnel313~$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 TrafficCompute MultiplierMonthly CostNotes
US-East (Primary)40%1.0x (baseline)$1,548,000All writes, primary Kafka, ML inference
EU-West30%0.85x$987,000Read replicas, local Redis, GDPR compliance overhead
AP-Southeast20%0.80x$619,000Read replicas, CDN edge, lighter ML models
South America10%0.90x$350,000CDN-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:

YearProjected MAUInfrastructurePersonnelAI/LLM CostsTotal Annual
Year 1 (Current)400M$46.4M$35.4M$30.0M$111.8M
Year 2520M$58.0M$42.5M$48.0M$148.5M
Year 3676M$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.

Key Takeaway: The most important lesson from Quora's architecture is that no single technology choice solves the problem — it's the thoughtful combination of PostgreSQL for transactional integrity, Redis for caching, Kafka for event streaming, ScyllaDB for time-series feeds, Elasticsearch for search, and ML models for ranking that creates a system capable of serving hundreds of millions of users with sub-second latency.

Written by Ayodhyya | System Design Series

Published July 14, 2026

ayodhyya.com