Concert Booking / Engineering case

commit 이후의 실패를 사라진 사건으로 만들지 않기

Outbox는 발행 실패를 DB에 남기고, DLT는 처리 실패를 정상 흐름에서 격리해 다시 시작할 위치를 제공합니다.

GitHub에서 코드 보기 ↗
User impact

예약 취소가 완료됐다는 응답 뒤에도 좌석 반환 이벤트가 사라지면 다른 사용자는 계속 좌석을 선택할 수 없습니다.

01

어떻게 깨지는가

  • DB commit 직후 process가 종료되면 Kafka 발행 여부를 알 수 없습니다.
  • poison message를 무한 재시도하면 정상 이벤트도 함께 막힙니다.

02

지켜야 할 경계

  • 도메인 변경과 이벤트 기록 사이에 틈이 없어야 합니다.
  • 중복 replay는 허용하되 좌석 재고가 두 번 늘면 안 됩니다.
Outbox와 DLT가 남기는 두 복구 지점발행 실패와 처리 실패를 다른 상태와 queue에 남깁니다.same commitclaimpublishconsumefailreplayDomain transactionreservation + outboxOutboxPENDING / FAILEDRelaybackoff + DEADKafkaoriginal topicConsumeridempotent effectDLTfailure isolation
  1. 도메인 변경과 Outbox event를 같은 transaction에 기록합니다.
  2. relay가 발행 실패를 상태와 retry 시각으로 남깁니다.
  3. consumer 실패는 DLT로 격리한 뒤 원인을 확인하고 replay합니다.
발행 실패와 처리 실패를 다른 상태와 queue에 남깁니다.
Outbox와 DLT가 남기는 두 복구 지점
Outbox와 DLT가 남기는 두 복구 지점발행 실패와 처리 실패를 다른 상태와 queue에 남깁니다.same commitclaimpublishconsumefailreplayDomain transactionreservation + outboxOutboxPENDING / FAILEDRelaybackoff + DEADKafkaoriginal topicConsumeridempotent effectDLTfailure isolation

03

선택한 설계

  • 도메인 변경과 Outbox insert를 같은 transaction에 묶습니다.
  • relay가 실패 횟수와 다음 시각을 기록하고 terminal DEAD 상태를 구분합니다.
  • consumer 실패는 DLT에 격리하고 원인을 확인한 뒤 replay합니다.

04

감수한 trade-off

  • 즉시 일관성 대신 eventual release를 받아들이고 UI에 반영 중 상태를 표시합니다.
  • 운영 상태가 늘지만 실패를 관찰하고 수동 복구할 수 있습니다.

05

어떻게 확인했는가

  • 강제 Kafka 실패 뒤 FAILED로 남고 다음 retry에서 PUBLISHED가 됩니다.
  • DLT replay를 두 번 실행해도 좌석 반환은 한 번만 반영됩니다.
  • 브라우저 E2E에서 취소와 만료 뒤 좌석이 AVAILABLE로 돌아옵니다.

Replay the boundary

글 대신 단계별로 따라보기

Flow 재생

Sources

코드와 테스트로 돌아가기

  • Outbox 실패·재시도 상태 전이

    공개 저장소에서 확인
    근거 ↗
  • DLT replay 멱등 복구 테스트

    공개 저장소에서 확인
    근거 ↗
  • 취소·만료 뒤 좌석 반환 브라우저 E2E

    공개 저장소에서 확인
    근거 ↗
한계와 다음 확인
  • exactly-once를 주장하지 않고 중복을 흡수할 상태와 키를 제공합니다.
  • 장시간 broker 장애의 운영 복구 시간을 측정하지 않았습니다.