5. Following and unfollowing

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:

  1. The follow write goes to the followers table. Writes spread across partitions by followee_id.
  2. DynamoDB Streams emits a change record for each row. The Count Aggregator consumes it and does UpdateItem with ADD followers_count 1 on the followee’s users item, and ADD following_count 1 on the follower’s. ADD is 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.