Transactional Outbox Pattern
Rozwiązanie problemu dual write — atomowo zapisz event z danymi, opublikuj na Kafka przez Debezium CDC, zapewnij exactly-once przez Inbox Pattern.
6 wzorców zapewnienia spójności danych
Od prostego Outbox po Event Sourcing i Kafka Transactions — każde podejście oferuje inny trade-off między złożonością a gwarancjami.
| Wzorzec | Gwarancja | Latencja | Overhead | Kiedy |
|---|---|---|---|---|
| Transactional Outbox | At-least-once publish | Sub-second (CDC) / Sekundy (polling) | Dodatkowa tabela + relay | Mikrousługi z DB + message broker |
| Inbox Pattern | Exactly-once consume | Brak dodatkowej | Dodatkowa tabela | At-least-once broker, krytyczne operacje |
| Debezium CDC | At-least-once capture | Sub-second (WAL) | Kafka Connect cluster | PostgreSQL/MySQL -> Kafka pipelines |
| Event Sourcing | Exactly-once (wbudowane) | Sub-second | Zmiana architektury | Nowe systemy, audit log wymagany |
| Kafka Transactions | Exactly-once end-to-end | Wyższa (2-phase) | Złożoność + niższy throughput | Kafka Streams, krytyczne pipelines |
| Temporal Workflows | Durable execution | Zależy od workflow | Temporal Server | Long-running, multi-step workflows |
Często zadawane pytania
Co to jest Transactional Outbox Pattern i jaki problem rozwiązuje?
Problem dual write: aplikacja musi zapisać dane do DB i wysłać event do message broker (Kafka, RabbitMQ). Jeśli zapis do DB powiedzie się ale broker jest niedostępny — event zgubiony. Jeśli broker dostępny ale DB fail — niespójność. Rozwiązanie — Transactional Outbox: 1. Zamiast wysyłać event bezpośrednio — zapisz event do tabeli 'outbox' w tej samej transakcji co główne dane. 2. Osobny Message Relay czyta z outbox i publikuje do brokera. 3. Po potwierdzeniu przez broker — oznacz event jako published. Gwarancja: event jest zapisany atomowo z główną zmianą. Nie ma dual write. At-least-once delivery (event może być opublikowany wielokrotnie — wymaga idempotentnych konsumentów). Outbox table schema: id, aggregate_type, aggregate_id, event_type, payload, created_at, published_at. Message Relay implementacje: Polling Publisher: prosty, odpytuje outbox co N sekund. Wada: latencja i obciążenie DB. Change Data Capture (CDC): efektywniejsze, używa log DB do wykrywania zmian. Debezium: open-source CDC connector dla PostgreSQL, MySQL, MongoDB, SQL Server. Czyta transaction log (WAL w PostgreSQL) — zero overhead na aplikacji.
Debezium CDC — jak działa Change Data Capture?
Debezium: open-source platforma CDC (Confluent/Red Hat). Działa jako Kafka Connect connector. Czyta Write-Ahead Log (WAL) PostgreSQL, binlog MySQL, oplog MongoDB. Żadnego odpytywania — event-driven, sub-second latencja. Architektura: Debezium Connector (Kafka Connect plugin) -> DB transaction log -> Kafka topic. Konfiguracja PostgreSQL: wal_level = logical. Replikacja slot dla Debezium. Konfiguracja connectora: database.hostname, port, user, password. database.server.name — prefix dla topic names. table.include.list — które tabele śledzić. Kafka topics: {server-name}.{schema}.{table} per tabela. Każda zmiana (INSERT/UPDATE/DELETE) jako event. Format eventu: before (stare dane) i after (nowe dane). Outbox Event Router (Debezium SMT): Single Message Transform który routuje eventy z outbox tabeli. Routing na podstawie aggregate_type i event_type. Envelope: after.event_type -> Kafka topic name. Zalety CDC vs Polling: Zero overhead na aplikacji (czyta log). Real-time (sub-second latencja). Łapie wszystkie zmiany (nawet bezpośrednie SQL). Wady CDC: złożoność setup. Zmiany schematu DB mogą zepsuć CDC. Wymaga PostgreSQL logical replication. Zarządzanie replication slots (nie mogą rosnąć bez końca).
Inbox Pattern — jak zapewnić exactly-once processing?
Inbox Pattern: odpowiednik Outbox dla consumerów. Problem: consumer dostaje wiadomość z brokera, przetwarza ją i zapisuje do DB. Jeśli aplikacja crashuje po zapisie DB ale przed commit offset — wiadomość zostanie przetworzona ponownie (at-least-once). Rozwiązanie: Inbox table — przed przetworzeniem zapisz wiadomość do inbox tabeli (w tej samej transakcji). Sprawdź czy message_id już w inbox — jeśli tak, pomiń (already processed). Inbox table schema: message_id (PK), topic, partition, offset, payload, processed_at. Gwarantuje exactly-once processing (idempotent consumer przez DB check). Wada: dodatkowy DB write per message, wzrost tabeli. Cleanup: usuń stare inbox entries (soft delete lub TTL). Idempotency Key: alternatywa do inbox — consumer generuje idempotency key (np. message_id) i sprawdza w Redis lub DB przed przetworzeniem. Pattern Transactional Outbox + Inbox razem: Producer używa Outbox (atomowy zapis + publikacja). Consumer używa Inbox (exactly-once processing). Razem = end-to-end exactly-once w distributed systemie (przy at-least-once brokerze). Kafka z transakcjami: Kafka producers z enable.idempotence = true. Kafka transactions (beginTransaction, commitTransaction). Exactly-once streams z Kafka Streams. Ale: droższe, niższy throughput, złożoniejsze.
Jak zaimplementować Outbox Pattern w Springu?
Spring Boot Outbox implementacja: Entity: Order (główna encja). OutboxEvent (tabela outbox). OrderService: @Transactional. 1. save(order). 2. outboxRepository.save(new OutboxEvent(order)). Jeden commit = oba zapisy atomowo. Polling Publisher (prosty): @Scheduled(fixedDelay = 1000). Pobierz niepublikowane eventy. Publish na Kafka. Oznacz jako published. Problem: race condition przy wielu instancjach. Rozwiązanie: SELECT FOR UPDATE SKIP LOCKED — pobierz i zablokuj. Jednocześnie tylko jedna instancja przetwarza. Spring Modulith Outbox: Spring Modulith 1.1+ ma wbudowane Event Publication Registry. @ApplicationModuleListener — automatycznie przechowuje eventy. Automatyczny retry. Transactional Domain Events. Debezium + Spring: nie wymaga Polling Publisher. Debezium czyta z WAL. Dodaj connector konfigurację do Kafka Connect. Outbox Event Router SMT automatycznie routuje. Axon Framework: Event Sourcing + CQRS + Saga + Outbox wbudowane. Axon Server jako event store i message broker. Produkcyjne przykłady: Eventuate Tram (Chris Richardson) — biblioteka do Saga + Outbox dla Java/Spring. Outboxer (Python). Transactional Outbox dla Go. Inbox/Outbox dla .NET (MassTransit).
Jakie są alternatywy dla Outbox Pattern?
Alternatywa 1 — Event Sourcing zamiast Outbox: zamiast osobnej outbox tabeli — aplikacja zapisuje eventy jako primary store. Event Store = outbox. Eventy czytane przez projektory i publishowane do brokera. EventStoreDB, Axon Server, Marten (PostgreSQL as event store). Wada: zmiana paradigmatu — duże refaktoryzacje istniejących aplikacji. Alternatywa 2 — Idempotent Producer: accept dual write. Przy failurze — retry z tym samym idempotency key. Kafka enable.idempotence = true — broker odrzuca duplikaty per producer session. Wada: nie gwarantuje spójności przy crash przed wysłaniem. Alternatywa 3 — Saga Orchestration z Temporal: Temporal persystuje stan workflow. Retry automatyczny przy failurze. Brak potrzeby osobnego outbox — Temporal zarządza durable execution. Wada: nowy komponent (Temporal Server), zmiana architektury. Alternatywa 4 — Kafka jako transaction log: traktuj Kafka jako source of truth. Aplikacja zapisuje tylko do Kafka (log-centric architecture). Brak DB jako primary store. Wada: trudne zapytania ad-hoc, streaming-first architecture. Wybór: prostość + istniejąca DB -> Outbox + Debezium. Event-driven new system -> Event Sourcing. Złożone long-running workflows -> Temporal. Kafka-native architecture -> Log-centric.
Powiązane artykuły
Skontaktuj się z nami
Porozmawiajmy o Twoim projekcie. Bezpłatna wycena w ciągu 24 godzin.
Wyślij zapytanie
Telefon
+48 790 814 814
Pon-Pt: 9:00 - 18:00
adam@fotz.pl
Odpowiadamy w ciągu 24h
Adres
Plac Wolności 16
61-739 Poznań
Godziny pracy
Wolisz porozmawiać?
Zadzwoń teraz i porozmawiaj z naszym specjalistą o Twoim projekcie.
Zadzwoń teraz