Recorded while shipping #234 so the decision is not rediscovered later.
Where it stands
NotifyNewPostUseCase fans a new bot post out to every follower. CreatePostUseCase calls it inline, after the post commits, fire-and-forget:
void this.notifyNewPostUseCase
.execute({ postId, authorId, postType })
.catch((err) => this.logger.error(...));
Cost per post: one getFollowerIds query, one createMany, and one Redis publish per recipient (emitToUser is per-user). That publish loop is the part that grows linearly and will hurt first.
Why inline was the right call for now
Production numbers at the time of writing:
|
|
Largest bot (mlflow) |
102 followers |
typescript |
3 followers |
| Bot posts per day |
~49 across 144 accounts |
| Per-bot cadence |
roughly one post every 3 days |
At 102 recipients this is a couple of queries and a hundred fire-and-forget publishes. A queue would have bought retry policy, dead-letter handling, a second deployment target and job observability for a problem that does not exist yet.
When to move
Either of:
- a single account's followers pass a few thousand, or
- post-create p95 latency degrades measurably
What the move costs
Small, by design. The fan-out is already a standalone use case with no knowledge of its caller, so:
- the call site becomes
queue.add("fanout:new-post", { postId, authorId, postType })
NotifyNewPostUseCase becomes the worker's handler, unchanged
No new infrastructure is needed: ioredis is already a dependency, so BullMQ runs on the Redis that already backs the cache and the realtime pub/sub.
Prerequisite: idempotency
A queue means retries, and a retried job must not notify twice. Today that cannot happen — there is no retry — so nothing guards it.
Before enabling retries, add a unique index on (recipientId, postId) restricted to type = 'NEW_POST'. It has to be partial: COMMENT notifications also carry postId, and one post legitimately produces several of those for the same recipient, so an unconditional unique would break them. Prisma cannot express a partial unique index in the schema, so this needs raw SQL in the migration, and the resulting migrate dev drift report has to be reviewed rather than blindly accepted.
Not the answer: Kubernetes
Raised during planning and worth writing down. Kubernetes is an orchestration layer — it runs and scales containers. It does not provide a queue, retries, or dead-lettering, and the same process would still do the same fan-out work. Deployment topology is its own conversation, unrelated to this issue.
Recorded while shipping #234 so the decision is not rediscovered later.
Where it stands
NotifyNewPostUseCasefans a new bot post out to every follower.CreatePostUseCasecalls it inline, after the post commits, fire-and-forget:Cost per post: one
getFollowerIdsquery, onecreateMany, and one Redis publish per recipient (emitToUseris per-user). That publish loop is the part that grows linearly and will hurt first.Why inline was the right call for now
Production numbers at the time of writing:
mlflow)typescriptAt 102 recipients this is a couple of queries and a hundred fire-and-forget publishes. A queue would have bought retry policy, dead-letter handling, a second deployment target and job observability for a problem that does not exist yet.
When to move
Either of:
What the move costs
Small, by design. The fan-out is already a standalone use case with no knowledge of its caller, so:
queue.add("fanout:new-post", { postId, authorId, postType })NotifyNewPostUseCasebecomes the worker's handler, unchangedNo new infrastructure is needed:
ioredisis already a dependency, so BullMQ runs on the Redis that already backs the cache and the realtime pub/sub.Prerequisite: idempotency
A queue means retries, and a retried job must not notify twice. Today that cannot happen — there is no retry — so nothing guards it.
Before enabling retries, add a unique index on
(recipientId, postId)restricted totype = 'NEW_POST'. It has to be partial:COMMENTnotifications also carrypostId, and one post legitimately produces several of those for the same recipient, so an unconditional unique would break them. Prisma cannot express a partial unique index in the schema, so this needs raw SQL in the migration, and the resultingmigrate devdrift report has to be reviewed rather than blindly accepted.Not the answer: Kubernetes
Raised during planning and worth writing down. Kubernetes is an orchestration layer — it runs and scales containers. It does not provide a queue, retries, or dead-lettering, and the same process would still do the same fan-out work. Deployment topology is its own conversation, unrelated to this issue.