{
  "id": 1423792,
  "title": "The outbox pattern is four sentences in a blog post. Here are three incidents from running it.",
  "url": "https://urgent.news/2026/08/17/the-outbox-pattern-is-four-sentences-in-a-blog-post-here-are-three",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-08-17T05:01:42.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/danzizhangdev/the-outbox-pattern-is-four-sentences-in-a-blog-post-here-are-three-incidents-from-running-it-4iek"
  },
  "original_language": "en",
  "account": "The outbox pattern consists of four steps: commit the row, poll the table, send to Kafka, and mark it as sent. This simple description is sufficient for understanding why the pattern is correct. However, the author built and ran the pattern between two services and discovered three practical issues that can cause problems in practice.\n\nPitfall 1 - The poison message that never gets counted: The outbox relay's job is to claim a batch of unsent rows, send each one, and mark it as SENT on a broker acknowledgment. The initial implementation of the loop looked reasonable, with a catch block to log a warning and break the loop if an exception occurs. However, the bug is that there is no way to track how many times a particular row has failed. If the failure is permanent, such as a broker being down or a message not serializable, the relay will retry the same row indefinitely, causing a backlog of messages. To fix this issue, the author implemented a counter to track the number of attempts and park the row as FAILED after a certain number of attempts.\n\nPitfall 2 - The trace dies at the queue: HTTP calls automatically carry a trace context, while order calls its reservation endpoint and passes the trace along. However, a polled outbox message does not carry this context. When the relay picks a row off the table, the HTTP request that originally caused the write is long finished, and its context is gone. As a result, Kafka has no way of associating the trace with the message, causing orphan spans in Jaeger that have no link back to the original checkout. The fix requires manually adding the trace ID as a column in the outbox message and reading it back in the relay to attach it as a Kafka header. This allows the consumer to restore the trace ID and tag every log line and span with the originating trace.\n\nPitfall 3 - At-least-once means you will get it twice: Kafka's delivery guarantee is at-least-once, not exactly-once. This means that a message may be delivered more than once, and the consumer must handle this possibility by making the processing idempotent. If the processing is not idempotent, the second copy of the message could cause issues such as double charging a customer's account. The author implemented a deduplication row and a side effect mechanism to ensure that processing a message twice does not cause any unintended consequences.",
  "summary": "Write the event into the same transaction as the row. Poll the table. Send to Kafka. Mark it sent. That's the whole pattern, and every version of it you'll find online stops right there, because at that level of description it's obviously correct — one commit, so a published fact can never disagree with the row that caused it. I built one, ran it for real between two services, and hit three…",
  "key_points": [],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}