Dead-Letter Queue: bound retries, don't loop forever

dead-letter-queue
Mechanism lab

Dead-Letter Queue: bound retries, don't loop forever

A failing ("poison") message sits at the head of an in-order queue. The broker retries it on every tick but never gives up and never sets it aside, so it loops forever and blocks the good message behind it. Fix it with bounded retries plus a dead-letter queue.

You'll predict the failure first, then write the fix and watch your own code drive the behavior.

Understand it step by step (한국어로 이해 → 영어로 말하기)

맨 앞 메시지 하나가 계속 처리에 실패한다. 그 하나를 무한 재시도하면 뒤에 있는 멀쩡한 메시지들이 영영 못 나간다. 몇 번 해보고 안 되면 옆에 따로 빼두자.
  1. 1

    메시지가 줄(큐)을 서 있다. 처리기는 항상 맨 앞 하나만 꺼내서 처리하고, 그게 끝나야 다음으로 넘어간다 (순서대로).

    String head = queue.peekFirst();
    if (!willFail) {
        queue.pollFirst();
        delivered.add(head);
        return true;
    }
    🗣 영어로 말해

    The broker processes the queue in order, one head message at a time.

    checking microphone…

  2. 2

    그런데 맨 앞 메시지가 '독'이다. 무슨 짓을 해도 절대 성공 못 한다. (예: 데이터가 깨져 있어서 영원히 실패)

    ⚖️ Trade-off: 이 하나는 고칠 방법이 없다. 재시도한다고 갑자기 성공하지 않는다.

    // poison: willFail is always true for this message,
    // so the !willFail branch never runs for it
    🗣 영어로 말해

    But the head message is poison — it can never succeed, no matter how many times we try.

    checking microphone…

  3. 3

    버그 있는 코드는 매 틱마다 이 맨 앞 메시지를 계속 다시 시도한다. 포기도 안 하고, 옆으로 치우지도 않는다.

    ⚖️ Trade-off: 맨 앞이 안 빠지니까 뒤에 있는 멀쩡한 메시지는 차례가 영영 안 온다 (줄 전체가 막힘).

    🔧 도구:in-order queue (FIFO)head-of-line blocking

    // head-of-line blocking: the failing head is never removed,
    // so "good" behind it never reaches the front
    String head = queue.peekFirst(); // still "poison" every tick
    🗣 영어로 말해

    The broker retries that head forever, so the good message behind it never gets its turn.

    checking microphone…

  4. 4

    해결 1단계: 시도 횟수를 센다. 한 메시지가 maxRetries번 실패하면 '이건 안 되는 거다'라고 판단하고 그만한다.

    ✅ Fix: 무한히 매달리지 않고, 정해진 횟수만큼만 해보고 손을 뗀다.

    🔧 도구:retry counter / maxRetriesattempt cap

    int n = attempts.merge(head, 1, Integer::sum);
    if (n >= maxRetries) {
        // stop retrying this message
    }
    🗣 영어로 말해

    I cap the attempts — after maxRetries failures, I stop trying that message.

    checking microphone…

  5. 5

    해결 2단계: 포기한 그 메시지를 맨 앞에서 빼내(poll) 따로 만든 '죽은 편지함(DLQ)'에 넣는다. 그러면 뒤에 있던 멀쩡한 메시지가 드디어 처리된다.

    ✅ Fix: 독 메시지를 옆 통에 격리 → 줄이 다시 흐른다.

    🔧 도구:dead-letter queue (DLQ)queue.poll()Kafka/SQS DLQ

    if (attempts.get(head) >= maxRetries) {
        queue.pollFirst();
        dlq.add(head);
        return true; // head advances, queue flows again
    }
    🗣 영어로 말해

    I move the failed message off the head into a dead-letter queue, so the rest can flow.

    checking microphone…

  6. 6

    대가: DLQ에 들어간 메시지는 자동으로 처리되지 않는다. 사람이 직접 보거나, 따로 만든 소비자가 다시 꺼내 봐야 한다.

    ⚖️ Trade-off: '무조건 배달'을 포기하는 셈이다. 독 메시지 하나는 결국 배달 안 된 채 남는다.

    ✅ Fix: 대신 줄 전체는 살아 움직인다. 멈춘 하나 때문에 전부 죽는 것보다, 하나를 격리하고 나머지를 살리는 게 낫다 (전체 liveness > 한 건의 보장).

    🔧 도구:DLQ consumermanual replay / reprocessing

    // DLQ is not auto-processed: a separate consumer replays it
    for (String msg : broker.dlq) {
        inspectAndReplay(msg);
    }
    🗣 영어로 말해

    The trade-off is those dead-lettered messages need a human or a separate consumer to inspect and replay them.

    checking microphone…

6단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.

Explain it in English

Say it out loud in your own words — hit these terms — then check against the gold answer.

Say it in your own words — but hit these: dead letter queue · retries · message queue · backpressure · java

checking microphone…