Design Mobile News Feed with Offline-First Architecture
1. Problem Statement & Scope
System Mission
Design a battle-tested, offline-first mobile news feed client and backend cloud synchronization architecture delivering instantaneous () feed rendering from local persistent cache, robust background delta synchronization via GraphQL and Protocol Buffers, optimistic mutation pipelines with deterministic rollback, and intelligent battery/network-aware media prefetching.
Single Source of Truth (SSOT) Invariant: UI components never bind directly to network responses. Network updates commit atomically to local SQLite in WAL mode, and the UI strictly observes local database changes via reactive streams (Flow / Combine), ensuring sub-16ms cold start rendering and rock-solid state consistency.
Synthesizing vector architecture diagram...
Functional Requirements
- Zero-Latency Feed Rendering: Cold launches render cached feed items from local SQLite storage in Write-Ahead Logging (WAL) mode with zero main-thread disk I/O ().
- Bidirectional Delta Synchronization: Client presents a server-assigned monotonic synchronization cursor
since_seq_id(never a device wall-clock timestamp, see §8.2); server returns a compressed changelog containing new, modified, and soft-deleted tombstone records. - Optimistic Local Mutations & Durable Offline Queue: Feed actions (posts, likes, comments, bookmarks) immediately commit to local SQLite and update the UI optimistically. Mutations queue in a durable local SQLite outbox and replay via Android WorkManager or iOS
BGAppRefreshTaskwith exponential backoff and jitter upon network recovery. - Deterministic Conflict Resolution & Rollback: Automatic rollback on HTTP
4xxvalidation errors or409 Conflictversion mismatches, reverting the optimistic UI state and alerting the user non-destructively. - Adaptive Network & Battery Media Prefetching: Prefetch high-resolution WebP/AVIF images on unmetered high-speed WiFi; restrict prefetching to low-resolution thumbnails or halt prefetching entirely on metered cellular data or when device battery falls below .
Non-Functional Requirements (SLAs/SLOs)
- Time-To-Content (TTC) Cold Start: P95 , P99 reading from local SQLite WAL on mobile device flash storage.
- UI Smoothness & Frame Budget: 120 FPS ( per frame budget) on modern 120Hz ProMotion / LTPO displays; strict zero disk or network I/O on the UI thread.
- Network Bandwidth Efficiency: Delta sync payload per refresh ( bandwidth reduction versus 60 KB full JSON payload).
- Battery & Thermal Budget: daily device battery drain attributable to background synchronization; zero background CPU execution while app is in suspended state.
- Data Durability Guarantee: Backend achieves (11 Nines) durability via Amazon DynamoDB and Amazon S3; mobile mutation queue provides zero data loss across app crashes, OS process terminations, and device reboots.
- CAP / PACELC Classification: Mobile client is an AP node operating in PA/EL mode (Partition Availability; Else Latency over Consistency). The client guarantees instant local read/write availability, converging to eventual consistency via monotonic delta reconciliation.
Out-of-Scope
- Peer-to-peer (P2P) mesh feed distribution (e.g., Bluetooth LE mesh feed sharing).
- Live real-time video stream broadcasting (covered in dedicated Live Video Streaming blueprints).
2. Capacity & Scale Estimation
Traffic & Sync Calculations
- Active Mobile Installs: 200 Million registered devices; 50 Million Daily Active Users (DAU).
- Daily Feed Refreshes: Average 20 feed delta syncs per user per day.
- Average & Peak Sync QPS:
- Daily Mutation Volume (Writes): Average 5 mutations (likes, comments, posts) per user per day.
Bandwidth & Payload Sizing
- Full Feed Payload: 20 posts with author metadata, thumbnails, engagement counters JSON.
- Delta Sync Payload (Protobuf / GraphQL Compressed): Average 5 changed items ( cellular data savings).
- Network Bandwidth (Egress & Ingress):
Cloud Storage Calculations (3-Year Horizon)
- User Feed Record: 1.5 KB average item size.
- 3-Year Post Metadata Growth:
- Replication Factor () Across Multi-AZ DynamoDB (physical footprint; DynamoDB bills the logical 16.4 TB and replicates internally):
- Change Data Capture (CDC) Delta Outbox Table: Maintains a rolling 14-day changelog buffer for delta queries (). Each mutation fans out to the delta outbox of every follower whose feed it touches, so the item count below is a lower bound (write fan-out multiplies it):
Mobile Device Local Storage Budget
- Local SQLite Cache: Max 500 cached posts @ 2 KB per record .
- SQLite WAL Journal File: Capped at via periodic auto-checkpointing.
- Local Media LRU Disk Cache: Capped strictly at on flash storage.
- Heap RAM Bounding: active memory allocation for feed view models and decoded bitmaps.
Cloud Fleet Sizing
- Backend ECS Fargate Sync Resolvers: Each Fargate task () processes up to 500 delta sync requests/sec.
3. AWS-First High-Level Architecture
Synthesizing vector architecture diagram...
Start on the device, in the "Mobile Client Architecture Deep Dive" panel: the UI only ever reads from the local SQLite database through a reactive stream, so it works the same online or offline. A like or post is written to the local database at once (optimistically) and queued in a local mutation table; when the connectivity monitor sees a network, the background scheduler sends the queue. On the server, AppSync receives both the queued mutations and delta-sync requests. In the "Primary Storage & Change Capture Tier" panel, workers write the feed table, whose stream copies each change into a delta table kept for 14 days, so a phone returning after a week downloads only what changed. In the "Dead Letter & Async Worker Tier" panel, writes that fail go to a DLQ, where a worker reconciles them. The local database is the UI's single source of truth; the network only keeps it in sync.
Data Flow Architecture
- Local Render Path (Offline / Online): UI components bind to reactive query observables (
Kotlin Flow/Swift Combine) emitted by local SQLite. Cached posts load into memory in with zero network blocking. - Delta Sync Ingress Path: Upon confirmed network reachability, the sync engine issues a delta query over HTTP/2 presenting the client's last acknowledged monotonic sequence token
since_seq_id. CloudFront routes to AWS AppSync, which queries the DynamoDB Delta Outbox for entries strictly newer thansince_seq_id. - Local Ingestion & Tombstone Purge: The server returns an atomic change batch containing newly published, modified, and soft-deleted records. In a single local SQLite transaction, mutations are upserted, tombstoned records (
_deleted == true) are purged fromlocal_feed_posts, andlast_sync_seq_idadvances. - Optimistic Mutation Outbox Path: User interactions (likes, comments, posts) execute an atomic local SQLite transaction that updates
local_feed_postswithsync_status = DIRTYand stages the payload intolocal_mutation_queue. Background workers drain the outbox sequentially with exponential backoff.
End-to-End Request Tracing Walkthrough
| Step # | Event / Action | Component State | Distributed Transition | Output / Response |
|---|---|---|---|---|
| Step 1 | App cold launch / foreground wake | Feed UI uninitialized | UI thread attaches reactive observer (Flow / Combine) to Room/GRDB | Local SQLite index seek initiated () |
| Step 2 | Cached feed items loaded from disk | SQLite WAL query active | Memory-mapped read seeking 20 most recent cached feed records | Initial feed timeline renders at 120 FPS ( network wait) |
| Step 3 | Network reachability confirmed | Sync Engine triggered | WorkManager / BGAppRefreshTask invokes syncFeed(since_seq_id) | HTTP/2 GraphQL delta sync query dispatched to CloudFront |
| Step 4 | Delta query routed to resolvers | CloudFront AppSync | AppSync executes Fargate resolver querying DynamoDB Delta Outbox | Outbox records filtered where server_seq_id > since_seq_id |
| Step 5 | Server returns compressed delta payload | Resolver streaming | Binary Protobuf / gzip response streamed with updated sequence cursor | Client receives 4 KB delta batch with upserts and tombstones |
| Step 6 | Atomic local SQLite ingestion | Sync Worker processing | BEGIN EXCLUSIVE TRANSACTION: upsert active posts, purge _deleted tombstones | Single transaction commits; reactive observer emits diff to UI |
| Step 7 | User taps "Like" on feed item | Feed UI active | Atomic local write: likes_count += 1, sync_status = DIRTY, enqueue outbox row | UI state transitions to liked immediately ( latency) |
| Step 8 | Background outbox drain initiated | Mutation queue draining | Worker dequeues oldest mutation, marks state = IN_FLIGHT, sends POST batch | HTTP POST /v1/feed/mutations/batch sent with idempotency token |
| Step 9 | Cloud idempotency & persistence | AppSync resolver execution | DynamoDB conditional put verifies mutation token; updates master feed row | Backend state committed; returns HTTP 200 with new server_version |
| Step 10 | Client ACK & outbox finalization | Sync Worker finalizing | Worker executes atomic DELETE FROM local_mutation_queue and marks post SYNCED | Outbox item cleared; reactive stream updates sync badge |
4. API Interface Design
1. GraphQL Delta Synchronization Query
graphqlquery SyncFeedTimeline($sinceSeqId: Long!, $limit: Int!) { syncFeed(sinceSeqId: $sinceSeqId, limit: $limit) { items { postId authorId authorName content mediaUrl thumbnailUrl likesCount commentsCount userHasLiked serverVersion serverSeqId _deleted tombstoneTimestampMs updatedAtMs } nextToken newSyncSeqId serverTimeMs } }
2. Protocol Buffers Wire Schema (feed_sync.proto)
For extreme bandwidth conservation on metered cellular networks, clients can negotiate binary Protobuf:
protobufsyntax = "proto3"; package feed.mobile.v1; message FeedPostDelta { string post_id = 1; string author_id = 2; string author_name = 3; string content = 4; string media_url = 5; string thumbnail_url = 6; int32 likes_count = 7; int32 comments_count = 8; bool user_has_liked = 9; int64 server_version = 10; bool is_deleted = 11; int64 tombstone_timestamp_ms = 12; int64 updated_at_ms = 13; int64 server_seq_id = 14; } message DeltaSyncRequest { int64 since_seq_id = 1; // server-assigned monotonic cursor, never device wall-clock int32 page_limit = 2; string pagination_token = 3; } message DeltaSyncResponse { repeated FeedPostDelta items = 1; string next_pagination_token = 2; int64 new_sync_seq_id = 3; // max server_seq_id in this batch; client stores it only after the local commit int64 server_epoch_ms = 4; // informational only (clock-skew telemetry), not a sync cursor }
3. Batch Mutation Replay Endpoint (POST /v1/feed/mutations/batch)
Replays queued offline mutations in strict chronological order with idempotency protection.
httpPOST /v1/feed/mutations/batch HTTP/1.1 Host: sync.production.aws.internal Content-Type: application/json X-Client-Sync-Epoch: 1773648000120 X-Client-Platform: android-arm64 Authorization: Bearer <jwt_access_token> { "mutations": [ { "client_mutation_id": "018e3d2a-7f12-7000-8000-123456789abc", "action_type": "LIKE", "post_id": "post_4091", "expected_version": 4, "payload": { "user_has_liked": true }, "created_at_epoch_ms": 1773648000050 }, { "client_mutation_id": "018e3d2a-7f12-7000-8000-123456789abd", "action_type": "COMMENT", "post_id": "post_4091", "expected_version": 4, "payload": { "comment_text": "Groundbreaking offline architecture!" }, "created_at_epoch_ms": 1773648000080 } ] }
Response: 200 OK
json{ "results": [ { "client_mutation_id": "018e3d2a-7f12-7000-8000-123456789abc", "status": "SUCCESS", "server_version": 5, "updated_at_epoch_ms": 1773648001010 }, { "client_mutation_id": "018e3d2a-7f12-7000-8000-123456789abd", "status": "SUCCESS", "server_version": 6, "updated_at_epoch_ms": 1773648001015 } ], "server_sync_cursor_ms": 1773648001015 }
4. Status Codes & Error Contracts
| HTTP Status | Reason Code | Error Contract Payload | Client Handling / Recovery Strategy |
|---|---|---|---|
200 OK | SYNC_SUCCESS | Standard delta response payload | Commit transaction, update sync cursor, clear processed outbox items |
400 Bad Request | INVALID_PAYLOAD | {"error": "Malformed mutation JSON payload"} | Discard mutation from queue; log to client crash telemetry |
401 Unauthorized | TOKEN_EXPIRED | {"error": "Session token invalid or revoked"} | Pause queue replay; invoke OAuth token refresh flow, then resume |
409 Conflict | VERSION_CONFLICT | {"error": "Server version 8 exceeds expected 4"} | Trigger Rollback: revert optimistic DB state, re-fetch entity via delta sync |
410 Gone | TOMBSTONED_ENTITY | {"error": "Target entity was permanently deleted"} | Purge entity from local SQLite; drop mutation from outbox |
429 Too Many Req | RATE_LIMITED | {"error": "Client sync quota exceeded", "retry_after": 5} | Enforce exponential backoff with full jitter; reschedule WorkManager task |
503 Service Unav | BACKEND_DEGRADED | {"error": "Upstream DynamoDB write throttling"} | Keep mutation in FAILED_RETRY status; retry during next sync cycle |
5. Data Models & Storage Architecture
Local SQLite Schema with WAL Mode (mobile_news_feed.db)
Configured explicitly with PRAGMA journal_mode = WAL; and PRAGMA synchronous = NORMAL; to decouple concurrent readers from writers.
sql-- Core Cached Feed Posts Table CREATE TABLE local_feed_posts ( post_id TEXT PRIMARY KEY NOT NULL, author_id TEXT NOT NULL, author_name TEXT NOT NULL, content TEXT NOT NULL, media_url TEXT, thumbnail_url TEXT, likes_count INTEGER NOT NULL DEFAULT 0, comments_count INTEGER NOT NULL DEFAULT 0, user_has_liked INTEGER NOT NULL DEFAULT 0, -- 0 = false, 1 = true server_version INTEGER NOT NULL DEFAULT 1, sync_status INTEGER NOT NULL DEFAULT 1, -- 1 = SYNCED, 2 = OPTIMISTIC_DIRTY, 3 = FAILED is_deleted INTEGER NOT NULL DEFAULT 0, tombstone_timestamp INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); -- Index optimizing instantaneous feed chronological scrolling CREATE INDEX idx_feed_timeline ON local_feed_posts(is_deleted, updated_at DESC); -- Durable Mutation Queue Table (Client Transactional Outbox) CREATE TABLE local_mutation_queue ( mutation_id TEXT PRIMARY KEY NOT NULL, -- Client-generated UUIDv7 post_id TEXT NOT NULL, action_type TEXT NOT NULL CHECK (action_type IN ('LIKE', 'UNLIKE', 'CREATE_POST', 'COMMENT', 'DELETE')), payload_json TEXT NOT NULL, expected_version INTEGER NOT NULL, state TEXT NOT NULL CHECK (state IN ('QUEUED', 'IN_FLIGHT', 'FAILED_RETRY')), retry_count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); -- Index optimizing chronological outbox drain CREATE INDEX idx_mutation_queue_drain ON local_mutation_queue(state, created_at ASC); -- Quarantine Dead-Letter Outbox Table for Poison Pill Mutations (Permanent HTTP 400/422 Failures) CREATE TABLE local_mutation_quarantine ( mutation_id TEXT PRIMARY KEY NOT NULL, post_id TEXT NOT NULL, action_type TEXT NOT NULL, payload_json TEXT NOT NULL, expected_version INTEGER NOT NULL, http_status INTEGER NOT NULL, error_code TEXT NOT NULL, error_message TEXT NOT NULL, quarantined_at INTEGER NOT NULL ); -- Sync Metadata Table (Monotonic Sequence Tracking) CREATE TABLE local_sync_metadata ( feed_key TEXT PRIMARY KEY NOT NULL, last_sync_seq_id INTEGER NOT NULL DEFAULT 0, -- Authoritative server-assigned 64-bit monotonic sequence counter last_sync_epoch_ms INTEGER NOT NULL, -- Informational server epoch (telemetry only, never used as the sync cursor) last_sync_success_at INTEGER NOT NULL ); -- SQLite Concurrency & Maintenance Configuration: -- PRAGMA journal_mode = WAL; -- PRAGMA synchronous = NORMAL; -- PRAGMA wal_autocheckpoint = 1000; -- Trigger passive checkpoint every 1,000 pages (~4MB) -- Background Idle Maintenance (Zero Active Readers): -- PRAGMA wal_checkpoint(TRUNCATE); -- Checkpoints and truncates WAL file to 0 bytes
Backend Amazon DynamoDB Single-Table Design
textTable Name: ProductionNewsFeedStore Partition Key (PK): String | Sort Key (SK): String Global Secondary Index 1 (GSI1): GSI1PK (String) | GSI1SK (String)
| Entity Type | PK | SK | GSI1PK | GSI1SK | Attributes |
|---|---|---|---|---|---|
| Post Entity | POST#<post_id> | METADATA | AUTHOR#<author_id> | CREATED#<epoch_ms> | content, media_url, likes_count, version, is_deleted |
| User Feed Item | USER#<user_id> | POST#<post_id> | TIMELINE | UPDATED#<epoch_ms> | post_id, author_name, cached_likes, server_version |
| Delta Outbox Record | USER#<user_id> | SEQ#<server_seq_id> (zero-padded, per-user counter) | ENTITY#<post_id> | SEQ#<server_seq_id> | seq_id, action_type, delta_payload, _deleted, ttl_timestamp |
| Idempotency Record | IDEMPOTENCY#<client_mutation_id> | LOCK | - | - | status, response_payload, ttl_timestamp |
Never use a single FEED_DELTA partition for the outbox. A DynamoDB partition is capped at 1,000 WCU / 3,000 RCU; at ~8,700 mutation writes/s and ~35,000 delta reads/s a global changelog partition would throttle immediately. The outbox is keyed per user (USER#<user_id>), so each user's changelog is small, naturally ordered, and its server_seq_id is a per-user counter (ADD seq :1 on a USER#<user_id>/META item inside the same transaction that writes the delta row). The number is monotonic per user, which is all the client cursor needs.
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~43%). Spend 1 Coin to unlock the remaining 6 production deep-dive sections for a full 24 hours.