How to Design Visual Discovery Platform like Pinterest
Building pin feeds, visual search, and recommendation at 480M+ monthly user scale
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.
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
- Pin Management: Users can create, save, and organize pins (images/videos with metadata) into boards.
- Home Feed: Personalized infinite-scroll feed of pins based on interests, follows, and past behavior.
- Visual Search: Upload an image to find visually similar pins (Pinterest Lens).
- Text Search: Search for pins, boards, and users using keywords with autocomplete.
- Board System: Create public/private boards, add collaborators, and reorder pins within boards.
- Follow System: Follow users, boards, and interest topics.
- Idea Pins: Multi-page video/image storytelling format for creators.
- Shopping: Product pins with pricing, availability, and direct purchase links.
- Ads: Promoted pins, shopping ads, and idea pin ads in feeds and search results.
- Creator Analytics: Dashboards showing impressions, saves, clicks, and audience demographics.
Non-Functional Requirements
| Attribute | Target | Rationale |
|---|---|---|
| Availability | 99.99% (52 min downtime/year) | Consumer platform with global users across time zones |
| Latency (Feed) | p50 < 100ms, p99 < 300ms | Infinite scroll requires fast page loads |
| Latency (Search) | p50 < 200ms, p99 < 500ms | Visual search includes embedding computation |
| Throughput | 1M+ reads/sec, 100K+ writes/sec | 480M MAU with 1-2 sessions/day |
| Durability | 99.999999999% (11 nines) | User-generated content must never be lost |
| Consistency | Eventual (strong for payments) | Social features tolerate slight delay |
| Storage | 500PB+ images, 50PB+ metadata | Billions of high-resolution images |
3. Capacity Estimation
QPS Estimation
| Operation | Calculation | QPS | With 2x Buffer |
|---|---|---|---|
| Feed Reads | 300M DAU * 8 feed loads/day / 86400s | ~28,000 | ~56,000 |
| Search Queries | 300M DAU * 3 searches/day / 86400s | ~10,400 | ~21,000 |
| Pin Views | 300M DAU * 50 pins viewed/day / 86400s | ~174,000 | ~348,000 |
| Pin Creates | 300M DAU * 0.5 pins/day / 86400s | ~1,740 | ~3,500 |
| Image Uploads | 300M 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 Type | Size per Item | Daily Volume | Daily Storage | Annual |
|---|---|---|---|---|
| Original Images | 3 MB avg | 200M pins | 600 TB | ~219 PB |
| Thumbnail (3 variants) | 150 KB x 3 | 200M pins | 90 TB | ~33 PB |
| Pin Metadata (JSON) | 2 KB | 200M pins | 400 GB | ~146 TB |
| User Data | 1 KB | 2M new users | 2 GB | ~0.7 TB |
| Search Logs | 500 B | 2.5B queries | 1.25 TB | ~456 TB |
| Embedding Vectors (512-d) | 2 KB | 200M pins | 400 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
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
| Tier | Requests/min | Image Uploads/day | Visual Searches/day |
|---|---|---|---|
| Free | 1,000 | 100 | 50 |
| Business | 5,000 | 1,000 | 500 |
| Partner API | 20,000 | 10,000 | 5,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.
Service Responsibilities
| Service | Responsibility | Storage | Latency Target |
|---|---|---|---|
| Pin Service | CRUD for pins, versioning, metadata | PostgreSQL | < 50ms p99 |
| Feed Service | Personalized feed generation | Redis + PostgreSQL | < 100ms p99 |
| Search Service | Text search + visual search | Elasticsearch + ScaNN | < 300ms p99 |
| Board Service | Board CRUD, pin ordering | PostgreSQL | < 50ms p99 |
| User Service | Profile, preferences, settings | PostgreSQL + Redis | < 30ms p99 |
| Social Service | Follow/unfollow, social graph | PostgreSQL | < 50ms p99 |
| Ads Service | Auction, bidding, ad serving | Redis + PostgreSQL | < 100ms p99 |
| Notification Service | Push, email, in-app notifications | Kafka + 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.
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.
Thumbnail Generation Strategy
| Variant | Width | Use Case | Format | Typical Size |
|---|---|---|---|---|
| Small Thumbnail | 236px | Feed grid, search results | WEBP | 15-30 KB |
| Medium Thumbnail | 564px | Pin detail page | WEBP | 50-100 KB |
| Large | 1080px | High-res view, zoom | JPEG | 150-400 KB |
| Original | Native | Download, re-pin source | Original | 1-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.
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
| Stage | Latency (p50) | Latency (p99) | Parallelizable? |
|---|---|---|---|
| Image Preprocessing | 15ms | 30ms | No |
| Object Detection | 40ms | 80ms | No |
| Embedding (per crop) | 25ms | 50ms | Yes (5 crops) |
| ANN Search | 30ms | 80ms | No |
| Re-ranking | 50ms | 120ms | Yes (batch) |
| Response Assembly | 10ms | 20ms | No |
| Total | 120ms | 280ms |
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.
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.
Feed Composition Breakdown
| Source | % of Feed | Update Frequency | Personalization |
|---|---|---|---|
| Algorithmic Recommendations | 60-70% | Real-time | High (user embedding) |
| Pins from Followed Users | 15-20% | Near real-time | Follow graph |
| Trending / Editorial | 5-10% | Hourly | Low (regional) |
| Shopping Recommendations | 5-10% | Daily | Medium (purchase intent) |
| Promoted Pins (Ads) | 5-8% | Real-time auction | High (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.
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
| Operation | API | Latency | Consistency | Special Handling |
|---|---|---|---|---|
| Create Board | POST /boards | < 50ms | Strong | Name uniqueness per user |
| Add Pin to Board | POST /pins/{id}/save | < 100ms | Eventual | Fanout to followers, update counts |
| Reorder Pins | PUT /boards/{id}/order | < 200ms | Strong | Optimistic concurrency control |
| Delete Board | DELETE /boards/{id} | < 50ms | Strong | Async cleanup of pin-board associations |
| Add Collaborator | POST /boards/{id}/collab | < 100ms | Eventual | Notification, permission check |
| Section Management | POST /boards/{id}/sections | < 100ms | Eventual | Nested 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)
);
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
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
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
Ad Ranking Model
| Feature Category | Features | Source |
|---|---|---|
| User Features | Demographics, interests, purchase history, device | Feature Store |
| Ad Features | Creative quality, landing page score, historical CTR | Ads DB |
| Context Features | Time of day, device, location, search query | Request |
| Cross Features | User-ad affinity, category match, recency | Computed |
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.
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
| Level | Technology | What's Cached | TTL | Hit Rate Target |
|---|---|---|---|---|
| L1: Client | App memory | Prefetched feed pages | 5 min | 60% |
| L2: CDN | CloudFront | Images, thumbnails, static assets | 30 days | 95% |
| L3: Application | Redis Cluster | Feed pages, user profiles, pin metadata | 5-60 min | 85% |
| L4: Database | PG buffer pool | Hot rows, index pages | N/A | 99% |
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.
Data Replication Strategy
| Data Type | Replication Mode | Consistency | Conflict Resolution |
|---|---|---|---|
| User Profiles | Async (leader to follower) | Eventual (< 500ms) | Last-writer-wins (LWW) |
| Pins | Async (leader to follower) | Eventual (< 1s) | LWW with version vectors |
| Boards/Saves | Async (leader to follower) | Eventual (< 1s) | CRDT (add-only sets) |
| Feed Cache | Local generation | N/A | Regenerated per region |
| Search Index | Periodic snapshot + log replay | Eventual (< 30s) | N/A (append-only) |
| Vector Index | Periodic rebuild per region | Eventual (< 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
| Component | Specification | Monthly 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 |
| Elasticsearch | 500x r6i.4xlarge (16 vCPU, 128 GB) | $400,000 |
| S3 Storage (Images) | 500 PB at $0.023/GB | $11,500,000 |
| CloudFront CDN | 500 PB transfer/month | $40,000,000 |
| Kafka (MSK) | 200x kafka.m5.4xlarge | $200,000 |
| Data Transfer | Inter-region + internet egress | $5,000,000 |
| ML Model Training | Monthly retraining runs | $2,000,000 |
| Monitoring and Logging | CloudWatch, Datadog, PagerDuty | $300,000 |
| Security and Compliance | WAF, 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
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;
}
}
}
26. Pinterest Trends & Analytics
Pinterest Trends is one of the platform's most strategically valuable features, providing real-time and historical insight into what users are searching for, saving, and engaging with across every category. For creators, brands, and merchants, this data drives content strategy, product development, and seasonal campaign planning. The analytics subsystem must process billions of engagement events daily, compute trend signals in near real-time, and present actionable insights through intuitive dashboards.
Trend Prediction Pipeline
Trend Scoring Algorithm
Each trend candidate receives a composite score derived from multiple signals. The system detects three categories of trends: emerging (search volume increasing over 7 days), seasonal (predictable annual patterns like "Christmas decorations" in November), and viral (sudden spikes driven by cultural events or influencer activity). The composite trend score combines velocity (rate of change), volume (absolute search/save count), breadth (number of unique users engaging), and diversity (number of distinct boards and categories the trend appears in).
Seasonal Content Pattern Recognition
Pinterest's seasonal prediction engine uses a combination of Fourier analysis on historical search volumes and external signals (holiday calendars, weather forecasts, fashion week schedules) to forecast demand 4-8 weeks ahead. For example, "outdoor wedding venues" begins trending in January, peaks in April, and declines by July. "Back to school" starts trending in late June. The model maintains per-category seasonal curves calibrated on 5+ years of historical data, enabling merchants to prepare inventory and creators to publish content ahead of peak interest windows.
Creator Analytics Dashboard
The creator analytics dashboard surfaces engagement metrics, audience demographics, and content performance data. It must handle real-time metric computation for millions of creators while maintaining fast dashboard load times.
| Metric Category | Key Metrics | Granularity | Refresh Rate | Data Source |
|---|---|---|---|---|
| Reach | Impressions, unique viewers, audience reach | Per pin, per day | Hourly | Flink aggregation |
| Engagement | Saves, clicks, close-ups, comments | Per pin, per day | Near real-time | Kafka stream |
| Audience | Demographics, interests, devices, geo | Aggregate per account | Daily | Batch processing |
| Content Performance | Save rate, click-through rate, video completion | Per pin | Hourly | Flink + batch |
| Trend Alignment | Trend score of your content categories | Per category | Daily | Trend engine |
| Growth | Follower growth, impressions trend, engagement trend | Weekly, monthly | Daily | Batch rollups |
// Trend score computation service
public class TrendScoringService
{
private readonly ITrendRepository _trendRepo;
private readonly IEventStream _eventStream;
public async Task<TrendScore> ComputeTrendScoreAsync(
string keyword, string category, DateTime windowStart)
{
var metrics = await _trendRepo
.GetAggregateMetricsAsync(keyword, windowStart);
double velocity = CalculateVelocity(
metrics.DailySearchCounts, 7);
double volume = metrics.TotalSearches;
double breadth = metrics.UniqueUsers;
double diversity = metrics.DistinctBoards;
double seasonalBoost = GetSeasonalBoost(
keyword, DateTime.UtcNow.Month);
double velocityNorm = Math.Min(velocity / 1000.0, 1.0);
double volumeNorm = Math.Min(volume / 1_000_000.0, 1.0);
double breadthNorm = Math.Min(breadth / 100_000.0, 1.0);
double diversityNorm = Math.Min(diversity / 10_000.0, 1.0);
double compositeScore =
(0.35 * velocityNorm) +
(0.25 * volumeNorm) +
(0.20 * breadthNorm) +
(0.10 * diversityNorm) +
(0.10 * seasonalBoost);
return new TrendScore
{
Keyword = keyword,
Category = category,
Score = compositeScore,
Velocity = velocity,
Volume = volume,
Breadth = breadth,
ClassifiedAs = ClassifyTrend(
velocity, volume, seasonalBoost),
ComputedAt = DateTime.UtcNow
};
}
private double CalculateVelocity(
List<long> dailyCounts, int windowDays)
{
if (dailyCounts.Count < 2) return 0;
var recent = dailyCounts.TakeLast(windowDays).ToList();
var earlier = dailyCounts
.TakeLast(windowDays * 2)
.Take(windowDays).ToList();
double recentAvg = recent.Average();
double earlierAvg = earlier.Count > 0
? earlier.Average() : 1;
return (recentAvg - earlierAvg) / Math.Max(earlierAvg, 1);
}
private double GetSeasonalBoost(string keyword, int month)
{
var seasonalPatterns = new Dictionary<string, int[]>
{
["christmas"] = new[] {0,0,0,0,0,0,0,0,0.2,0.5,0.8,1.0},
["halloween"] = new[] {0,0,0,0,0,0,0,0.3,0.6,1.0,0.1,0},
["wedding"] = new[] {0.3,0.4,0.6,0.8,1.0,0.9,0.7,0.5,0.4,0.3,0.2,0.2},
["back_to_school"] = new[] {0,0,0,0,0,0,0.4,0.8,1.0,0.3,0,0}
};
foreach (var pattern in seasonalPatterns)
{
if (keyword.Contains(pattern.Key))
return pattern.Value[month - 1];
}
return 0.3; // default neutral boost
}
private string ClassifyTrend(
double velocity, double volume, double seasonalBoost)
{
if (seasonalBoost > 0.6) return "seasonal";
if (velocity > 2.0) return "emerging";
if (velocity > 5.0 && volume > 500_000) return "viral";
return "steady";
}
}
public class TrendScore
{
public string Keyword { get; set; }
public string Category { get; set; }
public double Score { get; set; }
public double Velocity { get; set; }
public long Volume { get; set; }
public long Breadth { get; set; }
public string ClassifiedAs { get; set; }
public DateTime ComputedAt { get; set; }
}
Dashboard Caching Strategy: Creator analytics dashboards are pre-computed on an hourly basis for active creators and cached in Redis with a 30-minute TTL. Top creators with millions of followers receive near real-time updates (sub-5-minute lag) via incremental aggregation windows in Flink. Historical data beyond 90 days is rolled up into daily, weekly, and monthly summaries stored in ClickHouse, enabling fast range queries without scanning raw event data.
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
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.
| Feature | Supported Languages | Implementation | Special Handling |
|---|---|---|---|
| UI Text | 30+ languages | ICU resource bundles with fallback chains | Gender-aware plurals, date/number formatting |
| Content Translation | Top 10 MT languages | Neural MT with domain fine-tuning | Cultural context preservation, idiom handling |
| Search Tokenization | 40+ languages | Language-specific Elasticsearch analyzers | CJK bigrams, Arabic morphology, compound words |
| RTL Layout | Arabic, Hebrew, Urdu, Persian | CSS logical properties + bidirectional algorithm | Mixed LTR/RTL content in pin descriptions |
| Visual Search | Language-agnostic | Image embeddings bypass text language | Text-in-image OCR supports 20+ scripts |
| Content Policies | Per-region | Region-based policy rule engine | Country-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
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 Component | Endpoint Category | Rate Limit | Auth Level | Key Features |
|---|---|---|---|---|
| Pins API | CRUD operations for pins | 1,000 req/min (Basic) | OAuth 2.0 | Create, update, delete pins; bulk operations |
| Boards API | Board management | 1,000 req/min (Basic) | OAuth 2.0 | Create boards, manage collaborators |
| Catalog API | Product feed ingestion | 100 feeds/day | Partner OAuth | CSV/XML upload, feed scheduling, product matching |
| Ads API | Campaign management | 5,000 req/min (Partner) | Partner OAuth | Create campaigns, manage budgets, bid strategies |
| Analytics API | Performance data | 200 req/min | OAuth 2.0 | Impressions, saves, clicks, audience insights |
| Tags API | Conversion tracking | Server-side events | API Key + Secret | Event tracking, audience building, conversions |
| Webhooks API | Event notifications | Configurable | HMAC signature | Pin 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
- 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.
- 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.
- 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.
- Hybrid consistency: Strong consistency for critical paths (payments, board edits with optimistic locking) and eventual consistency everywhere else (feeds, search indexes, follower counts).
- 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.
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.
Follow Types and Fanout
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.