- Insert the new post into the
poststable. - Insert a small record into the
outboxtable that records the delivery intent (for example: post ID71, statuspending, and optional payload).
outbox table and finds rows with status = 'pending'. Typical dispatchers either poll every few seconds or use a database event/notify mechanism. A robust poller first claims rows atomically to avoid duplicate claims from multiple pollers (for example, update pending → processing with a lease or use FOR UPDATE SKIP LOCKED).
Poller responsibilities (high level):
- Claim pending outbox rows safely (avoid races).
- Send a message/ticket to the external message queue describing the work (e.g.,
{ "post_id": 71, "author_id": 123 }). - Mark the outbox row
done(or remove/archive it) only after the queue acknowledges delivery, or rely on visibility/lease semantics to enable retries.
done.

- Read the ticket (e.g., post ID
71, authorJohn). - Query the
followstable to list John’s followers (for example, 800 followers). - Push the new post into each follower’s feed/store (Redis lists, sorted sets, or a feed table).
- Acknowledge the ticket (so the queue marks the job complete).
A robust design assumes at-least-once delivery from the queue and therefore makes consumers and storage idempotent or de-duplicating. For example, enforce a unique constraint:
ZADD with the NX or check-before-insert pattern.
Why the outbox pattern?
- It gives you a transactional guarantee between your domain write (the new post) and the intent to deliver that event.
- It decouples the critical user-facing transaction from the (potentially slow) fan-out work.
- When combined with idempotent consumers and careful dispatch semantics, it yields a reliable, production-ready system for fan-out at scale.
Using the outbox pattern gives you a transactional guarantee between your domain write and the delivery intent. Pair it with idempotent consumers or storage-level deduplication to make overall delivery reliable and resilient to crashes.
- At-large scale, pushing a post synchronously to thousands of followers can be expensive; outbox + queue + workers lets you smooth that work asynchronously.
- At-least-once delivery is the practical default for many queues. Building idempotency into consumers is usually simpler and faster than trying to achieve exactly-once semantics.
- In some products, occasional duplicates that are corrected upon refresh are acceptable. In consumer-facing production systems you should still prefer idempotent processing or deduplication for a clean UX.
- Transactional Outbox Pattern — Martin Fowler
- Postgres: FOR UPDATE SKIP LOCKED
- Designing Data-Intensive Applications (book, for deeper patterns in distributed systems)