Designs by Duhart · project 1 of 28
One log, one topic, three consumers. A transactional outbox because the producer is a Cloudflare Worker and Workers cannot open TCP sockets — so Kafka and CQL are permanently out of reach from the edge.
Each consumer reads every event independently and can fail, lag, and retry alone. All four processes run in one binary today because they share a database pool and a Cassandra session — not because they are coupled. Splitting them across hosts is a deployment change, not a rewrite.
A Worker cannot enlist Kafka in a Postgres transaction. Producing directly leaves a window where a post exists with no event, or an event survives a rolled-back post. Writing the event inside the post's transaction closes it.
The relay produces before stamping published_at. A crash between the two
re-delivers. That is the chosen direction: a lost event cannot be recovered, a duplicate can
be absorbed. The reverse order would lose events instead.
One shared pipeline couples every sink's availability to every other's. Three independent consumer groups off one topic means a Cassandra outage stops feeds and nothing else.
The outbox row carries ~40 denormalized display columns. None reach the event. Baking display fields in freezes them at write time — and they are wrong the moment a user changes their name.
| Consumer group | Writes to | Idempotency mechanism | What a duplicate would look like |
|---|---|---|---|
| feed-materializer | Cassandra feed_timeline |
applied_activity, 7-day TTL | The same post twice in a feed |
| notification-worker | Postgres billboard_notifications |
unique index on activity_id + ON CONFLICT DO NOTHING |
Two buzzes on someone's phone |
| fanout | realtime channel | repeated frame dropped client-side | A duplicate row appearing live |
The idempotency key is the outbox row id — stable across re-delivery by construction. There is no exactly-once claim anywhere in this design, and there should not be.
LexoRank, not integer positions — reordering one track must not rewrite every row after it.
"Liked Songs" is a row with a reserved id, not a separate table with a parallel code path.
Discriminated by target_type, not one
table per followable kind.
Push below ~10k–50k followers, pull above. The threshold is a tuning constant, not a fork in the architecture.
Directive 6 required a Kafka and a Cassandra reachable from this host — both confirmed present
first. Directive 2 was identified as a migration, not a new column, because
/v1/playlists/{id}/shuffle already rewrites an integer position holding
live data, and the standing rule is that nothing holding data gets renamed. Where a directive
would have required infrastructure that was not present, it was written up as blocked rather
than quietly substituted.