Summary
The current follower count uses a simple integer increment/decrement with no concurrency protection. Under a burst of concurrent follow and unfollow operations for the same creator, lost updates cause the stored count to diverge from the true number of followers. This issue replaces the counter with a G-Counter CRDT per node — each server instance maintains its own increment and decrement registers, and the true count is computed as the sum of all increments minus the sum of all decrements across all nodes at read time.
Scope
1. CRDT counter storage
- Add a
FollowerCounterShard table: creatorWallet, nodeId, increments (BigInt), decrements (BigInt), updatedAt
nodeId is a stable identifier for the server instance (read from NODE_ID env var or generated on startup and persisted)
- Each node only ever increments its own shard — no cross-node writes
2. Follow and unfollow operations
- On follow: atomically increment the
increments column of the local node's shard using a database-level UPDATE ... SET increments = increments + 1 (no read-modify-write)
- On unfollow: atomically increment the
decrements column of the local node's shard
- Both operations must be idempotent: store a
FollowEvent record per (followerWallet, creatorWallet) and skip the shard update if the event already exists in the expected direction
3. Count resolution
getFollowerCount(creatorWallet): sum all increments across all shards, subtract sum of all decrements, clamp to 0
- Count resolution must read from the primary database replica (not a read replica) to avoid stale shard reads
- Cache the resolved count in Redis with a 10-second TTL; invalidate on any local shard write
4. Shard compaction
- A nightly compaction job merges all shards for a creator into a single canonical shard, resetting the per-node shards to 0 after recording the compacted total
- Compaction must be atomic: write the canonical shard and delete per-node shards in a single transaction
- Compaction skips any creator whose shards were written to in the last 5 minutes to avoid racing with active operations
5. Integration tests
- Simulate 100 concurrent follows from different wallets — assert the final count equals 100 with no lost updates
- Simulate 50 concurrent follows and 30 concurrent unfollows — assert the final count equals 20
- Double-follow from the same wallet — assert count increments only once
- Run compaction — assert the resolved count before and after compaction is identical
- Simulate two nodes each recording follows — assert count resolution sums across both shards
Acceptance Criteria
ETA: 24 hours
Coordinate on Telegram
Summary
The current follower count uses a simple integer increment/decrement with no concurrency protection. Under a burst of concurrent follow and unfollow operations for the same creator, lost updates cause the stored count to diverge from the true number of followers. This issue replaces the counter with a G-Counter CRDT per node — each server instance maintains its own increment and decrement registers, and the true count is computed as the sum of all increments minus the sum of all decrements across all nodes at read time.
Scope
1. CRDT counter storage
FollowerCounterShardtable:creatorWallet,nodeId,increments(BigInt),decrements(BigInt),updatedAtnodeIdis a stable identifier for the server instance (read fromNODE_IDenv var or generated on startup and persisted)2. Follow and unfollow operations
incrementscolumn of the local node's shard using a database-levelUPDATE ... SET increments = increments + 1(no read-modify-write)decrementscolumn of the local node's shardFollowEventrecord per (followerWallet, creatorWallet) and skip the shard update if the event already exists in the expected direction3. Count resolution
getFollowerCount(creatorWallet): sum allincrementsacross all shards, subtract sum of alldecrements, clamp to 04. Shard compaction
5. Integration tests
Acceptance Criteria
ETA: 24 hours
Coordinate on Telegram