system-design55 min read

How to Design Visual Discovery Platform like Pinterest — A Senior+ Guide | Ayodhyya

How to Design Visual Discovery Platform like Pinterest

Building pin feeds, visual search, and recommendation at 480M+ monthly user scale

Published: July 14, 2026 • By Ayodhyya • ~12,000 words • Senior+ Guide

1. Introduction — Pinterest at Scale

Pinterest is the world's largest visual discovery engine, serving over 480 million monthly active users (MAU) who collectively have saved more than 350 billion pins across billions of boards. Unlike traditional social networks built around follower graphs, Pinterest is fundamentally a search and discovery platform where users explore ideas — from home renovation and recipes to fashion and travel — by visually browsing curated collections of images, videos, and product links.

Key Scale Numbers (2025-2026):
480M+ MAU 350B+ Pins 10B+ Visual Searches/mo 500PB+ Image Storage 5M+ Business Accounts 800M+ Searches/mo

Pinterest's core loop is simple yet powerful: Discover → Save → Organize → Act. A user discovers visual inspiration, saves it to a board for later, organizes ideas into collections, and eventually takes action — buying a product, cooking a recipe, or starting a DIY project. This loop generates an enormous amount of implicit preference signals that fuel the recommendation engine.

What makes Pinterest's system design particularly interesting from an engineering perspective is the intersection of multiple complex systems: large-scale image processing pipelines, deep learning-based visual search (Pinterest Lens), real-time personalization at billions of requests per day, and a sophisticated ads auction system — all while maintaining sub-200ms p99 latency for feed and search APIs.

Why Study Pinterest's Architecture?

Studying Pinterest's architecture provides deep insights into several critical distributed systems patterns:

  • Visual Embedding Systems: Converting images into dense vector representations for similarity search at scale using approximate nearest neighbor (ANN) algorithms.
  • Real-time ML Pipelines: Feature stores, online serving, and continuous model training for personalization.
  • Content Distribution: Multi-tier CDN strategies for delivering billions of images with varying quality requirements.
  • Graph-Based Recommendations: Combining collaborative filtering, content-based filtering, and knowledge graphs for discovery.
  • Monetization Systems: Auction-based advertising integrated seamlessly into organic discovery feeds.

This guide walks through a complete system design for a Pinterest-like platform, covering every major subsystem from data modeling to multi-region deployment, with production-grade C# implementations and detailed capacity planning.

2. Requirements Gathering

Functional Requirements

Core Features

  1. Pin Management: Users can create, save, and organize pins (images/videos with metadata) into boards.
  2. Home Feed: Personalized infinite-scroll feed of pins based on interests, follows, and past behavior.
  3. Visual Search: Upload an image to find visually similar pins (Pinterest Lens).
  4. Text Search: Search for pins, boards, and users using keywords with autocomplete.
  5. Board System: Create public/private boards, add collaborators, and reorder pins within boards.
  6. Follow System: Follow users, boards, and interest topics.
  7. Idea Pins: Multi-page video/image storytelling format for creators.
  8. Shopping: Product pins with pricing, availability, and direct purchase links.
  9. Ads: Promoted pins, shopping ads, and idea pin ads in feeds and search results.
  10. Creator Analytics: Dashboards showing impressions, saves, clicks, and audience demographics.

Non-Functional Requirements

AttributeTargetRationale
Availability99.99% (52 min downtime/year)Consumer platform with global users across time zones
Latency (Feed)p50 < 100ms, p99 < 300msInfinite scroll requires fast page loads
Latency (Search)p50 < 200ms, p99 < 500msVisual search includes embedding computation
Throughput1M+ reads/sec, 100K+ writes/sec480M MAU with 1-2 sessions/day
Durability99.999999999% (11 nines)User-generated content must never be lost
ConsistencyEventual (strong for payments)Social features tolerate slight delay
Storage500PB+ images, 50PB+ metadataBillions of high-resolution images

3. Capacity Estimation

Key Assumptions: 480M MAU, 300M DAU (62.5% ratio), 10 reads per session, 2 writes per session per active user.

QPS Estimation

OperationCalculationQPSWith 2x Buffer
Feed Reads300M DAU * 8 feed loads/day / 86400s~28,000~56,000
Search Queries300M DAU * 3 searches/day / 86400s~10,400~21,000
Pin Views300M DAU * 50 pins viewed/day / 86400s~174,000~348,000
Pin Creates300M DAU * 0.5 pins/day / 86400s~1,740~3,500
Image Uploads300M DAU * 0.3 uploads/day / 86400s~1,040~2,100
Saves (to board)300M DAU * 3 saves/day / 86400s~10,400~21,000

Storage Estimation

Data TypeSize per ItemDaily VolumeDaily StorageAnnual
Original Images3 MB avg200M pins600 TB~219 PB
Thumbnail (3 variants)150 KB x 3200M pins90 TB~33 PB
Pin Metadata (JSON)2 KB200M pins400 GB~146 TB
User Data1 KB2M new users2 GB~0.7 TB
Search Logs500 B2.5B queries1.25 TB~456 TB
Embedding Vectors (512-d)2 KB200M pins400 GB~146 TB

Bandwidth Estimation

Outbound (CDN): 300M DAU * 100 images/session * 200KB avg = ~600 PB/day, ~5.5 Gbps avg (peak 10x = ~55 Gbps)

Inbound (Uploads): 60M uploads/day * 3 MB = 180 TB/day, ~17 Gbps avg

Internal (DB Replication): ~200 Gbps across all shards for metadata replication

4. Data Model

The data model must efficiently support the core operations: serving personalized feeds, querying visual similarity, managing board hierarchies, and tracking engagement signals. We use a combination of relational databases for transactional data and specialized stores for search and vector operations.

Entity Relationship Diagram

erDiagram USERS { bigint user_id PK string username string email string display_name string avatar_url text bio enum account_type timestamp created_at jsonb preferences } PINS { bigint pin_id PK bigint user_id FK bigint board_id FK string image_url string thumbnail_url string link_url string title text description enum pin_type float[] embedding_vector jsonb metadata timestamp created_at bigint saves_count } BOARDS { bigint board_id PK bigint user_id FK string name text description boolean is_private string cover_image_url timestamp created_at int pin_count } SAVES { bigint save_id PK bigint user_id FK bigint pin_id FK bigint board_id FK timestamp saved_at } FOLLOWS { bigint follow_id PK bigint follower_id FK bigint following_id FK enum follow_type timestamp created_at } SEARCHES { bigint search_id PK bigint user_id FK string query_text string image_url enum search_type timestamp searched_at jsonb filters } USERS ||--o{ PINS : creates USERS ||--o{ BOARDS : owns BOARDS ||--o{ PINS : contains USERS ||--o{ SAVES : saves PINS ||--o{ SAVES : saved_in USERS ||--o{ FOLLOWS : follows USERS ||--o{ SEARCHES : searches

Core Table Schemas

-- Users table (sharded by user_id)
CREATE TABLE users (
    user_id         BIGINT PRIMARY KEY,
    username        VARCHAR(50) UNIQUE NOT NULL,
    email           VARCHAR(255) UNIQUE NOT NULL,
    display_name    VARCHAR(100) NOT NULL,
    avatar_url      VARCHAR(500),
    bio             TEXT,
    account_type    VARCHAR(20) DEFAULT 'personal',
    preferences     JSONB DEFAULT '{}',
    created_at      TIMESTAMP DEFAULT NOW(),
    updated_at      TIMESTAMP DEFAULT NOW()
);

-- Pins table (sharded by user_id for co-location)
CREATE TABLE pins (
    pin_id          BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    board_id        BIGINT NOT NULL,
    image_url       VARCHAR(1000) NOT NULL,
    thumbnail_url   VARCHAR(1000),
    link_url        VARCHAR(2000),
    title           VARCHAR(500),
    description     TEXT,
    pin_type        VARCHAR(20) DEFAULT 'standard',
    metadata        JSONB DEFAULT '{}',
    saves_count     BIGINT DEFAULT 0,
    created_at      TIMESTAMP DEFAULT NOW(),
    updated_at      TIMESTAMP DEFAULT NOW()
);

-- Boards table
CREATE TABLE boards (
    board_id        BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    name            VARCHAR(200) NOT NULL,
    description     TEXT,
    is_private      BOOLEAN DEFAULT FALSE,
    cover_image_url VARCHAR(1000),
    pin_count       INT DEFAULT 0,
    created_at      TIMESTAMP DEFAULT NOW()
);

-- Saves junction table (sharded by user_id)
CREATE TABLE saves (
    save_id         BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    pin_id          BIGINT NOT NULL,
    board_id        BIGINT NOT NULL,
    saved_at        TIMESTAMP DEFAULT NOW(),
    UNIQUE(user_id, pin_id, board_id)
);

-- Pin embeddings (stored in vector database)
CREATE TABLE pin_embeddings (
    pin_id          BIGINT PRIMARY KEY,
    embedding       VECTOR(512) NOT NULL,
    model_version   VARCHAR(50) NOT NULL,
    created_at      TIMESTAMP DEFAULT NOW()
);

5. API Design

Pinterest uses a combination of REST APIs for CRUD operations and gRPC for internal service-to-service communication. All public APIs are authenticated via OAuth 2.0 tokens and rate-limited per user.

REST API Endpoints

===== Pin APIs =====
POST   /api/v5/pins                    Create a new pin
GET    /api/v5/pins/{pin_id}           Get pin details
PUT    /api/v5/pins/{pin_id}           Update pin metadata
DELETE /api/v5/pins/{pin_id}           Delete a pin
POST   /api/v5/pins/{pin_id}/save      Save pin to a board
DELETE /api/v5/pins/{pin_id}/unsave    Remove pin from board

===== Board APIs =====
POST   /api/v5/boards                  Create a board
GET    /api/v5/boards/{board_id}       Get board with pins
PUT    /api/v5/boards/{board_id}       Update board details
DELETE /api/v5/boards/{board_id}       Delete a board
GET    /api/v5/users/{user_id}/boards  List user's boards

===== Feed APIs =====
GET    /api/v5/homefeed                Get personalized home feed
       ?cursor={cursor}&limit={25}
GET    /api/v5/pins/related/{pin_id}   Get related pins
GET    /api/v5/pins/trending           Get trending pins

===== Search APIs =====
POST   /api/v5/search/visual           Visual search (upload image)
GET    /api/v5/search?query={text}     Text search for pins
GET    /api/v5/search/autocomplete?q={q}  Search suggestions

===== Social APIs =====
POST   /api/v5/follows                 Follow a user or board
DELETE /api/v5/follows/{target_id}     Unfollow
GET    /api/v5/users/{user_id}/followers  List followers
GET    /api/v5/users/{user_id}/following  List following

===== User APIs =====
GET    /api/v5/me                      Get current user profile
PUT    /api/v5/me                      Update profile
GET    /api/v5/users/{user_id}         Get public profile

API Response Schema

Paginated Response Pattern (Cursor-based):

{
  "data": [ ... ],
  "pagination": {
    "cursor": "eyJwaW5faWQiOjEyMzQ1fQ==",
    "has_more": true,
    "next_url": "/api/v5/homefeed?cursor=eyJwaW5faWQiOjEyMzQ1fQ=="
  },
  "meta": {
    "request_id": "req_abc123",
    "latency_ms": 45
  }
}

Rate Limiting

TierRequests/minImage Uploads/dayVisual Searches/day
Free1,00010050
Business5,0001,000500
Partner API20,00010,0005,000

6. High-Level Architecture

Pinterest's architecture follows a microservices pattern with clear separation between the read path (feeds, search, browse) and write path (pin creation, saves, follows). The system is optimized for read-heavy workloads with aggressive caching and pre-computation.

graph TB subgraph Clients Mobile["Mobile Apps"] Web["Web App"] API["API Consumers"] end subgraph Edge CDN["CloudFront CDN"] LB["Load Balancer"] WAF["WAF + Rate Limiter"] end subgraph Gateway GW["API Gateway"] AuthSvc["Auth Service"] end subgraph Core PinSvc["Pin Service"] FeedSvc["Feed Service"] SearchSvc["Search Service"] BoardSvc["Board Service"] UserSvc["User Service"] SocialSvc["Social Service"] AdsSvc["Ads Service"] end subgraph ML EmbedSvc["Embedding Service"] RecSvc["Recommendation"] SearchML["Visual Search Index"] end subgraph Data PG["PostgreSQL Sharded"] Redis["Redis Cluster"] ES["Elasticsearch"] VectorDB["Vector Store"] S3["S3 Object Store"] Kafka["Kafka Stream"] end subgraph Workers PinWorker["Pin Processing"] EmbedWorker["Embedding"] FanoutWorker["Feed Fanout"] end Mobile --> CDN Web --> CDN Mobile --> LB Web --> LB LB --> WAF WAF --> GW GW --> AuthSvc GW --> PinSvc GW --> FeedSvc GW --> SearchSvc GW --> BoardSvc GW --> UserSvc GW --> SocialSvc GW --> AdsSvc PinSvc --> PG PinSvc --> Kafka FeedSvc --> Redis SearchSvc --> ES SearchSvc --> VectorDB BoardSvc --> PG UserSvc --> Redis Kafka --> PinWorker Kafka --> EmbedWorker Kafka --> FanoutWorker PinWorker --> S3 EmbedWorker --> VectorDB FanoutWorker --> Redis

Service Responsibilities

ServiceResponsibilityStorageLatency Target
Pin ServiceCRUD for pins, versioning, metadataPostgreSQL< 50ms p99
Feed ServicePersonalized feed generationRedis + PostgreSQL< 100ms p99
Search ServiceText search + visual searchElasticsearch + ScaNN< 300ms p99
Board ServiceBoard CRUD, pin orderingPostgreSQL< 50ms p99
User ServiceProfile, preferences, settingsPostgreSQL + Redis< 30ms p99
Social ServiceFollow/unfollow, social graphPostgreSQL< 50ms p99
Ads ServiceAuction, bidding, ad servingRedis + PostgreSQL< 100ms p99
Notification ServicePush, email, in-app notificationsKafka + FCM/APNs< 5s (async)

7. Pin Creation & Processing Pipeline

When a user creates a pin by uploading an image, the system triggers an asynchronous multi-stage processing pipeline. This is a critical path that transforms a raw upload into a fully searchable, recommended, and displayable pin.

graph LR A["User Upload"] --> B["Upload Service"] B --> C{"Validation + Virus Scan"} C -->|Pass| D["Image Processor"] C -->|Fail| Z["Reject"] D --> E["Generate Thumbnails"] D --> F["Extract EXIF"] D --> G["Content Moderation"] E --> H["Store to CDN"] G --> J{"Safe?"} J -->|Yes| K["Queue Embedding Job"] J -->|No| L["Flag for Review"] K --> M["Embedding Model"] M --> N["Index in Vector Store"] M --> O["Index in Elasticsearch"] N --> P["Pin Available"] O --> P

Pin Processing Stages

Stage 1: Upload and Validation (200-500ms)

The upload service generates a pre-signed S3 URL and allows direct browser-to-S3 upload (bypassing application servers for large files). Once uploaded, an S3 event notification triggers the processing pipeline. Validation includes file type checking (JPEG, PNG, GIF, WEBP, MP4), size limits (max 200MB for images, 1GB for videos), and virus scanning via ClamAV.

Stage 2: Image Processing (2-5s)

Image processing runs on dedicated GPU-equipped workers. The pipeline generates three thumbnail variants (236px wide for feeds, 564px for detail views, 1080px for full quality), strips EXIF data for privacy, applies lossy/lossy compression for optimal file size, and extracts dominant color palettes. Videos are transcoded to HLS with multiple quality levels (360p, 720p, 1080p).

Stage 3: Content Moderation (1-3s)

A multi-classifier moderation system evaluates the image for NSFW content, spam, violence, text-in-image (OCR), and copyright violations. Pinterest uses a combination of CNN classifiers and LLM-based review for borderline cases. Pins flagged with high confidence are automatically hidden; borderline cases are queued for human review.

Stage 4: Embedding Generation (1-3s)

The image is passed through a fine-tuned Vision Transformer (ViT-L/14) model that produces a 512-dimensional embedding vector capturing visual semantics — not just pixel similarity but conceptual similarity (e.g., "rustic kitchen" maps near other "rustic kitchen" images regardless of specific layout). This vector is indexed in the ScaNN system for approximate nearest neighbor queries.

Stage 5: Indexing and Fanout (500ms-2s)

The pin is indexed in Elasticsearch for text search, the embedding is stored in the vector index, and metadata is written to PostgreSQL. If the user has followers, a feed fanout job creates pre-computed feed entries for their followers' fanout feeds.

8. Image Processing, CDN & Embeddings

Pinterest stores over 500 petabytes of image data, serving billions of image requests daily. The image serving architecture uses a multi-tier CDN strategy with intelligent format negotiation and quality adaptation.

graph TB S3["S3 Original"] --> Trans["Transform Service"] Trans --> CF["CloudFront CDN"] CF --> IM["ImageMagick On-the-fly"] IM --> AVIF["AVIF/WebP Conversion"] CF --> L1["L1: Browser Cache"] CF --> L2["L2: CDN Edge 30-day"] L2 --> L3["L3: Regional 7-day"] L1 --> Device["Device-aware Delivery"] L2 --> Device L3 --> Device

Thumbnail Generation Strategy

VariantWidthUse CaseFormatTypical Size
Small Thumbnail236pxFeed grid, search resultsWEBP15-30 KB
Medium Thumbnail564pxPin detail pageWEBP50-100 KB
Large1080pxHigh-res view, zoomJPEG150-400 KB
OriginalNativeDownload, re-pin sourceOriginal1-10 MB

Embedding Model Architecture

Pinterest's embedding model is a fine-tuned Vision Transformer trained on billions of pin interactions. The model is trained using contrastive learning where pins that are saved together or appear in similar boards form positive pairs, while random pins form negative pairs.

// Embedding generation pipeline (conceptual)
public class PinEmbeddingGenerator
{
    private readonly IModel _visionModel;  // ViT-L/14 fine-tuned
    private readonly IPreprocessor _preprocessor;

    public async Task<float[]> GenerateEmbeddingAsync(byte[] imageBytes)
    {
        var tensor = _preprocessor.Process(imageBytes);
        var rawOutput = await _visionModel.ForwardAsync(tensor);
        var embedding = L2Normalize(rawOutput);
        return embedding;  // 512-dimensional float array
    }

    private float[] L2Normalize(float[] vector)
    {
        var norm = Math.Sqrt(vector.Sum(v => v * v));
        return vector.Select(v => (float)(v / norm)).ToArray();
    }
}

9. Visual Search (Pinterest Lens)

Pinterest Lens is one of the most sophisticated visual search systems in production, processing over 10 billion visual searches per month. Users can point their camera at any real-world object, take a photo, and find visually similar pins, products, or ideas.

graph TB A["Camera Input"] --> B["Image Preprocessing"] B --> C{"Input Type?"} C -->|Object| D["Object Detection YOLO v8"] C -->|Scene| E["Scene Classification"] C -->|Text| F["OCR Engine"] D --> G["Region of Interest"] E --> H["Scene Embedding"] F --> I["Text Embedding"] G --> J["Object Embedding"] J --> K["Multi-modal Fusion"] H --> K I --> K K --> L["ScaNN ANN Index"] L --> M["Top 10K Candidates"] M --> N["Re-ranking Model"] N --> O["Final Results"]

Visual Search Pipeline Stages

1. Object Detection and Segmentation

The input image is processed through YOLO v8 to detect individual objects. For complex scenes with multiple objects, the system segments each object and generates separate embeddings. A photo of a living room might extract: a sofa, a coffee table, a lamp, and a rug — each searched independently.

2. Embedding Computation

Each detected object or scene is passed through the fine-tuned ViT model to generate a 512-dim embedding. The system uses multi-crop augmentation — taking 5 different crops of each detected object and averaging their embeddings — to improve robustness to viewpoint changes.

3. Approximate Nearest Neighbor (ANN) Search

The query embedding is searched against the ScaNN index containing 350B+ pin embeddings. ScaNN uses anisotropic vector quantization to achieve sub-millisecond search latency with 95%+ recall. The index is partitioned across hundreds of machines using product quantization.

4. Re-ranking and Personalization

The top 10,000 ANN candidates are re-ranked using a lightweight cross-attention model that considers: visual similarity score, user engagement history, recency, and content quality signals. This re-ranking stage improves relevance by 30-40% over raw ANN results.

Visual Search Latency Budget

StageLatency (p50)Latency (p99)Parallelizable?
Image Preprocessing15ms30msNo
Object Detection40ms80msNo
Embedding (per crop)25ms50msYes (5 crops)
ANN Search30ms80msNo
Re-ranking50ms120msYes (batch)
Response Assembly10ms20msNo
Total120ms280ms

10. Recommendation Engine

Pinterest's recommendation system is one of the most sophisticated in the industry, combining collaborative filtering, content-based filtering, knowledge graphs, and deep learning models. The system powers home feed ranking, related pins, visual search results, and shopping recommendations.

graph TB subgraph Signals Explicit["Explicit: Saves, Clicks"] Implicit["Implicit: Dwell Time"] Search["Search: Queries"] Social["Social: Follows"] end subgraph Models CF["Collaborative Filtering"] CBF["Content-Based Filtering"] KG["Knowledge Graph"] DL["Deep Learning Two-Tower"] CTR["CTR Prediction"] end subgraph Serving FS["Feature Store"] CandidateGen["Candidate Generation"] Ranking["Multi-stage Ranking"] Blending["Score Blending"] end Explicit --> CF Implicit --> CF Search --> CBF Social --> KG CF --> CandidateGen CBF --> CandidateGen KG --> CandidateGen DL --> Ranking CTR --> Ranking FS --> CandidateGen FS --> Ranking CandidateGen --> Blending Ranking --> Blending

Multi-stage Ranking Pipeline

Stage 1 — Candidate Generation: Generate 1,000-10,000 candidate pins from multiple sources: collaborative filtering (users who saved similar pins), content-based similarity (visual + text embedding neighbors), trending pins, and fresh pins from followed creators. This stage uses approximate methods (ANN, LSH) for speed.

Stage 2 — Pre-ranking: Filter candidates by basic eligibility: not already seen, not from blocked users, language match, content policy pass. Score with a lightweight model (logistic regression) to reduce to ~500 candidates.

Stage 3 — Full Ranking: A deep learning model (two-tower architecture with cross-attention) scores each candidate based on user features (browsing history, demographics, interests) and pin features (engagement rates, visual embeddings, content quality). This produces a predicted engagement probability: P(save | user, pin).

Stage 4 — Blending and Diversity: Apply diversity constraints to avoid showing too many similar pins. Use Maximal Marginal Relevance (MMR) to balance relevance and diversity. Insert ads at learned positions, apply freshness boosting for recent content, and ensure creator diversity.

Feature Store Schema

// Real-time feature store entries for pin ranking
public class PinRankingFeatures
{
    // User features (from Redis, updated in real-time)
    public float[] UserEmbedding { get; set; }          // 128-dim
    public int UserDailyActiveDays { get; set; }
    public float UserSaveRate { get; set; }
    public List<long> RecentPinIds { get; set; }
    public Dictionary<string, float> InterestScores { get; set; }

    // Pin features
    public float[] PinEmbedding { get; set; }           // 512-dim
    public float PinSaveRate { get; set; }
    public float PinClickRate { get; set; }
    public float PinCloseUpRate { get; set; }
    public float PinFreshnessScore { get; set; }
    public float CreatorEngagementScore { get; set; }

    // Cross features
    public float UserPinSimilarity { get; set; }
    public float UserCreatorAffinity { get; set; }
    public string PinCategory { get; set; }
}

11. Home Feed Generation

Pinterest's home feed is a personalized infinite-scroll experience that blends multiple content sources: algorithmic recommendations, pins from followed users, trending content, and ads. The feed is generated fresh for each user session but cached aggressively.

graph LR A["User Opens App"] --> B["Feed Request"] B --> C{"Cached Feed?"} C -->|Hit| D["Return Cached"] C -->|Miss| E["Feed Generation"] E --> F["Algorithmic Recs"] E --> G["Following Feed"] E --> H["Trending Pins"] E --> I["Shopping Recs"] F --> K["Merge + Rank ML"] G --> K H --> K I --> K K --> L["Inject Ads"] L --> M["Diversity Constraints"] M --> N["Cache + Return"]

Feed Composition Breakdown

Source% of FeedUpdate FrequencyPersonalization
Algorithmic Recommendations60-70%Real-timeHigh (user embedding)
Pins from Followed Users15-20%Near real-timeFollow graph
Trending / Editorial5-10%HourlyLow (regional)
Shopping Recommendations5-10%DailyMedium (purchase intent)
Promoted Pins (Ads)5-8%Real-time auctionHigh (ad targeting)

Feed Caching Strategy

Pre-computed Feed: For high-activity users (top 20% by DAU), feed candidates are pre-computed every 5 minutes and stored in Redis. When the user opens the app, the API simply reads the cached feed and returns it. This reduces feed generation latency from 200ms to under 10ms.

On-demand Feed: For less active users, the feed is generated on-demand but cached for 5 minutes. Subsequent requests within the same session reuse the cached version.

Feed Cursor: Each feed page is identified by an opaque cursor that encodes: the timestamp of the last pin shown, the mix of content sources used, and a deduplication Bloom filter to prevent showing the same pin twice.

12. Search System (Text + Visual)

Pinterest's search system handles over 800 million queries per month, combining traditional text-based search with visual similarity search. The system must understand user intent from ambiguous natural language queries and return visually cohesive, relevant results.

graph TB QT["Text Query"] --> Tokenize["Tokenization"] Tokenize --> Intent["Intent Classification"] Intent --> Expand["Query Expansion"] Expand --> ES["Elasticsearch"] QV["Visual Query"] --> ANN["ScaNN Index"] ES --> Hybrid["Hybrid Fusion RRF"] ANN --> Hybrid Hybrid --> LTR["Learning to Rank"] LTR --> Personal["Personalization"] Personal --> Fresh["Freshness Boost"]

Query Understanding Pipeline

Query Expansion

Pinterest expands short queries using a learned query expansion model. For example, "kitchen" expands to include: "kitchen design", "kitchen organization", "kitchen decor", "small kitchen ideas". The expansion is personalized — a user who frequently engages with rustic content gets "rustic kitchen" added.

Intent Classification

Queries are classified into intent categories: browse (general exploration), shop (purchase intent), how-to (instructional), and specific (precise item). Shopping-intent queries boost product pins, while browse-intent queries prioritize visual diversity.

Hybrid Search Fusion (Reciprocal Rank Fusion)

// Reciprocal Rank Fusion algorithm
public List<SearchResult> ReciprocalRankFusion(
    List<SearchResult> textResults,
    List<SearchResult> visualResults,
    double textWeight = 0.6,
    double visualWeight = 0.4,
    int k = 60)
{
    var scores = new Dictionary<long, double>();

    for (int i = 0; i < textResults.Count; i++)
    {
        long pinId = textResults[i].PinId;
        double score = textWeight / (k + i + 1);
        scores[pinId] = scores.GetValueOrDefault(pinId, 0) + score;
    }

    for (int i = 0; i < visualResults.Count; i++)
    {
        long pinId = visualResults[i].PinId;
        double score = visualWeight / (k + i + 1);
        scores[pinId] = scores.GetValueOrDefault(pinId, 0) + score;
    }

    return scores
        .OrderByDescending(kv => kv.Value)
        .Take(100)
        .Select(kv => new SearchResult { PinId = kv.Key, Score = kv.Value })
        .ToList();
}

13. Board Management

Boards are the primary organizational unit on Pinterest. Users create themed boards (e.g., "Dream Home", "Recipes to Try", "Wedding Ideas") and save pins to them. Board operations require careful design for consistency, especially when multiple users collaborate on shared boards.

Board Operations

OperationAPILatencyConsistencySpecial Handling
Create BoardPOST /boards< 50msStrongName uniqueness per user
Add Pin to BoardPOST /pins/{id}/save< 100msEventualFanout to followers, update counts
Reorder PinsPUT /boards/{id}/order< 200msStrongOptimistic concurrency control
Delete BoardDELETE /boards/{id}< 50msStrongAsync cleanup of pin-board associations
Add CollaboratorPOST /boards/{id}/collab< 100msEventualNotification, permission check
Section ManagementPOST /boards/{id}/sections< 100msEventualNested categorization within boards

Board Section System

Boards can be divided into sections for further organization. For example, a "Home Renovation" board might have sections: "Kitchen", "Bathroom", "Living Room", "Exterior". Sections support drag-and-drop reordering with optimistic concurrency control using version tokens.

CREATE TABLE board_sections (
    section_id      BIGINT PRIMARY KEY,
    board_id        BIGINT NOT NULL,
    name            VARCHAR(200) NOT NULL,
    position        INT NOT NULL,
    version         INT DEFAULT 1,
    created_at      TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY (board_id) REFERENCES boards(board_id),
    UNIQUE(board_id, name)
);

CREATE TABLE section_pins (
    section_id      BIGINT NOT NULL,
    pin_id          BIGINT NOT NULL,
    position        INT NOT NULL,
    version         INT DEFAULT 1,
    PRIMARY KEY (section_id, pin_id)
);

14. Following & Social Graph

Pinterest's social graph is relatively simple compared to platforms like Facebook or Instagram. Users can follow other users, follow specific boards, and follow interest topics. The follow graph is used primarily for: (1) generating the "following" feed tab, (2) seeding recommendation models, and (3) notification delivery.

graph LR A["User A"] -->|"follows User B"| B["User B"] A -->|"follows Board X"| X["Board X"] A -->|"follows Interest: DIY"| T["Interest: DIY"] C["User C"] -->|"follows User A"| A D["User D"] -->|"follows Board X"| X

Follow Types and Fanout

Follow TypeEntityFeed ImpactNotification
User FollowUser profilePins from all their boardsNew pins, idea pins
Board FollowSpecific boardPins saved to that board onlyBoard updates
Interest FollowTopic/CategoryTrending pins in that topicWeekly digest

Fanout-on-Write vs. Fanout-on-Read: Pinterest uses a hybrid approach. For users with fewer than 10,000 followers, we fanout on write (pre-compute feed entries for all followers). For users with more than 10,000 followers (celebrities, big brands), we fanout on read (pull their recent pins at feed request time). This avoids the "celebrity problem" where a single pin write would need to fanout to millions of followers.

15. Idea Pins & Video Content

Idea Pins are Pinterest's format for multi-page, immersive content — similar to Instagram Stories but designed for evergreen discovery rather than ephemeral sharing. They support up to 20 pages of images, videos, stickers, and text overlays.

Video Processing Pipeline

graph LR A["Video Upload"] --> B["Chunked Upload to S3"] B --> C["Video Intelligence"] C --> D["Transcoding FFmpeg"] D --> E["HLS Segments 360p/720p/1080p"] D --> F["Thumbnail Extraction"] D --> G["Scene Detection"] D --> H["Audio Transcription Whisper"] H --> I["Auto-captions"] E --> J["CDN Distribution"]

Idea Pin Page Schema

CREATE TABLE idea_pins (
    idea_pin_id     BIGINT PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    title           VARCHAR(500),
    board_id        BIGINT,
    page_count      INT NOT NULL,
    total_duration  INT,
    metadata        JSONB,
    created_at      TIMESTAMP DEFAULT NOW()
);

CREATE TABLE idea_pin_pages (
    page_id         BIGINT PRIMARY KEY,
    idea_pin_id     BIGINT NOT NULL,
    page_index      INT NOT NULL,
    media_type      VARCHAR(20),
    media_url       VARCHAR(1000),
    duration        INT,
    overlays        JSONB,
    FOREIGN KEY (idea_pin_id) REFERENCES idea_pins(idea_pin_id)
);

16. Shopping & Product Pins

Pinterest's shopping features enable merchants to tag products in pins, display real-time pricing, and facilitate direct purchases. Product pins are enriched with structured data (price, availability, brand) and surfaced when shopping intent is detected.

Shopping Data Flow

graph TB A["Merchant Feed Catalog"] --> B["Ingestion Service"] B --> C["Product Dedup + Matching"] C --> D["Product Graph"] D --> E["Pin-Product Association"] E --> F["Price Refresh Service"] F --> G["Shopping Pin Enrichment"] G --> H["Indexed in Shopping Search"] I["User Saves Product Pin"] --> J["Purchase Intent Signal"] J --> K["Shopping Recommendations"]

Product Pin Fields

{
  "pin_id": 123456,
  "product": {
    "merchant_id": "m_789",
    "product_id": "prod_456",
    "title": "Mid-Century Modern Accent Chair",
    "price": 299.99,
    "currency": "USD",
    "availability": "in_stock",
    "sale_price": 249.99,
    "brand": "FurniCo",
    "category": "Furniture > Chairs > Accent Chairs",
    "deep_link": "https://merchant.com/chair/456",
    "last_updated": "2026-07-14T10:30:00Z"
  }
}

17. Ads Platform

Pinterest's advertising revenue comes from promoted pins, shopping ads, and idea pin ads. The ads system must serve relevant ads at scale while maintaining user experience quality — ads should feel like natural content in the discovery feed.

Ad Auction Pipeline

sequenceDiagram participant Client participant API participant AdSelector participant Auction participant Ranker participant Logger Client->>API: Feed Request API->>AdSelector: Get Ad Candidates AdSelector->>AdSelector: Targeting Filter AdSelector->>Auction: 50-100 eligible ads Auction->>Ranker: Score each ad Note over Ranker: bid x P(click) x P(convert) x quality Ranker->>Auction: Ranked ads Auction->>API: Final ad placements API->>Client: Feed with ads API->>Logger: Log impression

Ad Ranking Model

Feature CategoryFeaturesSource
User FeaturesDemographics, interests, purchase history, deviceFeature Store
Ad FeaturesCreative quality, landing page score, historical CTRAds DB
Context FeaturesTime of day, device, location, search queryRequest
Cross FeaturesUser-ad affinity, category match, recencyComputed

Revenue Optimization: Pinterest uses a Generalized Second Price (GSP) auction with quality score adjustments. Ads with higher predicted engagement are charged less per impression, incentivizing advertisers to create high-quality, relevant content. The effective CPM = (bid x predicted_ctr x quality_score) x 1000.

18. Analytics & Creator Tools

Pinterest provides analytics dashboards for business accounts and creators, showing impressions, saves, clicks, audience demographics, and content performance. The analytics pipeline processes billions of events daily using stream processing.

Analytics Event Schema

{
  "event_id": "evt_abc123",
  "event_type": "pin_impression",
  "timestamp": "2026-07-14T10:30:00.123Z",
  "user_id": "usr_456",
  "session_id": "sess_789",
  "pin_id": "pin_012",
  "context": {
    "surface": "home_feed",
    "position": 7,
    "device": "ios",
    "country": "US"
  },
  "engagement": {
    "impression": true,
    "close_up": false,
    "save": false,
    "click": false,
    "dwell_ms": 2500
  }
}

Stream Processing (Kafka to Flink to ClickHouse): Raw events flow through Kafka topics partitioned by pin_id. Apache Flink jobs perform real-time aggregation (views per minute, save rate rolling averages) and write to ClickHouse for low-latency analytical queries. Materialized views pre-compute common dashboard queries.

Batch Processing (S3 to Spark to BigQuery): Daily batch jobs compute cohort analyses, attribution models, and audience segmentations. Results are written to BigQuery for complex ad-hoc queries by the analytics team.

19. Database Sharding

At Pinterest's scale, a single PostgreSQL instance cannot handle the data volume or query throughput. The system uses application-level sharding with consistent hashing across multiple database clusters.

Sharding Strategy

graph TB Router["Shard Router Consistent Hashing"] S1["Shard 1 users 0-134M"] S2["Shard 2 users 134M-268M"] S3["Shard 3 users 268M-402M"] S4["Shard 4 users 402M-480M+"] R1["Replica x3"] R2["Replica x3"] R3["Replica x3"] R4["Replica x3"] Router --> S1 Router --> S2 Router --> S3 Router --> S4 S1 --> R1 S2 --> R2 S3 --> R3 S4 --> R4

Sharding Configuration

EntityShard KeyShard CountRebalancing
Usersuser_id16 primary (4 groups x 4 shards)Consistent hashing with virtual nodes
Pinsuser_id (co-located)Same as usersCo-located with user for join performance
Boardsuser_idSame as usersCo-located with user
Savesuser_idSame as usersCo-located with user
Search IndexN/A (Elasticsearch)200+ shardsES auto-balancing
Vector Indexhash(pin_id)500+ partitionsScaNN rebalancing

Cross-Shard Queries: Some queries require cross-shard data, such as "show me pins saved by users I follow" where followers may be on different shards. Solutions: (1) Fanout Queries — scatter to all shards and merge at application layer. (2) Secondary Indexes — denormalized lookup tables in Redis. (3) Materialized Views — pre-computed cross-shard aggregations in ClickHouse.

20. Caching Strategy

Caching is critical for Pinterest's performance targets. The system uses a multi-level caching strategy across application, database, and CDN layers.

Cache Hierarchy

LevelTechnologyWhat's CachedTTLHit Rate Target
L1: ClientApp memoryPrefetched feed pages5 min60%
L2: CDNCloudFrontImages, thumbnails, static assets30 days95%
L3: ApplicationRedis ClusterFeed pages, user profiles, pin metadata5-60 min85%
L4: DatabasePG buffer poolHot rows, index pagesN/A99%

Cache Invalidation Patterns

Write-Through for Pin Updates

When a pin is updated (title, description, or metadata change), the write path updates both PostgreSQL and invalidates the Redis cache key simultaneously. The next read request triggers a cache miss and repopulates from the database.

Event-Driven Invalidation for Saves

When a user saves a pin to a board, a Kafka event triggers invalidation of: (1) the user's feed cache, (2) the board's pin list cache, (3) the pin's save count cache. This ensures eventual consistency within 1-2 seconds.

// Cache service implementation
public class PinCacheService
{
    private readonly IDistributedCache _redis;
    private readonly IPinRepository _db;
    private const int PIN_CACHE_TTL_MINUTES = 30;

    public async Task<PinDto?> GetPinAsync(long pinId)
    {
        string cacheKey = $"pin:{pinId}";
        var cached = await _redis.GetStringAsync(cacheKey);
        if (cached != null)
            return JsonSerializer.Deserialize<PinDto>(cached);

        var pin = await _db.GetPinByIdAsync(pinId);
        if (pin != null)
        {
            var dto = MapToDto(pin);
            await _redis.SetStringAsync(cacheKey,
                JsonSerializer.Serialize(dto),
                TimeSpan.FromMinutes(PIN_CACHE_TTL_MINUTES));
            return dto;
        }
        return null;
    }

    public async Task InvalidatePinCacheAsync(long pinId)
    {
        await _redis.RemoveAsync($"pin:{pinId}");
        await PublishInvalidationEventAsync(pinId);
    }
}

21. Multi-Region Design

Pinterest operates across multiple AWS regions to serve global users with low latency and high availability. The multi-region architecture uses active-active replication for reads and leader-based writes with conflict resolution.

graph TB US["US-East Primary"] -->|Async Replication p99 less than 500ms| EU["EU-West Secondary"] US -->|Async Replication p99 less than 1s| AP["AP-Southeast Tertiary"] US_K["Kafka"] -->|MirrorMaker 2| EU_K["Kafka"] US_K -->|MirrorMaker 2| AP_K["Kafka"]

Data Replication Strategy

Data TypeReplication ModeConsistencyConflict Resolution
User ProfilesAsync (leader to follower)Eventual (< 500ms)Last-writer-wins (LWW)
PinsAsync (leader to follower)Eventual (< 1s)LWW with version vectors
Boards/SavesAsync (leader to follower)Eventual (< 1s)CRDT (add-only sets)
Feed CacheLocal generationN/ARegenerated per region
Search IndexPeriodic snapshot + log replayEventual (< 30s)N/A (append-only)
Vector IndexPeriodic rebuild per regionEventual (< 5 min)N/A (immutable vectors)

Regional Routing

Users are routed to the nearest region using GeoDNS (Route 53 latency-based routing). Write operations are always directed to the primary region (US-East) to avoid multi-region write conflicts. Read replicas serve read traffic locally. In case of a primary region failure, a regional failover promotes EU-West to primary within 60 seconds using automated runbooks.

22. Cost Estimation

Running a Pinterest-scale infrastructure involves significant costs across compute, storage, networking, and ML inference. Here is a rough monthly cost breakdown for operating at 480M MAU.

Monthly Infrastructure Cost Breakdown

ComponentSpecificationMonthly Cost (USD)
Application Servers (EC2)2,000x c6i.4xlarge (16 vCPU, 32 GB)$1,200,000
GPU Instances (ML)500x p4d.24xlarge (8xA100 GPU)$3,500,000
PostgreSQL (RDS)64x r6g.16xlarge (64 vCPU, 512 GB)$400,000
Redis (ElastiCache)200x r6g.4xlarge (128 GB)$350,000
Elasticsearch500x r6i.4xlarge (16 vCPU, 128 GB)$400,000
S3 Storage (Images)500 PB at $0.023/GB$11,500,000
CloudFront CDN500 PB transfer/month$40,000,000
Kafka (MSK)200x kafka.m5.4xlarge$200,000
Data TransferInter-region + internet egress$5,000,000
ML Model TrainingMonthly retraining runs$2,000,000
Monitoring and LoggingCloudWatch, Datadog, PagerDuty$300,000
Security and ComplianceWAF, Shield, audits$200,000
Total Estimated Monthly~$65,000,000

Note: These are rough estimates based on public AWS pricing and industry benchmarks. Pinterest likely negotiates significant reserved instance discounts and custom pricing, potentially reducing costs by 30-40%. The actual cost structure also heavily depends on ad revenue offsetting infrastructure costs.

23. Interview Q&A — 15 Questions

Q1: How would you design the home feed for 480M monthly users?
Answer: Use a hybrid fanout model. For users with less than 10K followers, fanout on write (pre-compute feed entries in Redis). For users with more than 10K followers, fanout on read (query recent pins at request time). The feed generation service merges algorithmic recommendations (60-70%), following feed (15-20%), trending content (5-10%), and ads (5-8%). Cache pre-computed feeds in Redis with 5-minute TTL. Use cursor-based pagination with opaque cursors encoding timestamp + dedup Bloom filter for efficient infinite scroll.
Q2: How does Pinterest's visual search (Lens) work at scale?
Answer: The pipeline has 4 stages: (1) Object detection using YOLO v8 to segment individual objects in the scene. (2) Embedding generation using a fine-tuned ViT-L/14 model producing 512-dim vectors with multi-crop augmentation. (3) ANN search using ScaNN with anisotropic vector quantization across 500+ partitions for sub-millisecond lookup. (4) Re-ranking using a cross-attention model incorporating visual similarity, engagement signals, and personalization. The total p99 latency is under 300ms.
Q3: How do you handle the celebrity fanout problem when a famous user creates a pin?
Answer: Use a tiered fanout strategy. Users with less than 10K followers get on-write fanout (write pin to all followers' feed caches). Users with 10K-1M followers get hybrid fanout (write to a "hot pins" list, pulled on read). Users with more than 1M followers get on-read only (their pins are fetched live when any follower requests their feed). This prevents a single pin creation from triggering millions of cache writes.
Q4: How would you design the board system to support 350B+ pins across billions of boards?
Answer: Shard boards by user_id for co-location with user data, enabling efficient joins. Store pin-board associations in a junction table also sharded by user_id. For board ordering (drag-and-drop reordering), use position values with fractional indexing (insert between 1.0 and 2.0 for position 2) to avoid rewriting all positions on reorder. Cache hot board data in Redis. Use optimistic concurrency control (version tokens) for collaborative boards to handle concurrent edits.
Q5: How do you ensure content moderation at Pinterest's scale?
Answer: Multi-layer approach: (1) Pre-upload: file type and size validation, virus scanning. (2) Automated ML classifiers for NSFW, spam, violence, and copyright detection running on GPU workers. (3) Hash-matching against known violating content databases (PhotoDNA). (4) LLM-based review for borderline cases. (5) Human review queue for appeals and complex cases. (6) User reporting system with priority queuing. All classifiers run with less than 5s latency in the async pipeline so pins go live quickly while moderation catches violations shortly after.
Q6: How would you design the search autocomplete system?
Answer: Use a trie-based data structure stored in Redis for sub-millisecond prefix matching. The trie is built from: (1) Popular search queries ranked by frequency with time decay. (2) User's personal search history (higher rank). (3) Trending queries (boosted temporarily). The trie is pre-loaded into Redis and refreshed every 15 minutes from a batch computation job. For personalization, maintain a per-user search history set in Redis and merge trie results with personal history using a weighted scoring function.
Q7: How would you handle image processing at the scale of 200M new pins per day?
Answer: Use a distributed processing pipeline backed by Kafka and auto-scaling GPU worker fleets. The pipeline has 5 stages: upload validation (CPU), image resizing/optimization (CPU), content moderation (GPU), embedding generation (GPU), and indexing (CPU). Use pre-signed S3 URLs for direct browser-to-S3 upload to bypass application servers. Each stage publishes completion events to Kafka, enabling independent scaling. Target: process 200M images/day = ~2,300 images/second throughput. Use spot instances for GPU workers to reduce cost by 60%.
Q8: How do you achieve sub-100ms p50 latency for the home feed?
Answer: Three key strategies: (1) Pre-computation — generate feed pages every 5 minutes and cache in Redis. (2) Multi-layer caching — L1 client prefetch, L2 CDN for images, L3 Redis for feed data. (3) Efficient serialization — use Protocol Buffers for internal communication, avoid N+1 queries by batch-loading pin data. The feed API endpoint reads from Redis cache (p50 less than 5ms), attaches CDN image URLs (no DB lookup needed), and returns the response. The actual ML ranking happens asynchronously during the pre-computation step.
Q9: How would you design the recommendation engine to handle both new users (cold start) and power users?
Answer: For cold start (new users): (1) Use onboarding interest selection to bootstrap preferences. (2) During the first session, use contextual bandits to explore diverse content and quickly learn preferences from click/save signals. (3) Leverage demographic and geographic priors. For power users: (1) Maintain a 128-dim user embedding updated in real-time from recent interactions. (2) Use collaborative filtering to find similar users. (3) Apply content-based filtering using visual embeddings of saved pins. (4) Combine all signals in a two-tower deep learning model. Transition from exploration to exploitation as confidence increases.
Q10: How do you handle database failures and ensure data durability for user-generated content?
Answer: Multiple layers of protection: (1) Synchronous replication within an AZ for strong durability (RPO=0). (2) Asynchronous cross-AZ replication for read scaling. (3) Cross-region replication with less than 1s lag for disaster recovery. (4) Daily S3 snapshots with 90-day retention. (5) Images stored in S3 with 11-nines durability (3 AZ replication). (6) Kafka with replication factor 3 for event streams. (7) Automated failover with health checks and DNS reconfiguration in less than 60 seconds.
Q11: How would you design the shopping/product pin system to keep pricing accurate across millions of merchants?
Answer: Merchants provide product feeds via API or bulk upload (CSV/XML). An ingestion service normalizes and deduplicates products using title similarity + image hash matching. A periodic refresh service (every 4-6 hours) re-fetches prices and availability from merchant APIs. Use a product knowledge graph to link duplicate products across merchants for price comparison. Product pins display a "Price updated X hours ago" indicator. If a product goes out of stock, the pin is deprioritized in recommendations but not deleted.
Q12: How would you handle a sudden traffic spike (e.g., Black Friday for shopping pins)?
Answer: (1) Auto-scaling groups with predictive scaling based on historical traffic patterns. (2) CDN caching absorbs the read amplification — 95%+ of image requests hit CDN. (3) Feed cache TTL reduced from 5 min to 1 min to increase freshness, but cache serves as a buffer. (4) Read replicas auto-scale horizontally. (5) Circuit breakers on non-critical services (analytics, recommendations) to shed load. (6) Rate limiting on API endpoints with graceful degradation (return cached feed instead of generating fresh). (7) Pre-warm caches 24 hours before predicted spikes.
Q13: How does Pinterest handle pin deduplication to avoid showing the same image multiple times?
Answer: Multi-level deduplication: (1) Perceptual hashing (pHash) at upload time to detect near-duplicate images. (2) URL-based dedup for re-pins from the same source. (3) Feed-level Bloom filter per user session to prevent showing the same pin twice across paginated loads. (4) Cross-user dedup using content-based similarity — pins with cosine similarity greater than 0.98 in embedding space are grouped and shown based on the "best" version (highest engagement). For re-pins, the system links to the original pin rather than creating a duplicate.
Q14: How would you design the notification system for 480M users?
Answer: Use a priority-based notification pipeline: (1) Event triggers (new follower, save on your pin, comment) generate events in Kafka. (2) A notification service evaluates delivery preferences (push, email, in-app) and frequency caps per user. (3) High-priority notifications (direct messages, mentions) are delivered in less than 5s via push. (4) Medium-priority (saves on your pins, new pins from followed users) are batched into digests sent hourly or daily. (5) Low-priority (trending in your interests) are weekly digests. (6) Use FCM for Android, APNs for iOS, and SES for email.
Q15: How would you design the ads auction system to maximize revenue while maintaining user experience?
Answer: Use a GSP (Generalized Second Price) auction with quality score: Ad Rank = bid x predicted_ctr x quality_score. The quality score considers: (1) Historical CTR of the ad creative. (2) Landing page quality and relevance. (3) User engagement signals (saves, close-ups on the ad). Apply an ads-to-organic ratio cap (max 15% of feed positions) to protect UX. Use multi-armed bandits to optimize ad placement positions. Implement frequency capping per user (max 3 exposures per ad per day). Run A/B tests on ad density to measure impact on long-term user retention.

24. Full C# Implementation — Pin Service & Recommendation Engine

Below is a comprehensive C# implementation covering the core Pin Service, Visual Search Integration, Feed Generator, and Recommendation Engine. This production-grade code demonstrates the key patterns discussed throughout this article.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;

namespace Pinterest.VisualDiscovery.Core
{
    // ==================== MODELS ====================
    public enum PinType { Standard, Product, Idea, Video }
    public enum FollowType { User, Board, Interest }

    public class Pin
    {
        public long PinId { get; set; }
        public long UserId { get; set; }
        public long BoardId { get; set; }
        public string ImageUrl { get; set; } = string.Empty;
        public string ThumbnailUrl { get; set; } = string.Empty;
        public string? LinkUrl { get; set; }
        public string Title { get; set; } = string.Empty;
        public string? Description { get; set; }
        public PinType Type { get; set; } = PinType.Standard;
        public float[]? EmbeddingVector { get; set; }
        public long SavesCount { get; set; }
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    }

    public class Board
    {
        public long BoardId { get; set; }
        public long UserId { get; set; }
        public string Name { get; set; } = string.Empty;
        public string? Description { get; set; }
        public bool IsPrivate { get; set; }
        public int PinCount { get; set; }
        public int Version { get; set; } = 1;
    }

    public class User
    {
        public long UserId { get; set; }
        public string Username { get; set; } = string.Empty;
        public float[]? UserEmbedding { get; set; }
        public Dictionary<string, float> InterestScores { get; set; } = new();
    }

    public class SearchResult
    {
        public long PinId { get; set; }
        public double Score { get; set; }
        public string MatchType { get; set; } = string.Empty;
    }

    public class FeedPage
    {
        public List<FeedItem> Items { get; set; } = new();
        public string? NextCursor { get; set; }
        public bool HasMore { get; set; }
    }

    public class FeedItem
    {
        public long PinId { get; set; }
        public double RankScore { get; set; }
        public string Source { get; set; } = string.Empty;
        public bool IsPromoted { get; set; }
    }

    // ==================== CONFIGURATION ====================
    public class PinterestConfig
    {
        public int FeedPageSize { get; set; } = 25;
        public int FeedCacheTtlMinutes { get; set; } = 5;
        public int PinCacheTtlMinutes { get; set; } = 30;
        public double AdFeedRatio { get; set; } = 0.08;
    }

    // ==================== REPOSITORIES ====================
    public interface IPinRepository
    {
        Task<Pin?> GetPinByIdAsync(long pinId);
        Task<List<Pin>> GetPinsByIdsAsync(IEnumerable<long> pinIds);
        Task<long> CreatePinAsync(Pin pin);
        Task<List<Pin>> GetPinsByUserIdAsync(long userId, int limit, long? before);
        Task<List<Pin>> GetPinsByBoardIdAsync(long boardId, int limit, int offset);
        Task<List<Pin>> GetTrendingPinsAsync(int limit, string? category);
        Task<long> IncrementSaveCountAsync(long pinId);
        Task<long> DecrementSaveCountAsync(long pinId);
    }

    public interface IBoardRepository
    {
        Task<Board?> GetBoardByIdAsync(long boardId);
        Task<long> CreateBoardAsync(Board board);
        Task AddPinToBoardAsync(long boardId, long pinId, int position);
        Task RemovePinFromBoardAsync(long boardId, long pinId);
    }

    public interface IUserRepository
    {
        Task<User?> GetUserByIdAsync(long userId);
        Task<List<long>> GetFollowingIdsAsync(long userId);
    }

    public interface ISaveRepository
    {
        Task<bool> HasUserSavedPinAsync(long userId, long pinId);
        Task CreateSaveAsync(long userId, long pinId, long boardId);
        Task DeleteSaveAsync(long userId, long pinId);
    }

    public interface IVisualSearchEngine
    {
        Task<List<SearchResult>> SearchByImageAsync(byte[] imageBytes, int topK);
    }

    public interface ITextSearchEngine
    {
        Task<List<SearchResult>> SearchByTextAsync(string query, int topK);
        Task<List<string>> GetAutocompleteSuggestionsAsync(string prefix, int limit);
    }

    public interface IImageProcessingService
    {
        Task<(bool success, float[]? embedding, string? error)>
            ProcessImageAsync(byte[] imageBytes);
    }

    public interface IAdsService
    {
        Task<List<FeedItem>> GetRelevantAdsAsync(long userId, int count);
    }

    public interface IEventPublisher
    {
        Task PublishAsync(string topic, string key, object payload);
    }

    // ==================== SHARD ROUTER ====================
    public class ShardRouter
    {
        private readonly int _shardCount;

        public ShardRouter(int shardCount)
        {
            _shardCount = shardCount;
        }

        public int GetShardId(long userId)
        {
            unchecked
            {
                ulong k = (ulong)userId;
                k ^= k >> 33;
                k *= 0xff51afd7ed558ccd;
                k ^= k >> 33;
                k *= 0xc4ceb9fe1a85ec53;
                k ^= k >> 33;
                return (int)(k % (uint)_shardCount);
            }
        }
    }

    // ==================== PIN SERVICE ====================
    public class PinService
    {
        private readonly IPinRepository _pinRepo;
        private readonly IBoardRepository _boardRepo;
        private readonly IUserRepository _userRepo;
        private readonly ISaveRepository _saveRepo;
        private readonly IImageProcessingService _imageProcessor;
        private readonly IEventPublisher _eventPublisher;
        private readonly IDistributedCache _cache;
        private readonly PinterestConfig _config;
        private readonly ILogger<PinService> _logger;

        public PinService(
            IPinRepository pinRepo, IBoardRepository boardRepo,
            IUserRepository userRepo, ISaveRepository saveRepo,
            IImageProcessingService imageProcessor,
            IEventPublisher eventPublisher,
            IDistributedCache cache, PinterestConfig config,
            ILogger<PinService> logger)
        {
            _pinRepo = pinRepo;
            _boardRepo = boardRepo;
            _userRepo = userRepo;
            _saveRepo = saveRepo;
            _imageProcessor = imageProcessor;
            _eventPublisher = eventPublisher;
            _cache = cache;
            _config = config;
            _logger = logger;
        }

        public async Task<Pin> CreatePinAsync(
            long userId, long boardId, byte[] imageBytes,
            string title, string? description, string? linkUrl)
        {
            _logger.LogInformation(
                "Creating pin for user {UserId} on board {BoardId}",
                userId, boardId);

            var board = await _boardRepo.GetBoardByIdAsync(boardId);
            if (board == null || board.UserId != userId)
                throw new InvalidOperationException("Board not found");

            var result = await _imageProcessor.ProcessImageAsync(imageBytes);
            if (!result.success)
                throw new InvalidOperationException(
                    $"Image processing failed: {result.error}");

            var pin = new Pin
            {
                PinId = GenerateId(),
                UserId = userId,
                BoardId = boardId,
                Title = title,
                Description = description,
                LinkUrl = linkUrl,
                EmbeddingVector = result.embedding,
                CreatedAt = DateTime.UtcNow
            };

            await _pinRepo.CreatePinAsync(pin);
            await _boardRepo.AddPinToBoardAsync(
                boardId, pin.PinId, board.PinCount);

            await Task.WhenAll(
                _eventPublisher.PublishAsync(
                    "pin.created", pin.PinId.ToString(), pin),
                _eventPublisher.PublishAsync(
                    "embedding.generate", pin.PinId.ToString(),
                    new { PinId = pin.PinId }),
                _eventPublisher.PublishAsync(
                    "feed.fanout", pin.PinId.ToString(),
                    new { PinId = pin.PinId, UserId = userId }));

            await _cache.RemoveAsync($"board:{boardId}:pins");

            _logger.LogInformation(
                "Pin {PinId} created successfully", pin.PinId);
            return pin;
        }

        public async Task<Pin?> GetPinAsync(long pinId)
        {
            string cacheKey = $"pin:{pinId}";
            var cached = await _cache.GetStringAsync(cacheKey);
            if (cached != null)
                return JsonSerializer.Deserialize<Pin>(cached);

            var pin = await _pinRepo.GetPinByIdAsync(pinId);
            if (pin != null)
            {
                await _cache.SetStringAsync(cacheKey,
                    JsonSerializer.Serialize(pin),
                    new DistributedCacheEntryOptions
                    {
                        AbsoluteExpirationRelativeToNow =
                            TimeSpan.FromMinutes(
                                _config.PinCacheTtlMinutes)
                    });
            }
            return pin;
        }

        public async Task SavePinToBoardAsync(
            long userId, long pinId, long boardId)
        {
            var board = await _boardRepo.GetBoardByIdAsync(boardId);
            if (board == null || board.UserId != userId)
                throw new InvalidOperationException("Board not found");

            if (await _saveRepo.HasUserSavedPinAsync(userId, pinId))
                throw new InvalidOperationException("Already saved");

            await _saveRepo.CreateSaveAsync(userId, pinId, boardId);
            await _pinRepo.IncrementSaveCountAsync(pinId);

            await _eventPublisher.PublishAsync("pin.saved",
                $"{userId}:{pinId}",
                new { UserId = userId, PinId = pinId, BoardId = boardId });

            await _cache.RemoveAsync($"board:{boardId}:pins");
            await _cache.RemoveAsync($"user:{userId}:feed");
        }

        public async Task RemovePinFromBoardAsync(
            long userId, long pinId, long boardId)
        {
            var board = await _boardRepo.GetBoardByIdAsync(boardId);
            if (board == null || board.UserId != userId)
                throw new InvalidOperationException("Board not found");

            await _saveRepo.DeleteSaveAsync(userId, pinId);
            await _boardRepo.RemovePinFromBoardAsync(boardId, pinId);
            await _pinRepo.DecrementSaveCountAsync(pinId);

            await _cache.RemoveAsync($"board:{boardId}:pins");
        }

        public async Task<List<Pin>> GetRelatedPinsAsync(
            long pinId, int limit = 20)
        {
            var pin = await _pinRepo.GetPinByIdAsync(pinId);
            if (pin?.EmbeddingVector == null)
                return new List<Pin>();

            string cacheKey = $"related:{pinId}:{limit}";
            var cached = await _cache.GetStringAsync(cacheKey);
            if (cached != null)
                return JsonSerializer.Deserialize<List<Pin>>(cached)
                    ?? new();

            var relatedIds = await FindSimilarByEmbeddingAsync(
                pin.EmbeddingVector, limit + 1);
            relatedIds.Remove(pinId);

            var relatedPins = await _pinRepo.GetPinsByIdsAsync(
                relatedIds.Take(limit).ToList());

            await _cache.SetStringAsync(cacheKey,
                JsonSerializer.Serialize(relatedPins),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow =
                        TimeSpan.FromMinutes(15)
                });

            return relatedPins;
        }

        private async Task<List<long>> FindSimilarByEmbeddingAsync(
            float[] embedding, int count)
        {
            // In production: calls ScaNN vector index
            await Task.CompletedTask;
            return Enumerable.Range(0, count)
                .Select(i => (long)(i + 1000)).ToList();
        }

        private static long GenerateId()
        {
            var epoch = new DateTime(2024, 1, 1, 0, 0, 0,
                DateTimeKind.Utc);
            var ts = (long)(DateTime.UtcNow - epoch).TotalMilliseconds;
            var rand = new Random().Next(0, 9999);
            return (ts << 12) | (long)rand;
        }
    }

    // ==================== SEARCH SERVICE ====================
    public class SearchService
    {
        private readonly IVisualSearchEngine _visualSearch;
        private readonly ITextSearchEngine _textSearch;
        private readonly ILogger<SearchService> _logger;

        public SearchService(
            IVisualSearchEngine visualSearch,
            ITextSearchEngine textSearch,
            ILogger<SearchService> logger)
        {
            _visualSearch = visualSearch;
            _textSearch = textSearch;
            _logger = logger;
        }

        public async Task<List<SearchResult>> HybridSearchAsync(
            string? textQuery, byte[]? imageBytes, int topK = 100)
        {
            var tasks = new List<Task<List<SearchResult>>>();

            if (!string.IsNullOrWhiteSpace(textQuery))
                tasks.Add(_textSearch.SearchByTextAsync(
                    textQuery, topK * 2));

            if (imageBytes != null && imageBytes.Length > 0)
                tasks.Add(_visualSearch.SearchByImageAsync(
                    imageBytes, topK * 2));

            await Task.WhenAll(tasks);

            var allResults = tasks
                .SelectMany(t => t.Result).ToList();

            return ReciprocalRankFusion(allResults, topK);
        }

        public async Task<List<string>> GetAutocompleteAsync(
            string prefix, int limit = 10)
        {
            return await _textSearch
                .GetAutocompleteSuggestionsAsync(prefix, limit);
        }

        private List<SearchResult> ReciprocalRankFusion(
            List<SearchResult> allResults, int topK, double k = 60)
        {
            var scores = new Dictionary<long, double>();
            var sources = allResults
                .GroupBy(r => r.MatchType).ToList();
            double weight = 1.0 / Math.Max(1, sources.Count);

            foreach (var source in sources)
            {
                var ranked = source
                    .OrderByDescending(r => r.Score).ToList();
                for (int i = 0; i < ranked.Count; i++)
                {
                    long id = ranked[i].PinId;
                    double s = weight / (k + i + 1);
                    scores[id] =
                        scores.GetValueOrDefault(id, 0) + s;
                }
            }

            return scores
                .OrderByDescending(kv => kv.Value)
                .Take(topK)
                .Select(kv => new SearchResult
                {
                    PinId = kv.Key,
                    Score = kv.Value,
                    MatchType = "fused"
                })
                .ToList();
        }
    }

    // ==================== FEED SERVICE ====================
    public class FeedService
    {
        private readonly IUserRepository _userRepo;
        private readonly IPinRepository _pinRepo;
        private readonly IAdsService _adsService;
        private readonly IDistributedCache _cache;
        private readonly PinterestConfig _config;
        private readonly ILogger<FeedService> _logger;

        public FeedService(
            IUserRepository userRepo, IPinRepository pinRepo,
            IAdsService adsService, IDistributedCache cache,
            PinterestConfig config,
            ILogger<FeedService> logger)
        {
            _userRepo = userRepo;
            _pinRepo = pinRepo;
            _adsService = adsService;
            _cache = cache;
            _config = config;
            _logger = logger;
        }

        public async Task<FeedPage> GetHomeFeedAsync(
            long userId, string? cursor = null)
        {
            string cacheKey = $"feed:{userId}:{cursor ?? "start"}";
            var cached = await _cache.GetStringAsync(cacheKey);
            if (cached != null)
                return JsonSerializer.Deserialize<FeedPage>(cached)
                    ?? new FeedPage();

            var user = await _userRepo.GetUserByIdAsync(userId);
            if (user == null)
                throw new InvalidOperationException("User not found");

            var feedItems = new List<FeedItem>();

            // Algorithmic recommendations (60-70%)
            var algoPins = await _pinRepo
                .GetTrendingPinsAsync(15, null);
            feedItems.AddRange(algoPins.Select(p => new FeedItem
            {
                PinId = p.PinId, RankScore = 0.8,
                Source = "algorithmic"
            }));

            // Following feed (15-20%)
            var followingIds = await _userRepo
                .GetFollowingIdsAsync(userId);
            foreach (var fid in followingIds.Take(50))
            {
                var pins = await _pinRepo
                    .GetPinsByUserIdAsync(fid, 3, null);
                feedItems.AddRange(pins.Select(p => new FeedItem
                {
                    PinId = p.PinId, RankScore = 0.9,
                    Source = "following"
                }));
            }

            // Trending (5-10%)
            var trending = await _pinRepo
                .GetTrendingPinsAsync(3, null);
            feedItems.AddRange(trending.Select(p => new FeedItem
            {
                PinId = p.PinId, RankScore = 0.6,
                Source = "trending"
            }));

            // Deduplicate and rank
            var ranked = feedItems
                .GroupBy(i => i.PinId)
                .Select(g => g.First())
                .OrderByDescending(i => i.RankScore)
                .ToList();

            // Inject ads
            int adCount = (int)Math.Ceiling(
                ranked.Count * _config.AdFeedRatio);
            var ads = await _adsService
                .GetRelevantAdsAsync(userId, adCount);
            ranked = InjectAds(ranked, ads);

            // Apply diversity constraints
            ranked = ApplyDiversity(ranked);

            var page = new FeedPage
            {
                Items = ranked,
                HasMore = true,
                NextCursor = GenerateCursor(ranked.LastOrDefault())
            };

            await _cache.SetStringAsync(cacheKey,
                JsonSerializer.Serialize(page),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow =
                        TimeSpan.FromMinutes(
                            _config.FeedCacheTtlMinutes)
                });

            return page;
        }

        private List<FeedItem> InjectAds(
            List<FeedItem> items, List<FeedItem> ads)
        {
            var result = new List<FeedItem>(items);
            int interval = Math.Max(4,
                result.Count / Math.Max(1, ads.Count));

            for (int i = 0; i < ads.Count
                && i * interval < result.Count; i++)
            {
                int pos = Math.Min(
                    (i + 1) * interval, result.Count);
                ads[i].IsPromoted = true;
                result.Insert(pos, ads[i]);
            }
            return result;
        }

        private List<FeedItem> ApplyDiversity(List<FeedItem> items)
        {
            var result = new List<FeedItem>();
            var recentSources = new Queue<string>();

            foreach (var item in items)
            {
                var last = recentSources.TakeLast(2).ToList();
                if (last.Count == 2
                    && last.All(s => s == item.Source))
                    continue;

                result.Add(item);
                recentSources.Enqueue(item.Source);
                if (recentSources.Count > 10)
                    recentSources.Dequeue();
            }
            return result;
        }

        private string GenerateCursor(FeedItem? last)
        {
            if (last == null) return string.Empty;
            return Convert.ToBase64String(Encoding.UTF8.GetBytes(
                JsonSerializer.Serialize(
                    new { last.PinId, last.RankScore })));
        }
    }

    // ==================== BOARD SERVICE ====================
    public class BoardService
    {
        private readonly IBoardRepository _boardRepo;
        private readonly IPinRepository _pinRepo;
        private readonly IDistributedCache _cache;
        private readonly ILogger<BoardService> _logger;

        public BoardService(
            IBoardRepository boardRepo, IPinRepository pinRepo,
            IDistributedCache cache,
            ILogger<BoardService> logger)
        {
            _boardRepo = boardRepo;
            _pinRepo = pinRepo;
            _cache = cache;
            _logger = logger;
        }

        public async Task<Board> CreateBoardAsync(
            long userId, string name,
            string? description, bool isPrivate)
        {
            var board = new Board
            {
                BoardId = GenerateId(),
                UserId = userId,
                Name = name,
                Description = description,
                IsPrivate = isPrivate
            };
            await _boardRepo.CreateBoardAsync(board);
            _logger.LogInformation(
                "Board {BoardId} created by user {UserId}",
                board.BoardId, userId);
            return board;
        }

        public async Task<List<Pin>> GetBoardPinsAsync(
            long boardId, int limit = 25, int offset = 0)
        {
            string key = $"board:{boardId}:pins:{offset}:{limit}";
            var cached = await _cache.GetStringAsync(key);
            if (cached != null)
                return JsonSerializer.Deserialize<List<Pin>>(cached)
                    ?? new();

            var pins = await _pinRepo
                .GetPinsByBoardIdAsync(boardId, limit, offset);

            await _cache.SetStringAsync(key,
                JsonSerializer.Serialize(pins),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow =
                        TimeSpan.FromMinutes(10)
                });
            return pins;
        }

        public async Task ReorderBoardPinsAsync(
            long boardId, List<long> pinIds, int expectedVersion)
        {
            var board = await _boardRepo.GetBoardByIdAsync(boardId);
            if (board == null)
                throw new InvalidOperationException("Board not found");

            if (board.Version != expectedVersion)
                throw new InvalidOperationException(
                    "Version conflict — board was modified");

            for (int i = 0; i < pinIds.Count; i++)
            {
                await _boardRepo.RemovePinFromBoardAsync(
                    boardId, pinIds[i]);
                await _boardRepo.AddPinToBoardAsync(
                    boardId, pinIds[i], i);
            }

            await _cache.RemoveAsync($"board:{boardId}:pins");
        }

        private static long GenerateId()
        {
            var epoch = new DateTime(2024, 1, 1, 0, 0, 0,
                DateTimeKind.Utc);
            var ts = (long)(DateTime.UtcNow - epoch).TotalMilliseconds;
            var rand = new Random().Next(0, 9999);
            return (ts << 12) | (long)rand;
        }
    }
}

27. Multi-Language & Internationalization

Pinterest operates in over 75 countries and supports 30+ languages, serving localized content to users across diverse cultural contexts. The internationalization (i18n) subsystem handles content translation, localized recommendations, right-to-left (RTL) script rendering, and region-specific content policies — all while maintaining a consistent user experience.

Content Translation Pipeline

graph TB A["Pin Created with Text"] --> B["Language Detection"] B --> C{"Target Languages?"} C --> D["Translation Queue"] D --> E["Neural MT Model"] E --> F["Quality Score Filter"] F -->|"Score > 0.7"| G["Store Translations"] F -->|"Score < 0.7"| H["Human Review Queue"] G --> I["Elasticsearch Multilingual Index"] G --> J["Pin Metadata Update"] I --> K["Localized Search"] J --> L["Localized Pin Display"]

Internationalization Architecture

The i18n system operates across four key dimensions: language detection, content translation, localized ranking, and regional policy enforcement. When a pin is created, its text content (title, description, alt text) is automatically translated into the top 10 most-spoken languages on the platform using a fine-tuned neural machine translation model. Translations are quality-scored, and only those above a confidence threshold of 0.7 are published automatically. Lower-confidence translations are queued for human review by native-speaker moderators.

Localized Recommendation Strategies

Recommendations are not simply translated — they are re-ranked based on regional relevance. A user in Japan browsing "home decor" receives results weighted toward Japanese aesthetics (wabi-sabi, minimalism), while a user in Brazil receives results weighted toward colorful, tropical styles. The recommendation engine incorporates regional engagement patterns, local trending topics, and cultural event calendars (Lunar New Year, Diwali, Ramadan) to adjust content surfacing. Country-specific boards and creators are boosted within their regions to promote local community growth.

RTL and Script Support

Right-to-left languages (Arabic, Hebrew, Urdu, Persian) require special handling across the entire UI stack and content pipeline. The CSS layer uses direction: rtl and unicode-bidi properties, with logical CSS properties (e.g., margin-inline-start instead of margin-left) to automatically mirror layouts. Within the recommendation and search systems, tokenization and text processing use language-aware analyzers in Elasticsearch that correctly handle Arabic morphology, Hebrew root extraction, and CJK (Chinese, Japanese, Korean) character segmentation.

FeatureSupported LanguagesImplementationSpecial Handling
UI Text30+ languagesICU resource bundles with fallback chainsGender-aware plurals, date/number formatting
Content TranslationTop 10 MT languagesNeural MT with domain fine-tuningCultural context preservation, idiom handling
Search Tokenization40+ languagesLanguage-specific Elasticsearch analyzersCJK bigrams, Arabic morphology, compound words
RTL LayoutArabic, Hebrew, Urdu, PersianCSS logical properties + bidirectional algorithmMixed LTR/RTL content in pin descriptions
Visual SearchLanguage-agnosticImage embeddings bypass text languageText-in-image OCR supports 20+ scripts
Content PoliciesPer-regionRegion-based policy rule engineCountry-specific legal requirements
// Multi-language content processing service
public class LocalizationService
{
    private readonly ITranslationEngine _translator;
    private readonly ILanguageDetector _languageDetector;
    private readonly IContentPolicyEngine _policyEngine;

    public async Task<LocalizedPin> LocalizePinAsync(
        Pin pin, string targetLanguage, string targetRegion)
    {
        var detectedLang = await _languageDetector
            .DetectAsync(pin.Title, pin.Description);

        var translations = new Dictionary<string, PinTranslation>();
        if (detectedLang != targetLanguage)
        {
            var result = await _translator.TranslateAsync(
                new TranslationRequest
                {
                    SourceText = $"{pin.Title}\n{pin.Description}",
                    SourceLanguage = detectedLang,
                    TargetLanguage = targetLanguage,
                    Domain = "pinterest_home_decor"
                });

            if (result.ConfidenceScore >= 0.7)
            {
                var parts = result.TranslatedText
                    .Split('\n', 2);
                translations[targetLanguage] = new PinTranslation
                {
                    Title = parts.ElementAtOrDefault(0)
                        ?? pin.Title,
                    Description = parts.ElementAtOrDefault(1)
                        ?? pin.Description,
                    Confidence = result.ConfidenceScore,
                    ModelVersion = result.ModelVersion
                };
            }
        }

        var policyCheck = await _policyEngine
            .EvaluateAsync(pin, targetRegion);

        var displayTitle = translations.ContainsKey(targetLanguage)
            ? translations[targetLanguage].Title
            : pin.Title;

        return new LocalizedPin
        {
            PinId = pin.PinId,
            OriginalLanguage = detectedLang,
            DisplayTitle = displayTitle,
            DisplayDescription = translations
                .GetValueOrDefault(targetLanguage)?.Description
                ?? pin.Description,
            Translations = translations,
            IsApproved = policyCheck.IsApproved,
            PolicyFlags = policyCheck.Flags,
            RTL = IsRTL(targetLanguage)
        };
    }

    public async Task<SearchResults> LocalizedSearchAsync(
        string query, string userLanguage, string region,
        int topK)
    {
        var expandedQuery = await ExpandQueryForLocale(
            query, userLanguage, region);

        var results = await SearchWithQuery(
            expandedQuery, userLanguage, topK * 2);

        var localized = new List<LocalizedSearchResult>();
        foreach (var result in results)
        {
            var loc = await LocalizePinAsync(
                result.Pin, userLanguage, region);
            if (loc.IsApproved)
                localized.Add(new LocalizedSearchResult
                {
                    LocalizedPin = loc,
                    OriginalScore = result.Score,
                    RegionalBoost = GetRegionalBoost(
                        result.Pin, region)
                });
        }

        return new SearchResults
        {
            Items = localized
                .OrderByDescending(r =>
                    r.OriginalScore * r.RegionalBoost)
                .Take(topK)
                .ToList(),
            QueryLanguage = userLanguage,
            Region = region
        };
    }

    private bool IsRTL(string languageCode) =>
        languageCode is "ar" or "he" or "ur" or "fa" or "yi";

    private double GetRegionalBoost(Pin pin, string region)
    {
        if (pin.Metadata?.Region == region) return 1.3;
        if (pin.Metadata?.GlobalAppeal == true) return 1.1;
        return 1.0;
    }

    private async Task<string> ExpandQueryForLocale(
        string query, string language, string region)
    {
        var expansions = new Dictionary<string, string[]>
        {
            ["ja"] = new[] { "和風", "日本デザイン" },
            ["ar"] = new[] { "عربي", "تقليدي" },
            ["pt-BR"] = new[] { "brasileiro", "tropical" }
        };

        if (expansions.TryGetValue(region, out var extra))
            return $"{query} {string.Join(" ", extra)}";
        return query;
    }
}

public class LocalizedPin
{
    public long PinId { get; set; }
    public string OriginalLanguage { get; set; }
    public string DisplayTitle { get; set; }
    public string DisplayDescription { get; set; }
    public Dictionary<string, PinTranslation> Translations { get; set; }
    public bool IsApproved { get; set; }
    public List<string> PolicyFlags { get; set; }
    public bool RTL { get; set; }
}

public class PinTranslation
{
    public string Title { get; set; }
    public string Description { get; set; }
    public double Confidence { get; set; }
    public string ModelVersion { get; set; }
}

Region-Specific Content Policies: Different countries impose distinct content regulations. Germany restricts certain historical symbols, China requires content filtering for specific political topics, and EU regions mandate GDPR-compliant data handling. The content policy engine maintains a per-region rule set evaluated at pin creation time and during periodic re-scans. Pins that violate regional policies are geo-blocked rather than globally removed, ensuring compliance without unnecessary content restriction in other markets.

28. Pinterest API & Developer Platform

Pinterest's developer platform exposes a comprehensive set of APIs and tools that enable third-party developers, merchants, and enterprise partners to integrate with Pinterest's ecosystem. The platform supports content publishing, catalog management, tag-based tracking, analytics retrieval, and shopping integrations — serving over 5 million business accounts and thousands of API partners.

Developer Platform Architecture

graph TB subgraph DeveloperAccess DevPortal["Developer Portal"] OAuth["OAuth 2.0 Auth"] APIKeys["API Key Management"] end subgraph APIGateway RateLimit["Rate Limiter"] QuotaMgr["Quota Manager"] VersionMgr["API Version Router"] Gateway["API Gateway"] end subgraph CoreAPIs PinAPI["Pins API"] BoardAPI["Boards API"] CatalogAPI["Catalog API"] AdsAPI["Ads API"] AnalyticsAPI["Analytics API"] end subgraph Services Webhook["Webhook Dispatcher"] BatchJob["Batch Processing"] TagMgr["Tag Manager"] FeedProc["Catalog Feed Processor"] end DevPortal --> OAuth OAuth --> APIKeys APIKeys --> RateLimit RateLimit --> QuotaMgr QuotaMgr --> VersionMgr VersionMgr --> Gateway Gateway --> PinAPI Gateway --> BoardAPI Gateway --> CatalogAPI Gateway --> AdsAPI Gateway --> AnalyticsAPI PinAPI --> Webhook CatalogAPI --> FeedProc CatalogAPI --> BatchJob AdsAPI --> TagMgr

Catalog Ingestion Pipeline

Merchants upload product catalogs as structured feeds (CSV, XML, or via API) containing product titles, descriptions, prices, images, and availability. The catalog ingestion pipeline normalizes, validates, deduplicates, and matches products to existing pins or creates new product pins automatically.

Tag Management System

The Pinterest Tag is a JavaScript snippet installed on merchant websites that tracks user actions (page views, add-to-cart, purchases) for conversion attribution and retargeting audiences. The tag system must handle billions of events per day, support server-side tagging for privacy compliance, and provide real-time audience building for ad targeting. Tags are versioned and managed through a UI that allows merchants to configure event triggers, custom parameters, and consent modes.

API ComponentEndpoint CategoryRate LimitAuth LevelKey Features
Pins APICRUD operations for pins1,000 req/min (Basic)OAuth 2.0Create, update, delete pins; bulk operations
Boards APIBoard management1,000 req/min (Basic)OAuth 2.0Create boards, manage collaborators
Catalog APIProduct feed ingestion100 feeds/dayPartner OAuthCSV/XML upload, feed scheduling, product matching
Ads APICampaign management5,000 req/min (Partner)Partner OAuthCreate campaigns, manage budgets, bid strategies
Analytics APIPerformance data200 req/minOAuth 2.0Impressions, saves, clicks, audience insights
Tags APIConversion trackingServer-side eventsAPI Key + SecretEvent tracking, audience building, conversions
Webhooks APIEvent notificationsConfigurableHMAC signaturePin creation, board updates, campaign status
// Pinterest API client for catalog ingestion
public class PinterestCatalogApiClient
{
    private readonly HttpClient _httpClient;
    private readonly string _accessToken;

    public PinterestCatalogApiClient(
        HttpClient httpClient, string accessToken)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = new Uri(
            "https://api.pinterest.com/v5/");
        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue(
                "Bearer", accessToken);
        _accessToken = accessToken;
    }

    public async Task<CatalogFeedResponse> UploadCatalogFeedAsync(
        string merchantId, Stream csvFeedStream,
        string feedName)
    {
        var content = new MultipartFormDataContent();
        content.Add(new StreamContent(csvFeedStream),
            "feed", $"{feedName}.csv");
        content.Add(new StringContent(merchantId),
            "merchant_id");
        content.Add(new StringContent(feedName),
            "name");
        content.Add(new StringContent("csv"),
            "format");
        content.Add(new StringContent("catalog"),
            "default_availability");

        var response = await _httpClient.PostAsync(
            "catalogs/feeds", content);
        response.EnsureSuccessStatusCode();

        return await response.Content
            .ReadFromJsonAsync<CatalogFeedResponse>();
    }

    public async Task<List<ProductMatchResult>>
        MatchProductsToPinsAsync(
            string catalogId, List<ProductItem> products)
    {
        var results = new List<ProductMatchResult>();

        foreach (var batch in products.Chunk(100))
        {
            var request = new
            {
                items = batch.Select(p => new
                {
                    product_id = p.ProductId,
                    title = p.Title,
                    description = p.Description,
                    price = p.Price,
                    currency = p.Currency,
                    image_link = p.ImageUrl,
                    availability = p.InStock
                        ? "in_stock" : "out_of_stock",
                    brand = p.Brand,
                    category = p.Category
                }).ToList()
            };

            var json = JsonSerializer.Serialize(request);
            var httpContent = new StringContent(
                json, Encoding.UTF8, "application/json");

            var response = await _httpClient.PostAsync(
                $"catalogs/{catalogId}/items/bulk",
                httpContent);
            response.EnsureSuccessStatusCode();

            var batchResult = await response.Content
                .ReadFromJsonAsync
                    <List<ProductMatchResult>>();
            if (batchResult != null)
                results.AddRange(batchResult);
        }

        return results;
    }

    public async Task<ConversionEventResponse>
        TrackConversionEventAsync(
            ConversionEvent conversion)
    {
        var request = new
        {
            event_name = conversion.EventType,
            event_time = conversion.Timestamp
                .ToUnixTimeSeconds(),
            user_email = HashEmail(conversion.UserEmail),
            user_ip = conversion.UserIp,
            pin_id = conversion.PinId.ToString(),
            order_quantity = conversion.Quantity,
            value = conversion.Value,
            currency = conversion.Currency,
            custom_data = conversion.CustomProperties
        };

        var json = JsonSerializer.Serialize(request);
        var content = new StringContent(
            json, Encoding.UTF8, "application/json");

        var response = await _httpClient.PostAsync(
            "events/track", content);
        response.EnsureSuccessStatusCode();

        return await response.Content
            .ReadFromJsonAsync<ConversionEventResponse>();
    }

    public async Task<CreatorAnalyticsResponse>
        GetCreatorAnalyticsAsync(
            string profileId, DateTime startDate,
            DateTime endDate)
    {
        var url = $"analytics/" +
            $"profiles/{profileId}?" +
            $"start_date={startDate:yyyy-MM-dd}" +
            $"&end_date={endDate:yyyy-MM-dd}" +
            $"&metrics=IMPRESSIONS,SAVES,CLICKS" +
            $"&granularity=DAY";

        var response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await response.Content
            .ReadFromJsonAsync<CreatorAnalyticsResponse>();
    }

    private string HashEmail(string email)
    {
        using var sha = SHA256.Create();
        var hash = sha.ComputeHash(
            Encoding.UTF8.GetBytes(email.ToLower().Trim()));
        return Convert.ToBase64String(hash);
    }
}

public class CatalogFeedResponse
{
    public string FeedId { get; set; }
    public string Status { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class ProductMatchResult
{
    public string ProductId { get; set; }
    public string MatchedPinId { get; set; }
    public string MatchStatus { get; set; }
    public double MatchScore { get; set; }
}

public class ConversionEvent
{
    public string EventType { get; set; }
    public DateTime Timestamp { get; set; }
    public string UserEmail { get; set; }
    public string UserIp { get; set; }
    public long? PinId { get; set; }
    public int Quantity { get; set; }
    public decimal Value { get; set; }
    public string Currency { get; set; }
    public Dictionary<string, string> CustomProperties { get; set; }
}

Developer Platform Scalability: The API gateway processes over 50 billion API calls per month. Rate limiting uses a sliding window algorithm with per-key quotas enforced at the edge via Cloudflare Workers. The catalog ingestion pipeline handles 100+ million product items daily across all merchants, with incremental feed updates processed within 4 hours. Webhook delivery uses an at-least-once guarantee with exponential backoff and dead-letter queues for failed deliveries. All API responses include structured error codes, request IDs for debugging, and deprecation notices for versioned endpoints.

29. Conclusion

Designing a Pinterest-like visual discovery platform at 480M+ monthly users is one of the most challenging and rewarding system design exercises. The system sits at the intersection of multiple engineering disciplines: large-scale distributed systems, machine learning, computer vision, real-time personalization, and content delivery — all operating at extreme scale.

Key Architectural Takeaways

  1. Read-heavy optimization: The system is heavily optimized for reads with multi-level caching (client, CDN, Redis, database), pre-computed feeds, and denormalized data. The read-to-write ratio is approximately 50:1.
  2. ML-native architecture: Machine learning is not a bolt-on feature but deeply integrated into every system — feed ranking, visual search, content moderation, ads auction, and recommendation all rely on ML models served in real-time.
  3. Async processing: Heavy operations (image processing, embedding generation, feed fanout, analytics) are handled by async worker fleets processing Kafka events, keeping the critical path (API responses) fast.
  4. Hybrid consistency: Strong consistency for critical paths (payments, board edits with optimistic locking) and eventual consistency everywhere else (feeds, search indexes, follower counts).
  5. Graceful degradation: Circuit breakers, fallback feeds, cached responses, and read-only modes ensure the platform remains functional even when downstream services experience issues.

What to Study Next

  • Two-Tower Models: Deep dive into Pinterest's two-tower architecture for candidate generation and ranking.
  • ScaNN and Vector Quantization: Understanding anisotropic vector quantization for billion-scale ANN search.
  • Real-time Feature Stores: How to build and maintain low-latency feature stores for online ML serving.
  • Content Moderation at Scale: Multi-modal classifiers, hash-matching systems, and human-in-the-loop review pipelines.
  • Monetization Optimization: Auction theory, GSP mechanisms, and the balance between ad revenue and user experience.

Pinterest's architecture continues to evolve as the platform grows. Recent investments in AI-generated content, augmented reality try-on features, and multi-modal understanding (combining text, image, and video signals) push the boundaries of what a visual discovery platform can be. Understanding these systems provides a solid foundation for designing any large-scale content platform.

© 2026 Ayodhyya. All rights reserved.

System Design Articles for Senior Engineers