Skip to content

ADR-0001. In-process EventBus + BullMQ for multi-merchant product sync

FieldValue
StatusAccepted
Date2026-03-01
Deciderscommerce-team
Supersedes

Context

  • A product created/updated for one merchant often must be replicated to additional merchants of the same organizer (chain stores share a catalog).
  • The aggregate create/update is already a single DB transaction (Product + Info + identifiers + channel links + default variant). Replication touches many rows across N merchants and can be slow.
  • It must not block or fail the primary aggregate response, and must survive partial failure (one merchant's copy failing should not roll back the primary).
  • Commerce already runs a BullMQ Redis connection and a worker role.

Decision

Split replication into two stages connected by an in-process EventBus (eventemitter3) and a BullMQ queue:

  1. ProductCreateService / ProductUpdateService commit the primary aggregate, then emit product.aggregate.{created,updated} on the EventBus (EVENT_EMITTER keys in src/common/event.ts).
  2. ProductAggregateCreatedListener / ProductAggregateUpdatedListener handle the event. The created listener also marks the merchant onboarding PRODUCT step. Both call pushJobToQueue() — but only when syncMerchantIds.length > 0.
  3. The job lands on SYNC_PRODUCT_QUEUE (@nx/commerce/sync-product-queue) on the BullMQ Redis.
  4. SyncProductWorker consumes it (job name .create vs .update) and calls ProductCreateSyncService / ProductUpdateSyncService.syncToAdditionalMerchants().

The EventBus decouples emission from enqueue; BullMQ provides durability, retry, and bounded concurrency (APP_ENV_BULLMQ_WORKER_CONCURRENCY).

Consequences

ProsCons
Primary aggregate response is fast — replication is fully asyncEmit happens post-commit; a crash between commit and enqueue loses the sync trigger
Per-merchant failures isolated, retried by BullMQTwo infra hops (EventBus → Redis queue) to reason about
Onboarding step + sync share one event, registered twiceEventBus is in-process only — no cross-replica delivery
Worker concurrency tunable; runs only in worker roleEventual consistency: replicated merchants lag the primary

Alternatives Considered

OptionProsConsWhy rejected
Replicate synchronously inside the aggregate TXStrong consistencySlow response; one bad merchant rolls back allUX + blast radius unacceptable
Kafka topic for sync fan-outCross-replica, durableAdds consumer wiring; commerce is producer-only by ADR-0002Overkill for an internal, same-service job
Direct BullMQ enqueue from the service (no EventBus)Fewer hopsCouples service to queue; onboarding-step side-effect would need a second pathEventBus cleanly multiplexes the event to both listeners

References

  • src/common/event.ts (EVENT_EMITTER.PRODUCT.*)
  • src/components/event-bus/component.ts (registry, double registration of created)
  • src/events/listeners/product-aggregate-created.listener.ts / ...updated.listener.ts
  • src/components/queues/queue.component.ts, src/components/workers/sync-product.worker.ts
  • src/services/product/product-create.service.ts:225, product-update.service.ts:228

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.