Designing a Photo-Sharing App (Instagram)
A photo-sharing service has two connected jobs: publish an author's image and help other people discover it. The image bytes, post metadata, and follower feed have different storage and delivery needs.
The classic Instagram system design interview covers uploading photos, storing relationships (following/followers), and generating the News Feed.
Storage Segregation
We separate our storage layers based on the type of data:
- Binary Data (Photos/Videos): Stored in Object Storage (Amazon S3). Served globally via a Content Delivery Network (CDN) to reduce latency for end-users.
- User Metadata and Social Graph: Relationships like "User A follows User B" need indexes for the actual access paths, such as followers and followees. Instagram historically used sharded PostgreSQL; graph or wide-column stores are alternatives only when their query, consistency, and operational tradeoffs fit the workload.
- Post Metadata: Pointers to the S3 objects, timestamps, captions, and view counts are stored in a relational database or NoSQL store.
Generating the News Feed
When you open the app, you see a chronological (or algorithmically sorted) list of photos from people you follow. Generating this on the fly (pull model) by querying the database for "all photos by people I follow, sorted by time" is incredibly slow if you follow 1,000 people.
Instead, we use a Push Model (Fan-out on Write):
For an ordinary author, the Fanout Service can asynchronously push the post ID into followers' bounded feed caches. Reading one page is then a bounded range read plus batched hydration—not an O(1) operation independent of page size—and cache misses still need a durable fallback.
The Celebrity Problem
The push model breaks down for celebrities with 200 million followers. Pushing one photo to 200 million Redis lists takes too long and wastes huge amounts of memory, especially for inactive users.
A hybrid model can fan out ordinary authors and merge selected high-fanout authors at read time. The threshold is workload-dependent and moves cost rather than balancing it perfectly: reads now need bounded multi-source merge, ranking, pagination stability, and a policy for stale or missing cached entries.
Worked example: back-of-the-envelope numbers
- Uploads: 100 million photos per day is about 1,160 per second on average, perhaps 3,500 at peak.
- Storage: a 2 MB original plus 4 variants (1080, 640, 320 and a thumbnail) of about 600 KB in total, so about 2.6 MB per photo and about 260 TB per day. That is about 95 PB per year before replication or erasure coding. Old originals can move to colder storage tiers.
- Reads: 500 million daily users each viewing about 100 images is 50 billion image fetches per day, about 580,000 per second. The CDN must absorb 95% or more of this.
- Metadata: about 1 KB per post, or 100 GB per day, easily handled by sharded Postgres. Instagram sharded Postgres into thousands of logical shards mapped onto fewer physical servers.
Storage by data type
| Data | Store | Access pattern | Why |
|---|---|---|---|
| Image bytes | Object storage + CDN | Write once, read many, cache forever (immutable URLs) | Cheap, durable, cacheable at the edge |
| Posts, users | Sharded Postgres (by user_id) | Point lookups, recent posts by author | Transactions, secondary indexes |
| Follow graph | Sharded SQL or a graph store (Meta TAO) | Followers of X, followees of Y | Two indexes, one per direction |
| Feed timelines | Redis or Cassandra lists of post IDs | Range read of the top N | Precomputed for fast opens |
| Likes and comments | Wide-column store + counters | Write-heavy, counts shown on every post | Avoids hot rows |
Upload flow
Instagram's early engineering blog described generating IDs inside Postgres: 41 bits of milliseconds since a custom epoch, 13 bits of logical shard ID and 10 bits of per-shard sequence. The IDs sort by time, and the owning shard can be read directly from the ID. This is a useful detail to mention in an interview when explaining how IDs are generated.