Przejdź do treści
Architektura 8 min czytania

Event-driven architecture w sklepie B2B - co, kiedy, jak

Event-driven architecture (EDA) jest jednym z najbardziej nadużywanych buzzwordów ostatnich pięciu lat. Każdy konsultant chce go wdrażać, każdy CTO chce go słyszeć w propozycji, a kiedy patrzysz na realny sklep B2B, okazuje się, że jedna trzecia czasu wdrożenia poszła na infrastrukturę event busu, której nikt nie używa do niczego, czego nie zrobiłby zwykły webhook. Moja realna obserwacja jest taka: integracja sklepu z ERP, OMS, WMS, hurtowniami przez prawdziwy event bus daje wartość konkretną i mierzalną - decoupling, skalowanie, odporność na awarie. Wewnętrzna komunikacja między modułami monolitu przez eventy jest najczęściej zbyteczną komplikacją, którą wdrożyłeś, bo było modne. Ten artykuł pokazuje, gdzie EDA się opłaca, gdzie to czysty overhead i jak nie wpaść w pułapkę "event everything".

Jakub Owsianka Autor
Zaktualizowano:
Okladka artykulu: Event-driven architecture w sklepie B2B - co, kiedy, jak
Okladka artykulu: Event-driven architecture w sklepie B2B - co, kiedy, jak
Spis treści (6)

Co to event-driven w kontekście sklepu

Event-driven w skrócie oznacza, że systemy komunikują się przez emitowanie i nasłuchiwanie zdarzeń, zamiast wprost wywoływać siebie nawzajem przez API. To brzmi abstrakcyjnie, więc lepiej pokazać na konkretnym scenariuszu: złożenie zamówienia w sklepie B2B z integracjami ERP, WMS, OMS i mailingiem.

W klasycznym, synchronicznym podejściu wygląda to tak:

Klient klika "Złóż zamówienie"
  → Sklep tworzy zamówienie w bazie
  → Sklep wywołuje ERP: POST /orders
  → Sklep wywołuje WMS: POST /pick-tasks
  → Sklep wywołuje OMS: POST /allocate
  → Sklep wywołuje EmailService: send confirmation
  → Sklep odpowiada klientowi: Sukces

Czas trwania to suma wszystkich requestów. Jeśli WMS odpowiada w 2 sekundy, ERP w 4, OMS w 1, mailing w 0.5, klient czeka 7-8 sekund na odpowiedź. Jeden serwis padnięty oznacza, że cały checkout się wywala, choć technicznie zamówienie zostało już zapisane w sklepie.

W podejściu event-driven schemat zmienia się fundamentalnie:

Klient klika "Złóż zamówienie"
  → Sklep tworzy zamówienie w bazie
  → Sklep emituje event "OrderCreated"
  → Sklep odpowiada klientowi: Sukces (poniżej 200 ms)

Niezależnie i równolegle:
  ERP konsumuje "OrderCreated" → tworzy zamówienie u siebie
  WMS konsumuje "OrderCreated" → generuje pick task
  OMS konsumuje "OrderCreated" → allocation magazynowa
  EmailService konsumuje "OrderCreated" → wysyła potwierdzenie

Czas trwania z perspektywy klienta to teraz tylko zapis do bazy i emisja eventu - milisekundy. Reszta dzieje się "eventually consistent" - czyli prędzej czy później, ale klient nie musi czekać.

Korzyści są namacalne. Decoupling: sklep nie wie nic o WMS, OMS i EmailService, dokłada się nowy konsument bez modyfikacji sklepu. Skalowanie: każdy konsument można skalować niezależnie. Resilience: awaria WMS na godzinę nie blokuje sklepu, eventy czekają w kolejce i są przetworzone, gdy WMS wróci.

Koszty też trzeba uczciwie pokazać. Eventually consistent oznacza, że klient widzi "Sukces", ale zamówienie w ERP może realnie nie istnieć przez kilkanaście sekund. Dla większości scenariuszy B2B to akceptowalne, ale handlowiec, który dzwoni do klienta od razu po złożeniu, może zobaczyć "nie ma takiego zamówienia". Debugowanie staje się trudniejsze - flow przez pięć serwisów wymaga distributed tracingu. Infrastruktura urośnie o event bus, monitoring, dead letter queues, sumarycznie 100-300 EUR miesięcznie plus część etatu DevOpsa.

Monolit vs microservices - EDA ma sens głównie tam, gdzie masz monolit z integracjami zewnętrznymi albo prawdziwe microservices. Dla pojedynczego monolitu bez integracji to overengineering.

Domain events vs integration events

To są dwa fundamentalne rodzaje eventów, których nie wolno mylić - z tego wynika połowa katastrofalnych wdrożeń EDA, jakie widziałem.

Domain events żyją wewnątrz jednego bounded contextu (jednego serwisu). Reprezentują zmianę stanu domeny - "OrderPlaced" wewnątrz modułu Order, "ProductStockUpdated" wewnątrz katalogu. Mogą być synchroniczne lub asynchroniczne, ale działają w obrębie jednej aplikacji. W Magento klasyka to event/observer pattern:

$this->eventManager->dispatch('sales_order_place_after', ['order' => $order]);

Observers w innych modułach Magento słuchają i reagują - to też jest event-driven, choć w pamięci jednego procesu.

Integration events działają między różnymi bounded contextami (różnymi serwisami albo aplikacjami). Są zawsze asynchroniczne, przez message broker, mają ustabilizowany format jako kontrakt między serwisami. Przykład - "OrderCreated" wystawiany przez sklep, konsumowany przez ERP:

{
  "event_type": "order.created",
  "event_id": "evt_abc123",
  "occurred_at": "2026-06-21T10:30:45Z",
  "version": "1.0",
  "data": {
    "order_id": "ORD-2026-12345",
    "customer_id": "CUST-998",
    "items": [...],
    "total": 1234.56
  }
}

Pułapka, którą widzę często: zespół używa tego samego eventu jako domain i integration. Wewnętrzny sales_order_place_after w Magento jest dosłownie publikowany do RabbitMQ jako integration event. Efekt: każda zmiana wewnętrznej struktury Magento łamie kontrakt z ERP-em, ERP musi reagować na zmiany, których nie powinien widzieć. Lepsze rozwiązanie to domain events wewnątrz, plus osobna warstwa, która tłumaczy je na integration events przed wysłaniem do brokera.

Zasada, którą trzymam jak żelaznej: integration events to publiczne API twojego serwisu. Wersjonowanie ("version": "1.0"), backward compatibility, dokumentacja - tak samo jak dla REST API. Jeśli traktujesz je jak prywatny szczegół implementacyjny, ERP się rozpadnie przy pierwszej refaktoryzacji.

Event bus - Kafka, RabbitMQ, Redis Streams

Wybór brokera dla typowego sklepu B2B to nie jest dyskusja "co jest najlepsze", tylko "co adekwatne do skali". Cztery realne opcje, każda z własnym profilem.

RabbitMQ to AMQP protocol, dojrzały, dobrze znany w polskim ekosystemie. Routing przez exchanges (direct, topic, fanout), persystencja wiadomości, ACK/NACK, retry mechaniki. Magento 2 ma natywny Magento Queue zbudowany na RabbitMQ, Symfony Messenger też wspiera. To mój default dla typowego sklepu B2B, bo ekipa zwykle to zna, dokumentacja jest doskonała, operacje przewidywalne.

Apache Kafka jest distributed log z partitionowaniem i replikacją. Bardzo wysoki throughput - miliony wiadomości na sekundę. Trzymanie historii eventów (kompaktowanie, retention) umożliwia event sourcing i replay. To overkill dla małego sklepu - kosztowny w utrzymaniu, ma wysoką krzywą uczenia, wymaga osobnego ZooKeepera albo KRaft. Sięgam po niego dopiero, gdy sklep przekracza 50 tys. eventów na sekundę albo gdy event sourcing jest świadomym wymogiem architektonicznym.

Redis Streams to lekka opcja wbudowana w Redis. Persystencja, consumer groups, partial ordering. Bez dodatkowej infrastruktury, bo Redis i tak masz w stacku. Limit: jeden serwer Redis, chyba że stawiasz Redis Cluster, co już komplikuje sprawę. Dobre na start małego sklepu, który nie chce inwestować w RabbitMQ z dnia pierwszego.

AWS SQS albo Google Pub/Sub to managed messaging w chmurze. Zerokoszt utrzymania, pay-per-use, świetna integracja z resztą stacku AWS/GCP. SQS FIFO ma limit 300 wiadomości na sekundę, standard SQS jest at-least-once z dużo wyższą przepustowością. Wybór dla cloud-native projektów, które już żyją w jednej chmurze.

Decyzja dla typowego sklepu B2B z 50 tys. SKU wygląda tak:

Zastosowanie Broker
Integracje sklep-ERP RabbitMQ
Wewnętrzne kolejki (email, reindexer) RabbitMQ albo Redis Streams
High-throughput analytics events Kafka, jeśli przekroczysz 50 tys. eventów/s
Cloud-native sklep w AWS SQS plus EventBridge

Kolejki w e-commerce - osobny artykuł o kolejkach, bo to szersze zagadnienie niż sam EDA.

Idempotencja, retry, dead letter queue

Bez tych trzech rzeczy EDA w produkcji się nie utrzyma. Pomijanie któregoś z nich to gwarancja, że za miesiąc zespół będzie wyciągać dane z bazy po incydencie, w którym podwójnie zafakturowano klienta.

Idempotencja oznacza, że ten sam event może być dostarczony wielokrotnie bez efektów ubocznych. Powody, dla których eventy dostarczane są wielokrotnie, są banalne: większość brokerów ma semantykę at-least-once (woli wysłać dwa razy niż zgubić), retry po nieudanym ACK, replay przez consumer group po restarcie konsumenta. Implementacja prosta:

public function handle(OrderCreatedEvent $event): void
{
    if ($this->repository->findByEventId($event->id)) {
        return;
    }

    $order = $this->createOrder($event->data);

    $this->repository->markProcessed($event->id, $order->id);
}

Pierwszy strzał sprawdza, czy event był już procesowany. Jeśli tak - return, nic się nie dzieje. Jeśli nie - normalna obsługa plus zapamiętanie event_id. Bez tego dostarczenie tego samego "OrderCreated" dwa razy generuje dwa zamówienia w ERP i fakturę dla klienta na podwójną kwotę.

Retry strategies dzielę na trzy poziomy. Immediate retry dla transient errors (network glitch, krótki spike na serwerze) - próbuję natychmiast raz albo dwa razy. Exponential backoff dla bardziej trwałych problemów - 1, 2, 4, 8, 16 sekund. Max attempts ustawiam typowo na 5-10 prób, po których event idzie do dead letter queue.

Dead letter queue to miejsce dla eventów, które po wszystkich retry'ach wciąż nie idą. Zwykle wymagają interwencji człowieka - bo coś jest nie tak ze strukturą eventu, ERP zwraca błąd biznesowy (klient nie istnieje, produkt zniknął z katalogu) albo konsument ma bug. Architektura:

RabbitMQ kolejka główna → 5 nieudanych prób → DLQ "orders.dlq"
                                              → monitoring głębokości DLQ
                                              → alert do zespołu
                                              → manualne reprocess po naprawie

DLQ musi być monitorowane jako pierwsza linia alertingu. Jeśli wpisy się tam piętrzą, ktoś ma problem. Cisza w DLQ oznacza zdrowy system. Widziałem zespoły, które miały 10 tys. eventów w DLQ i nikt o tym nie wiedział, bo nikt nie patrzył - po pół roku wyszły z tego problemy księgowe na sześciocyfrowe kwoty.

Event sourcing - kiedy ma sens

Event sourcing to osobne podejście, często mylone z event-driven. Idea: zamiast trzymać aktualny stan jako wiersz w tabeli, trzymamy listę wszystkich eventów, które do tego stanu doprowadziły.

Zamiast:
  orders table: { id: 1, status: "shipped", total: 1234.56 }

Mamy listę eventów:
  1. OrderCreated { total: 1000 }
  2. ItemAdded { id: 1, sku: "X", price: 234.56 }
  3. PaymentReceived { amount: 1234.56 }
  4. OrderShipped { tracking: "DPD-12345" }

Aktualny stan to projekcja - replay eventów daje aktualną wersję. To brzmi ekstrawagancko, ale ma realne plusy w konkretnych domenach. Pełny audit log za darmo, bo każda zmiana to event - kto, kiedy, co. Możliwość replay: "co by się stało, gdybyśmy zmienili regułę X". Rekonstrukcja stanu na dowolny moment w przeszłości (klient pyta "jak wyglądało moje zamówienie 30 czerwca o 15:00"). Naturalna kompatybilność z CQRS.

Minusy są też realne. Złożoność implementacji rośnie znacząco, zespół musi to znać. Eventy historyczne nie mogą być zmieniane (jeśli zmienisz schemat eventu, replay się nie udaje - musisz wersjonować eventy od pierwszego dnia). Snapshotting jest wymagany dla wydajności - rekonstrukcja stanu z 10 tys. eventów to sekundy, nikt nie poczeka. Zespół musi rozumieć DDD i event sourcing porządnie, bo brak tej wiedzy generuje katastrofalne błędy projektowe.

Event sourcing ma sens, kiedy audit jest absolutnym wymogiem (sektor finansowy, regulacje prawne, rozliczenia z dużymi klientami B2B). Kiedy reporting wymaga danych historycznych (analiza decyzji w czasie, trendy z roku na rok). Kiedy domena jest realnie skomplikowana - wiele zmian stanu, dużo reguł biznesowych. Kiedy zespół jest doświadczony w DDD i CQRS.

Event sourcing nie ma sensu dla klasycznego CRUD-owego sklepu, w małym zespole (1-3 osoby), kiedy wydajność jest absolutną priorytetową albo kiedy biznes nie wymaga audit logu na poziomie pełnej historii. W sklepie B2B stosuję go selektywnie - dla wybranych obszarów (zamówienia, faktury, rozliczenia z klientami kontraktowymi), reszta (katalog, profile klientów) idzie klasycznym CRUD-em. Hybryda jest realniejsza niż "all-in".

Kiedy zostać przy synchronicznych integracjach

To moja ulubiona sekcja, bo idzie w kontrze do mody. EDA jest popularne, w propozycjach konsultantów jest wszędzie, ale często synchroniczne integracje są lepszym wyborem - tańsze w utrzymaniu, łatwiejsze do debugowania, wystarczające do skali.

Zostań przy synchronicznych integracjach w pięciu sytuacjach.

Operacja musi być atomiczna z punktu widzenia klienta. Zapłata kartą plus rejestracja zamówienia - klient musi wiedzieć natychmiast, czy płatność przeszła, czy nie. Synchroniczny API gateway daje odpowiedź klientowi w sekundę, event-driven dorzuca tu komplikację bez wartości.

Niska skala. Małe sklepy z mniej niż 1000 zamówień dziennie nie potrzebują event busu. Synchroniczny API call do ERP z prostym retry on failure wystarcza, koszt jest zerowy, debugowanie trywialne. Event bus przy takiej skali to overhead per wartość.

Mało touchpointów. Jeśli sklep integruje się tylko z ERP (jeden touchpoint), klasyczne API call jest po prostu prostsze. Event bus zaczyna się opłacać przy trzech-czterech systemach konsumujących te same eventy.

Brak zespołu do utrzymania event busu. Kafka i RabbitMQ wymagają wiedzy operacyjnej. Bez DevOpsa, który to umie, event bus padnie w nocy i nikt nie wie, jak go podnieść. Lepiej zostać przy synchronicznych do czasu, aż zespół urośnie.

Strong consistency wymagana. "Klient płaci, klient natychmiast widzi, że zamówienie zostało złożone i przekazane do magazynu" - synchroniczne lepsze niż eventually consistent. Eventually consistent to dla większości klientów B2B akceptowalne, ale dla tych z kontraktowymi SLA "potwierdzenie w ciągu 5 sekund" nie da się obejść asynchronicznie.

W praktyce najczęściej najlepiej działa miks. Krytyczne operacje (złożenie zamówienia, płatność) - synchroniczne. Powiadomienia (email, SMS, push) - asynchroniczne przez prostą kolejkę. Integracje z ERP, WMS, OMS - asynchroniczne przez event bus, jeśli skala uzasadnia, synchroniczne webhooks, jeśli nie. To nie jest "all-in EDA" ani "wszystko synchroniczne", tylko świadoma decyzja per case.

FAQ

Czy event-driven to to samo co microservices? Nie. Microservices to podział aplikacji na osobne procesy (różne deploye, różne bazy), event-driven to wzorzec komunikacji. Można mieć monolit z event-driven internal communication (Symfony Messenger z domain events). Można mieć microservices komunikujące się synchronicznie (REST API gateway). Te koncepty często idą razem, ale są niezależne i wybierasz je z różnych powodów.

Jak debugować błędy przez 5 serwisów wpinających się w event? Distributed tracing jest obowiązkowy, nie opcjonalny. OpenTelemetry propaguje trace_id przez event bus (dorzucasz traceparent do metadanych wiadomości). W Grafanie albo Datadog widzisz całą ścieżkę eventu od emisji po wszystkie konsumenty. Plus correlation_id w każdym evencie, żeby móc znaleźć powiązane logi we wszystkich serwisach.

Ile dodatkowej infrastruktury wymaga event bus? Minimum to klaster RabbitMQ z trzema nodami (1.5-3 EUR za godzinę na AWS), monitoring (Prometheus plus Grafana - dodatkowe maszyny). Realistycznie dla typowego sklepu B2B z 50 tys. SKU to 100-300 EUR miesięcznie. Plus 0.2-0.3 etatu DevOps na utrzymanie, monitoring DLQ, reagowanie na incydenty.

Czy event bus jest single point of failure? Potencjalnie tak, ale właściwa konfiguracja to eliminuje. RabbitMQ cluster 3+ node z replikacją. Kafka partitions plus replicas z minimum 3 broker nodes. Plus circuit breaker w aplikacji - gdy event bus pada, fallback do synchronicznych wywołań albo lokalny bufor w bazie, żeby nie tracić eventów. Z dobrą konfiguracją event bus jest bardziej dostępny niż twoja baza główna.

Czy mogę zacząć z event-driven na małą skalę? Tak, i polecam to robić właśnie tak. Redis Streams albo Symfony Messenger z lokalnym transportem - lekko, bez dodatkowej infrastruktury. Później migracja na RabbitMQ, kiedy skala uzasadnia. Nie zaczynaj od Kafki, to overkill dla większości polskich sklepów B2B i krzywa uczenia jest stroma.

Co dalej

O autorze

Jakub Owsianka

Architekt rozwiązania w WiseB2B - silniku platform B2B. Zaczynał po stronie biznesu (własne sklepy), potem deweloper, dziś projektuje wdrożenia dla sklepów z katalogami w dziesiątkach tysięcy SKU. W ostatnich latach wdrożył AI-development w zespole i funkcjonalności oparte o AI bezpośrednio w silniku sklepu.

Masz pytanie do tego artykułu?

Dodatkowy kontekst, problem z własnym wdrożeniem, druga opinia - napisz wprost. Odpowiadam osobiście w 1-2 dni robocze.