System Design Problem

Design Ephemeral Stories (Instagram Stories)

Commonly Asked By:MetaSnapLinkedInWhatsApp

Interview Setup

Interview Prompt

Design Instagram Stories with ephemeral photo and video posts that auto-delete after 24 hours. A daily active user base of 500M posts 500M stories per day, and users view a story tray of followed accounts with unviewed ring indicators.

Clarifying Questions (ask before designing)

QuestionWhy it matters
Must the story become inaccessible at exactly 24 hours, or is a few minutes of physical cleanup lag acceptable?Serving authorization can enforce expires_at precisely, while storage cleanup is asynchronous and may require a small operational grace window.
Does the poster see who viewed their story?The viewer list generates 10B writes per day, or roughly 115K writes per second, which requires a dedicated high volume asynchronous write path isolated from media serving.
How is the story tray ordered, such as pure recency or close friends first?Combining close friends affinity, interaction score, and unviewed ring state requires a multi signal ranking function rather than a simple timestamp sort.
Do Highlights (pinned stories) opt out of deletion?Highlights copy media to permanent storage before the 24h TTL fires on the original.

Scope

In scope

  • 24 hour TTL
  • Viewer list tracking
  • Story ring ordering
  • CDN pre warming
  • Soft delete
  • Capacity estimation with shown math

Out of scope (state explicitly)

  • Full ML ranking model training pipeline
  • Direct messaging and real time chat
  • Ad insertion and monetization

Functional Requirements

Confirm 24 hour TTL and viewer list tracking constraints. The architecture must support story uploads, follower tray assembly, asynchronous view tracking, and automatic expiry, while clarifying requirements for permanent story highlights.

In the room: ask whether viewer lists must be updated in real time or can tolerate slight lag, because real time writes introduce significant database load.

  • Post stories: Upload photo and video stories that become inaccessible after 24 hours, with physical storage reclamation occurring asynchronously
  • View stories: Retrieve and watch stories from followed users through a ranked story tray
  • Story ring: Display a colored ring indicator highlighting unviewed stories
  • View tracking: Track and display which accounts viewed each posted story
  • Story reactions: Allow direct replies and emoji reactions to active stories
  • Close friends: Restrict story visibility to an author-curated subset of followers
  • Highlights: Pin selected stories permanently to a user profile to opt out of 24 hour deletion
  • Story ordering: Prioritize close friends and unviewed stories before sorting by recency

Non-Functional Requirements

Story tray rendering must load under a few hundred milliseconds. Storage must reclaim expired media automatically. Measure CDN egress for story views separately from upload bandwidth because CDN delivery dominates network costs, and reliable TTL deletion prevents storage bloat across billions of daily views.

  • Low Latency: Story tray loads in under 300 ms and media playback begins within 1 second
  • Auto-Deletion: Stories must reliably disappear after 24 hours within a tight operational window
  • High Throughput: Support 500M stories posted per day and 10B daily story views
  • High Availability: Maintain 99.99% availability for story tray rendering and playback
  • Eventual Consistency: View counts and tray indicators converge within seconds in the serving region, while cross-region state follows the configured replication lag

Capacity Estimations

Daily active users multiplied by stories posted per day and average media file size dictates object storage requirements, while total daily views dictate CDN edge bandwidth and egress capacity.

MetricCalculationValue
DAUGiven500M
Stories posted / dayGiven500M
Story views / dayGiven10B
Avg story media sizeGiven2 MB
Upload storage / day500M x 2 MB1 PB logical media volume (24h window)
Average view event rate10B ÷ 86400~115.7K events/sec
Peak story view requests / secScenario peak200K
Theoretical CDN transfer / day10B x 2 MB20 PB/day upper bound before cache savings
Storage steady stateGiven~1 PB logical raw media (24h window, excluding variants and overhead)

Architecture Diagram

Stories are ephemeral media governed by a 24 hour product lifecycle. The upload workflow stores raw assets in object storage, indexes metadata with explicit expires_at timestamps and processing status, publishes a story ready event only after media processing and moderation succeed, serves follower trays from an in-memory timeline cache, and enforces logical expiry at read time while storage level TTL mechanisms in Amazon S3 and Apache Cassandra handle physical reclamation. Background sweepers remain a recovery mechanism rather than the primary deletion path.

In the room: confirm whether viewer lists fall within primary scope because real time per viewer writes create immense database pressure, whereas batch processing or approximate counters absorb peak traffic gracefully.

Loading...

Component Deep Dives

24 Hour Auto-Deletion Architecture

The lifecycle moves media from upload through processing, moderation, CDN distribution, tray assembly, and logical expiration. Cassandra story records use a native TTL of 86400 seconds, while Time-Window Compaction Strategy (TWCS) helps reclaim expired TTL data when its windows align with the workload. Amazon S3 lifecycle rules make raw and transcoded media under the /stories/ prefix eligible for deletion after 24 hours, but physical deletion is asynchronous. Redis metadata and tray entries use explicit TTLs. The serving path always checks expires_at and is_deleted, so stale caches or objects cannot extend story visibility. A recovery sweeper can repair derived state if an expiry event is missed. For Highlights, the service copies the asset into permanent object storage and verifies the copy before the original story lifecycle ends. When an expired story record is updated, the remaining TTL is preserved so the update does not extend retention.

View Tracking and High Volume Writes

Handling 10B story views per day translates to an average write rate of about 115.7K view events per second. To prevent database degradation on the critical playback path, client view beacons publish directly to a Kafka topic partitioned by viewer ID. Each event carries a stable event_id, and consumer workers batch idempotent writes into a Cassandra story_viewers table bucketed by (story_id, viewer_bucket)with 256 buckets and a 24 hour TTL. For rapid ring rendering in the user interface, workers update a Redis set to answer whether a user has viewed an author's story. Unique view counters are incremented from deduplicated state and periodically reconciled from Cassandra so at-least-once replays cannot permanently inflate counts. Consumer offsets are committed only after the durable batch write succeeds.

Story Tray Ordering Algorithm

The story tray ranks followed accounts using a dynamic scoring function that balances unviewed state, historical affinity, content freshness, user fatigue, and audience tiering: Score = interaction_score x recency x unviewed_multiplier x close_friend_multiplier − fatigue. The exact weights are scenario assumptions. Offline Apache Spark jobs compute historical interaction scores daily by evaluating direct messages, comments, and profile visits. Close friends receive an explicit 2x multiplier, while unviewed stories receive the highest unviewed multiplier. To maintain sub-300 millisecond response times, computed tray orderings are cached in Redis under tray:{user_id} with a 5 minute TTL. The tray service still filters cached authors against current status and expires_at before returning results.

Event Bus Design (Kafka)

An event driven Kafka backbone decouples media ingestion from heavy processing, fan out invalidation, expiry events, and asynchronous view logging. Cassandra CDC prevents a direct database-to-Kafka dual write on the story creation path, while story-ready acts as the gate before derived distribution.

YAML
topics:
  story-uploaded:
    partition_key: author_id
    partitions: 64
    retention_hours: 48
    replication_factor: 3
    purpose: "Media processing and transcoding pipeline"
  story-ready:
    partition_key: author_id
    partitions: 64
    retention_hours: 48
    replication_factor: 3
    purpose: "Fan-out only after media processing and moderation succeed"
  story-viewed:
    partition_key: viewer_id
    partitions: 64
    retention_hours: 48
    replication_factor: 3
    purpose: "Asynchronous view tracking and tray state updates"
  story-expired:
    partition_key: author_id
    partitions: 64
    retention_hours: 48
    replication_factor: 3
    purpose: "Derived-state invalidation after logical expiry"
  story-invalidated:
    partition_key: author_id
    partitions: 64
    retention_hours: 48
    replication_factor: 3
    purpose: "Derived-state invalidation after manual deletion or audience changes"
  story-reacted:
    partition_key: reactor_id
    partitions: 32
    retention_hours: 48
    replication_factor: 3
    purpose: "Asynchronous story reaction persistence and notifications"

event_schemas:
  story_uploaded:
    event_id: "uuid"
    story_id: "uuid"
    author_id: "uuid"
    s3_key: "string"
    audience: "everyone | close_friends"
    posted_at: "timestamp"
    expires_at: "timestamp"
  story_ready:
    event_id: "uuid"
    story_id: "uuid"
    author_id: "uuid"
    processed_media_manifest: "string"
    ready_at: "timestamp"
    expires_at: "timestamp"
  story_viewed:
    event_id: "uuid"
    viewer_id: "uuid"
    author_id: "uuid"
    story_id: "uuid"
    viewer_bucket: "integer"
    view_session_id: "uuid"
    timestamp: "timestamp"
  story_reacted:
    event_id: "uuid"
    reactor_id: "uuid"
    author_id: "uuid"
    story_id: "uuid"
    reaction: "string"
    timestamp: "timestamp"
  story_expired:
    event_id: "uuid derived deterministically from story_id + expires_at"
    story_id: "uuid"
    author_id: "uuid"
    expires_at: "timestamp"
    timestamp: "timestamp"
  story_invalidated:
    event_id: "uuid"
    story_id: "uuid"
    author_id: "uuid"
    reason: "deleted | privacy_changed"
    timestamp: "timestamp"

consumer_groups:
  media-processor:
    actions:
      - "Generate image resizes, transcoded video variants, and thumbnails"
      - "Execute automated content moderation"
      - "Upload processed assets to S3 and persist status=ready with the processed media manifest only when expires_at is still in the future and the story is not deleted"
      - "Cassandra CDC publishes story-ready on the verified status=ready transition"
      - "On processing or moderation failure, persist status=failed and do not publish story-ready"
  tray-fanout:
    actions:
      - "Execute ZADD on active_stories:{shard} with score=expires_at after story-ready"
      - "Insert a bucketed story_expiry_queue record keyed by expiry_bucket and expiry_shard"
      - "Invalidate follower tray caches via tray:{follower_id}, where repeated story ready events remain idempotent by story_id"
      - "On story-expired, remove the author from the derived active-story index when no active story remains"
  view-tracker:
    actions:
      - "Use story_id + viewer_id as the durable idempotency key and derive viewer_bucket from viewer_id"
      - "Batch idempotent view records to Cassandra story_viewers and commit Kafka offsets only after the durable write succeeds"
      - "After durable persistence, ZADD story_viewed:{viewer}:{author} with score=expires_at and increment the counter only when the viewer is newly added. Prune members whose score is at or below now, and use replay to repair missing derived Redis state"
  reaction-tracker:
    actions:
      - "Validate active-story visibility before accepting the reaction"
      - "Idempotently upsert the current reaction by story_id + reactor_id and batch write to Cassandra story_reactions"
      - "Emit author notification events only after the durable reaction write"
  invalidation-tracker:
    actions:
      - "Invalidate follower tray caches after story invalidated events"
      - "Refresh or remove derived active-story state after manual deletion or audience changes"

execution_paths:
  synchronous: "Client uploads media through a server-issued object-storage upload reference, the service verifies ownership and records metadata with status=processing, and Cassandra CDC publishes story-uploaded"
  asynchronous: "Media processing and moderation publish story-ready, which drives CDN edge warm-up and tray fan out. An expiry worker processes due story_expiry_queue buckets incrementally and publishes story-expired for derived-state invalidation. Manual deletion and audience changes publish story-invalidated. Cassandra TTL and S3 lifecycle rules perform physical cleanup, while the serving path enforces expires_at"
  staging_cleanup: "An object-storage lifecycle rule reclaims abandoned staging uploads after a short retention window, such as 6 hours"
  dead_letter_queue:
    topic: "story-uploaded-dlq"
    alert_condition: "Media processor consumer lag exceeds 2 minutes"

API Design

Create Story Upload

Creates a server-issued upload reference so the client never chooses an arbitrary object-storage path. The upload URL targets a temporary staging object, is scoped to the authenticated user, media type, and size limit, and has a short expiration. Only verified uploads are finalized into the story storage namespace.

HTTP
POST /api/v1/story-uploads HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>
Content-Type: application/json

{
  "media_type": "video/mp4",
  "size_bytes": 2097152
}

HTTP/1.1 201 Created
Content-Type: application/json

{
  "upload_id": "upload-uuid",
  "upload_url": "https://object-store.example/signed-put-token",
  "expires_at": "2026-03-15T10:15:00Z"
}

Post Story

Finalizes a new ephemeral story record after the uploaded object in the staging area is verified, then moves or copies it to the immutable story object path and begins asynchronous media processing and moderation.

HTTP
POST /api/v1/stories HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>
Idempotency-Key: create-story-uuid
Content-Type: application/json

{
  "upload_id": "upload-uuid",
  "audience": "everyone",
  "stickers": [],
  "mentions": ["@alice"]
}

HTTP/1.1 201 Created
Content-Type: application/json

{
  "story_id": "s-uuid",
  "expires_at": "2026-03-16T10:00:00Z",
  "status": "processing"
}

Delete Story

Authorizes the owner, marks the story deleted immediately for serving authorization, and emits an invalidation event so derived tray state is removed asynchronously.

HTTP
DELETE /api/v1/stories/s-uuid HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "story_id": "s-uuid",
  "status": "deleting"
}

Save Story to Highlights

Starts an asynchronous copy of the processed media into permanent highlight storage. The original story remains governed by its 24 hour lifecycle until the durable copy has been verified.

HTTP
POST /api/v1/stories/s-uuid/highlight HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>
Idempotency-Key: highlight-event-uuid

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "story_id": "s-uuid",
  "status": "copying_to_highlight_storage"
}

Get Story Tray

Retrieves the ordered tray of active story circles for the authenticated user, indicating unviewed status and story counts.

HTTP
GET /api/v1/stories/tray HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>

HTTP/1.1 200 OK
Content-Type: application/json

{
  "tray": [
    {
      "user_id": "u-alice",
      "username": "alice",
      "has_unviewed": true,
      "story_count": 3,
      "latest_at": "2026-03-15T09:45:00Z"
    }
  ]
}

View Story

Fetches media metadata and a short lived signed CDN playback URL for a specific story while asynchronously recording a view event. The returned view count is eventually consistent. The service rejects stories that are not ready, are deleted, are expired, or are outside the caller's audience.

HTTP
GET /api/v1/stories/s-uuid HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>

HTTP/1.1 200 OK
Content-Type: application/json

{
  "story_id": "s-uuid",
  "media_url": "https://cdn.instagram.com/stories/s-uuid.mp4?token=<short lived-signature>",
  "posted_at": "2026-03-15T10:00:00Z",
  "view_count": 342,
  "view_count_consistency": "eventual",
  "view_session_id": "view-session-uuid"
}

Get Story Viewers

Returns a paginated viewer list for the story author after an ownership check. The opaque cursor records progress across the 256 Cassandra viewer buckets. The API does not promise a strict global viewed_at ordering because the durable deduplication key uses viewer_id as the Cassandra clustering column.

HTTP
GET /api/v1/stories/s-uuid/viewers?cursor=<opaque_cursor>&limit=100 HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>

HTTP/1.1 200 OK
Content-Type: application/json

{
  "viewers": [
    {"user_id": "u-alice", "viewed_at": "2026-03-15T10:02:11Z"}
  ],
  "next_cursor": "<opaque_cursor>"
}

React to Story

Records an emoji reaction for an active story. Reaction delivery is asynchronous, and the story service enforces the same visibility rules used for playback.

HTTP
POST /api/v1/stories/s-uuid/reactions HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>
Idempotency-Key: reaction-event-uuid
Content-Type: application/json

{
  "reaction": "❤️"
}

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "story_id": "s-uuid",
  "status": "accepted"
}

Reply to Story

Accepts a reply and hands delivery to the existing messaging service because real time chat is outside this problem's scope.

HTTP
POST /api/v1/stories/s-uuid/replies HTTP/1.1
Host: api.instagram.com
Authorization: Bearer <user_token>
Content-Type: application/json

{
  "text": "Great story!"
}

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "status": "accepted",
  "delivery": "messaging-service"
}

Common Error Responses

Standard HTTP status codes and error payloads returned by the stories service endpoints.

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff

Data Model

Cassandra: Stories and Viewers Schema

Cassandra provides horizontal write scalability and native Time-To-Live (TTL) metadata. Compaction reclaims expired data asynchronously, while the serving path relies on expires_at rather than waiting for compaction to enforce visibility. MySQL owns close friends membership, while Redis contains only derived audience and tray state.

SQL
CREATE TABLE stories (
    author_id   UUID,
    story_id    TIMEUUID,
    media_url   TEXT,
    processed_media_manifest TEXT,
    media_type  TEXT,
    audience    TEXT,
    status      TEXT,        -- processing, ready, failed
    is_deleted  BOOLEAN,
    posted_at   TIMESTAMP,
    expires_at  TIMESTAMP,
    PRIMARY KEY (author_id, story_id)
) WITH CLUSTERING ORDER BY (story_id DESC)
  AND default_time_to_live = 86400;
-- Per-write updates preserve the remaining lifetime until expires_at.

-- Bucket viewers to avoid a hot Cassandra partition for viral stories.
-- viewer_bucket is derived from a stable hash of viewer_id using 256 buckets.
CREATE TABLE story_viewers (
    story_id      UUID,
    viewer_bucket SMALLINT,
    viewer_id     UUID,
    viewed_at     TIMESTAMP,
    PRIMARY KEY ((story_id, viewer_bucket), viewer_id)
);
-- Each write uses USING TTL with the remaining lifetime until the parent story's expires_at.

CREATE TABLE story_reactions (
    story_id        UUID,
    reaction_bucket SMALLINT,    -- hash(reactor_id) % 256
    reactor_id      UUID,
    reaction        TEXT,
    reacted_at      TIMESTAMP,
    PRIMARY KEY ((story_id, reaction_bucket), reactor_id)
);
-- Each write uses USING TTL with the remaining lifetime until the parent story's expires_at.

-- Durable, bucketed expiry schedule. expiry_bucket is an hour boundary.
CREATE TABLE story_expiry_queue (
    expiry_bucket TIMESTAMP,
    expiry_shard  SMALLINT,
    expires_at    TIMESTAMP,
    story_id      UUID,
    author_id     UUID,
    PRIMARY KEY ((expiry_bucket, expiry_shard), expires_at, story_id)
) WITH default_time_to_live = 172800;
-- expiry_shard = hash(story_id) % 256, allowing the expiry worker to process due buckets incrementally.

Redis Data Structures and Caching Layout

In-memory data structures accelerate tray assembly, unviewed indicator lookups, and low latency story view metrics. Redis is derived state. Cassandra and MySQL remain the durable sources of truth for story records and close friends membership respectively.

Key                                  Type      TTL      Description
────────────────────────────────────────────────────────────────────────────────────────────
active_stories:{shard}               ZSET      Derived  Author IDs scored by latest active expires_at across 256 shards
tray:{user_id}                       List      5m       Ordered author IDs for the story tray
story_viewed:{viewer}:{author}       ZSET      48h      Derived story IDs scored by expires_at, pruning expired members on access
story_views:{story_id}:{bucket}      Integer   48h      Derived sharded unique-view counter where 32 buckets prevent a viral hot key
Shard counts: 256 active-story shards, 32 view-counter shards
active_shard = hash(author_id) % 256
view_shard = hash(viewer_id) % 32

Close Friends Membership Schema

MySQL is the durable source of truth for close friends membership. Membership changes and the per-author membership version update occur in the same transaction so cached authorization state can be invalidated safely.

SQL
CREATE TABLE close_friends_membership_version (
    author_id          BINARY(16) PRIMARY KEY,
    membership_version BIGINT NOT NULL,
    updated_at         TIMESTAMP
);

CREATE TABLE close_friends (
    author_id BINARY(16),
    member_id BINARY(16),
    PRIMARY KEY (author_id, member_id)
);

Close Friends Authorization

Close friends membership is durable in MySQL and cached in Redis with a membership version. Authorization checks use the versioned cache when fresh, fall back to MySQL on a cache miss or version mismatch, and fail closed when membership state cannot be verified.

Fault Tolerance

ConcernSolution
Story remains visible after 24hEnforce expires_at on every serving path, then use Cassandra TTL, S3 lifecycle cleanup, Redis derived-state cleanup, and an asynchronous story expired event. Keep the sweeper as a recovery path for missed derived-state updates.
View tracking lossBuffer view events in Kafka, deduplicate by story_id + viewer_id, write bucketed batches to Cassandra, and commit offsets only after durable success
Media processing failureRetry transcoding three times with exponential backoff before routing to a dead letter queue and flagging as failed
Story tray stalenessEnforce a 5 minute cache TTL and invalidate entries immediately upon new story posts, story expired events, or manual pull to refresh
Reaction replay or duplicate notificationIdempotently upsert by story_id + reactor_id, use the event_id for replay detection, and publish notification events only after the durable reaction is accepted
Expires while viewingRevoke new playback authorization at expires_at. If product requirements allow a 5 minute grace period, let already-authorized sessions continue without issuing fresh signed URLs after expiry

Additional Considerations

Fan-Out for Story Tray

To assemble the story tray efficiently without overwhelming storage, the system implements a hybrid fan out model. For regular users with fewer than 10,000 followers, using 10,000 as an illustrative scenario cutoff, publishing a story pushes an invalidation signal into each follower's cached tray. For high profile celebrity accounts with millions of followers, a pure push model would cause massive fan out write amplification. Instead, the system adopts a pull on read approach for celebrity accounts by querying active story state dynamically during tray assembly. Active author membership is sharded across Redis sets. A parallel multi-shard Redis batch can evaluate 1,000 author checks in approximately 5 milliseconds as an illustrative benchmark assumption.

Close Friends Privacy

Close friends relationships have a durable MySQL source of truth and a versioned Redis cache containing approved user identifiers. Privacy rules are strictly enforced on the server during story fetch and playback authorization rather than delegating filtering to client applications. If an author removes a follower from their close friends list, the membership version increments and cache invalidations propagate. A stale or unverifiable membership state fails closed. Existing signed media URLs are short lived and bounded by the story's expires_at, but immediate revocation of an already issued URL requires very short token lifetimes or an additional CDN authorization and revocation layer.

Related Problems and Core Concepts

Ephemeral media distribution and friend feed assembly connect directly to Instagram, News Feed System, and Twitter Timeline. Direct messaging and conversational interaction excluded from this scope are explored in Real-Time Chat. High volume video processing and chunked delivery align with Video Streaming Platform. To deepen your architectural foundation, review CDN and Edge Delivery, Caching Patterns and Invalidation, Scaling 0 to 1M Users, Kafka Architecture and Guarantees, and System Design Interview Patterns.

Interview Walkthrough

  • 25 minute cut

    Skip the advanced staff-level depth unless the interview calls for detailed global scale failure handling.

    • Emphasize the 24 hour TTL constraint and storage lifecycle (5 min)
    • Separate media delivery via CDN and S3 from Cassandra metadata storage (6 min)
    • Design the story tray using hybrid push and pull fan out (5 min)
    • Track views with deduplicated counters and asynchronous Kafka pipelines (5 min)
    • Enforce Close Friends privacy, story readiness, deletion, and CDN authorization at fetch time (4 min)
  • Emphasize the 24 hour lifecycle constraint early because stories are ephemeral by design. Explain that Cassandra native TTL marks data for expiry and TWCS helps reclaim expired data efficiently when its windows align with the workload. The serving path still enforces expires_at independently.
  • Decouple large media delivery from structured metadata by serving photos and video via CDN and Edge Delivery backed by Amazon S3, while Cassandra manages story records, view counts, and expiry timestamps.
  • Architect the story tray using a hybrid fan out pattern that pushes new-story invalidation signals for accounts below the illustrative 10,000 follower cutoff while pulling active story state on tray load for high follower celebrity accounts.
  • Implement view tracking with deduplicated viewer and story pairs in bucketed Cassandra partitions, using Redis state for low latency indicators and sharded counters that are reconciled from durable data. Expose the author-only viewer list through a paginated API over the Cassandra buckets, without promising a strict global timestamp order.
  • Enforce Close Friends privacy strictly at fetch time by verifying caller identity against a versioned Redis cache backed by the durable MySQL membership source rather than trusting client side visibility logic.
  • Serve media assets through CDN edge nodes using signed URLs whose expiry never exceeds the story's expires_at, and direct metadata queries to Cassandra partitions keyed deterministically by author ID.
  • Quantify storage requirements using Back-of-the-Envelope Estimation, calculating that 500M daily active users posting 500M stories at 2 MB each generate 1 PB of fresh media daily, making proactive CDN caching and automated TTL reclamation indispensable.
  • Highlight the common interview pitfall of naive fan out on write, warning that pushing uploads to hundreds of millions of followers instantly triggers massive write storms that crash messaging queues and inbox caches.

Engineering Trade-offs

Cassandra Native TTL vs Application Level Sweepers

Relying on scheduled application cron sweepers to decide whether stories are still visible requires scanning large indexes and can create sustained read spikes. In contrast, Cassandra Native TTL attaches expiration metadata inside the storage layer. Time-Window Compaction Strategy (TWCS) can make TTL-heavy workloads easier to reclaim when write times align with configured windows. Expired data is physically reclaimed during compaction rather than precisely at the TTL boundary, so the application must still enforce expires_at on reads. This makes TTL plus serving-path expiry a better fit for ephemeral content than relying on a batch sweep as the primary mechanism.

Pull vs Push Fan Out for Story Trays

A pure Push Model delivers instant story tray rendering because follower timelines are precomputed in Redis at write time. However, when high profile accounts with tens of millions of followers publish content, push fan out produces catastrophic write amplification that saturates message queues and storage backends. Conversely, a pure Pull Model incurs zero write amplification on publish by querying every followed user upon tray load, but this shifts substantial read latency and Redis round-trips to the client rendering path.

The production approach is a Hybrid Model using the 10,000 follower boundary as an illustrative scenario cutoff. Stories from standard users below that cutoff push invalidation signals to follower inbox caches, while celebrity accounts bypass fan out queues. The story tray service retrieves follower caches and performs targeted read time lookups only for the small subset of followed celebrity accounts. This preserves sub-300 millisecond tray latency in the scenario while limiting celebrity write amplification.

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...