Architecture
flowchart TD
Client(["Client"])
GW["API Gateway
(AWS API Gateway)"]
FS["Follow Service
(Fargate)"]
DR[("DynamoDB
— followers —
PK: followee_id")]
DU[("DynamoDB
— users —")]
Client -->|"POST / DELETE
/users/{id}/follow"| GW
GW --> FS
CA["Count Aggregator
(Lambda)"]
FS -->|"PutItem / DeleteItem"| DR
DR -.->|"DynamoDB Stream"| CA
CA -->|"UpdateItem ADD ±1"| DU
The endpoints
POST /v1/users/{id}/follow → 204
DELETE /v1/users/{id}/follow → 204
Idempotent by definition — following someone you already follow is a no-op, not a 409. Same for unfollow. This matters because retries on flaky networks are common, and a 409 on retry would force the client to special-case it.
The access pattern
The hot path on this graph is “who follows me?” — the fanout worker calls it every time you post a tweet, to push the tweet ID into each follower’s home timeline. That’s a partition-key lookup by followee_id.
The reverse direction — “who do I follow?” — only matters once we mix in pull-side reads from celebrity accounts at home-timeline read time. We defer that, and the second table that supports it, to chapter 9.
Where it lives
DynamoDB, one table:
followers (partition: followee_id, sort: follower_id)
A follow is a single PutItem; unfollow is a single DeleteItem. No transaction needed yet — there’s only one row to write.
What about Postgres?
Defensible at small scale — UNIQUE (follower_id, followee_id) enforces “you can’t follow someone twice” with one line. But the graph grows fast (billions of edges), partitioning is on you, and the access pattern is a pure key lookup with no need for joins. DynamoDB’s managed sharding wins here for the same reasons it won for tweets in chapter 3.
Counts
A user’s follower count and following count are the kind of number people refresh constantly. Don’t compute them by counting rows on every profile view — that’s a partition scan against a potentially huge partition.
Maintain them as denormalized counters, updated async off the edge writes. A Count Aggregator Lambda subscribes to the DynamoDB Stream on the followers table and applies both deltas — the event carries both follower_id and followee_id, so one stream is enough. Lambda fits here (rather than the Fargate default for backend services): each invocation is a few-millisecond UpdateItem per stream record, well inside the 15-minute / 10 GB limits, and the stream-record-trigger integration with DynamoDB Streams is built in — no long-running consumer to operate. The Follow Service itself never touches counters; its only job is the edge write.
The pattern is DynamoDB → Streams → users:
- The follow write goes to the
followerstable. Writes spread across partitions byfollowee_id. - DynamoDB Streams emits a change record for each row. The Count Aggregator consumes it and does
UpdateItemwithADD followers_count 1on the followee’susersitem, andADD following_count 1on the follower’s.ADDis atomic, so concurrent updates don’t lose increments. Unfollow is the same shape with-1.
The displayed count lags real time by a second or so — nobody notices, a follower count is not a bank balance.
The user sees their own following count update via optimistic UI — the client increments locally the moment the 204 returns. Anyone else viewing the profile sees a number that’s stale by a second or so, which is invisible.