Engineering deep diveHow exactly-once works at this scale
For engineers: the stack, the dispatch path, the decisions behind it, and the code shape of the core mechanism.
Stack
- Service
- NestJSTypeScriptgRPC
- Orchestration
- Temporalparent / child workflows
- Messaging
- Apache PulsarShared subscription
- State
- MongoDBRedis
- Quality
- Jestdependency-cruiser
One reward, from campaign to wallet
Every step can be retried or run twice by another pod. Each one is written so a repeat is harmless.
- TemporalThe parent workflow runs targeting and creates one PENDING delivery row per recipient (idempotent).
- TemporalA child dispatcher workflow, keyed by campaign and epoch, pages through PENDING rows in chunks of 500.
- MongoDBEach row is claimed with a compare-and-swap PENDING to IN_PROGRESS. Only the winner continues (mark first, send second).
- RedisThe budget pool is debited atomically. If the daily cap would be exceeded, the row is held and operations are alerted.
- PulsarThe winner publishes {deliveryId, epoch}. A rate limiter releases messages gradually so downstream services are not flooded.
- ConsumerRe-reads the row (the row is the truth, not the message) and rejects anything stamped with an old epoch.
- MongoDBA second CAS IN_PROGRESS to SENDING. A pod that loses the race acknowledges and skips.
- ChannelThe reward is delivered and the row is marked SUCCESS or FAILED.
- ReconcilerA cron pass finds stuck rows after pod crashes, requeues only what is missing, and closes the campaign.
Delivery state machine
Pending→Claimed→Sending→Success/Failed
Every transition is a conditional update, so two pods can never both move the same row forward.
Key decisions
Epoch fencing instead of cancelling tasks
- Problem
- A restart or reschedule left up to 100K in-flight tasks from the old run. Cancelling them one by one was slow and expensive.
- Choice
- Each run gets an epoch number, stored in Redis and stamped on every message. Restarting bumps it. Old messages see a newer epoch and skip themselves.
- Why
- Restarts became instant. Old and new runs can briefly overlap, and correctness still holds.
Temporal + Pulsar hybrid
- Problem
- All orchestration lived in Pulsar consumers, so recovery depended on hidden consumer state.
- Choice
- Temporal owns orchestration, retries and recovery as explicit workflow code. Pulsar keeps the high-throughput fan-out.
- Why
- Each tool does what it is best at. Migrated in phases with zero downtime.
A daily budget pool above every campaign
- Problem
- 5 to 10 campaigns run at once, each targeting different players. Together they could overspend.
- Choice
- One Redis pool per day (for example RM 1M) debited with atomic INCRBYFLOAT, guarded by a single-flight lock and TTL, in UTC+8. Near the cap, operations are notified to top up or pause.
- Why
- Overspend is impossible even across 1.5M deductions running in parallel.
The core check, simplified
delivery.consumer.tssimplified sketch
async handle(msg: { deliveryId; epoch }) {
// the row is the source of truth, not the message
const row = await deliveries.findById(msg.deliveryId);
if (row?.status !== 'IN_PROGRESS') return; // ack and skip
// epoch fence: messages from an older run never touch the row
if (msg.epoch < await campaigns.currentEpoch(row.campaignId)) return;
// CAS: only one pod can move IN_PROGRESS -> SENDING
const won = await deliveries.updateOne(
{ _id: row._id, status: 'IN_PROGRESS' },
{ $set: { status: 'SENDING' } },
);
if (won.modifiedCount !== 1) return; // lost the race
const ok = await channel.deliver(row);
await deliveries.markTerminal(row._id, ok ? 'SUCCESS' : 'FAILED');
}
An open demo with the same architecture (business logic removed): nestjs-temporal-pulsar-demo ↗
Scale
1.5Mdispatches a day at peak
5-10campaigns running at once
6worker pods
1K-5Kmessages per second, per pod
0duplicate payouts
Reliability and operations
- Rate-limited releaseLarge campaigns are drained gradually instead of all at once.
- Single-flight schedulerLocks, claim-based CAS and unique indexes stop a job running twice across pods.
- Never drop silentlyUnknown errors are not acknowledged, so Pulsar redelivers them.
- Layering in CIAn L0 to L3 dependency standard is checked on every change by dependency-cruiser.