Library · Kotlin · Spring Boot · Outbox

bk-spring-outbox

Spring Boot용 Transactional Outbox. Kotlin-first, coroutine-native, autoconfigured. "DB에 쓰고 + 이벤트 발행"을 같은 트랜잭션 안에서 원자적으로 처리합니다. 모든 마이크로서비스 튜토리얼이 추천하지만 보일러플레이트가 끔찍해서 아무도 안 깔던 그 패턴, 가장 작은 형태로 정리했습니다. 김빌(Bill Kim)이 직접 설계하고 메인테이닝합니다.

한 줄 요약

같은 @Transactional 안에서 도메인 write + outbox.publish(...) 한 번. 백그라운드 코루틴 relay가 outbox_events 테이블을 드레인해서 Kafka(또는 직접 정의한 publisher)로 전달.

@RestController
class OrderController(
    private val orders: JdbcTemplate,
    private val outbox: OutboxPublisher,
) {

    @PostMapping("/orders")
    @Transactional                                       // ← same TX wraps both writes
    fun createOrder(@RequestBody req: CreateOrderRequest): OrderResponse {
        val id = "o_${UUID.randomUUID()}"
        orders.update("INSERT INTO orders (id, user_id, sku, qty) VALUES (?, ?, ?, ?)",
                      id, req.userId, req.sku, req.quantity)

        outbox.publish(                                  // ← persists in the SAME TX
            topic = "orders",
            aggregateType = "Order",
            aggregateId = id,
            eventType = "OrderCreated",
            payload = mapper.writeValueAsBytes(OrderCreated(id, req)),
        )

        return OrderResponse(id, "CREATED")
    }
}

커밋 후 백그라운드 relay가 outbox_events를 드레인해서 Kafka(또는 사용자 정의 publisher)로 전달합니다. 애플리케이션 관점에선 exactly-once, 브로커 관점에선 at-least-once.

왜 만들었나

"Dual write" 문제는 모든 마이크로서비스 팀의 입문 보스입니다. 주문을 저장하고 OrderCreated 이벤트를 발행하고 싶을 때, 둘 다 애플리케이션에서 (save()producer.send()) 하면 그중 하나는 결국 실패하고 도메인 상태와 이벤트 로그가 어긋납니다. 한 시간 안에 누군가 reconciliation 잡을 짜고 있습니다.

Transactional Outbox 패턴은 이벤트를 같은 DB 트랜잭션 안에서 로컬 테이블에 기록해 이 문제를 풀어냅니다. 백그라운드 워커(relay)가 테이블을 읽어 브로커로 forward합니다.

Kotlin/Spring 생태계에는 옵션이 있지만 대부분 자바 우선(Eventuate Tram, Debezium)이거나 heavyweight 런타임/CDC 파이프라인을 요구합니다. bk-spring-outbox는 인-프로세스 가장 작은 중간 옵션 — 주입할 OutboxPublisher, JDBC 테이블, 폴링/발행하는 코루틴 워커.

주요 기능

  • OutboxStore → 기본 JDBC. 시작 시 outbox_events 생성. H2 / PostgreSQL 자동 감지.
  • OutboxPublisher → 서비스에 주입.
  • OutboxRelay → 코루틴 기반 폴링 워커. ApplicationReadyEvent에서 시작.
  • Kafka publisher 옵션 모듈 — outbox-publisher-kafka starter 추가하면 Kafka로 forward.
  • bk-spring-saga와 짝으로 동작.

관련 자료