Skip to content

Fan out NEW_POST notifications through a queue instead of inline #235

Description

@aquie00t

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions