Backend & Node.js · APIs, data and failure
How do you build an idempotent message consumer?
Short answer
Assign every message a stable id, record processed ids in the same transaction as the side effect, and skip duplicates. Make the side effect itself idempotent or use an inbox table so retries and replays cannot double-apply.
With at-least-once delivery, a consumer can see the same message twice: the broker redelivers after a timeout, a rebalance moves a partition, or an operator replays a batch. Idempotency is not optional; it is part of the consumer contract.
BEGIN;
INSERT INTO processed_messages (message_id)
VALUES ('evt_01J8Z3')
ON CONFLICT (message_id) DO NOTHING;
UPDATE orders
SET status = 'paid'
WHERE id = 1042 AND status = 'pending';
COMMIT;Patterns that work
- Use a producer-generated message id, not a random value created on each attempt
- Store processed ids in the same database and transaction as the side effect
- Make external calls idempotent with an idempotency key
- Use conditional updates or unique constraints instead of blind increments
- Retain dedupe records long enough to cover the broker retention and replay window
- Handle ordering explicitly; a later event must not overwrite newer state
A separate dedupe cache is weaker than a transactional inbox because the cache write and the database commit can diverge. If you must use one, accept a small duplicate window and design the side effect to tolerate it. For payments and inventory, prefer the transactional approach.
Follow-ups you should expect
Interviewers use these to check whether the first answer was understanding or memorisation.
- 1Why is a dedupe cache alone insufficient?
- 2How long should processed message ids be retained?
- 3How do you handle out-of-order events?
- 4What is the difference between idempotent and exactly-once?
Say it in the interview
I give each message a stable producer-generated id and record it in a processed-messages table in the same transaction as the business change. A duplicate hits the unique constraint and the side effect is skipped. For external calls I pass an idempotency key, and for state changes I use conditional updates. A cache-only dedupe can diverge from the database, so I prefer a transactional inbox for critical work.
Use this as a model, not a script. Saying it in your own words is what makes it survive follow-up pressure.
Related questions
Reading is step one
Now say it out loud.
Understanding an answer and delivering it under pressure are different skills. Rehearse this topic in a live Backend & Node.js mock interview and get scored feedback on depth, clarity and confidence.