Event Catalog¶
DEPRECATED: This manual event catalog has been superseded by EventCatalog. View the live catalog at: https://staging-connect-eventcatalog.forma3d.be Source:
docs/03-architecture/eventcatalog/
This document provides a comprehensive reference for all events in the Forma3D Connect system — both cross-service events that travel over Redis/BullMQ and internal events that stay within a single service process.
Architecture Overview¶
The system uses a two-layer event architecture:
| Layer | Transport | Scope | Library |
|---|---|---|---|
| Cross-service | BullMQ (Redis) | Between microservices | @forma3d/service-common BullMqEventBus |
| Internal | NestJS EventEmitter2 | Within a single service process | @nestjs/event-emitter |
Every microservice uses EventEmitterModule.forRoot() for internal coordination and EventBusModule.forRootAsync() for cross-service messaging. Two bridge services in each app connect the layers:
EventPublisherService— listens to internal EventEmitter2 events via@OnEvent(...)and publishes them to BullMQ so other services can react.EventSubscriberService— subscribes to incoming BullMQ events from other services and re-emits them as internal EventEmitter2 events for local modules.
Internal vs Cross-Service Event Naming¶
Internal event names (EventEmitter2) and cross-service event names (BullMQ queue) may differ. The bridge services handle the translation:
| Internal Name (EventEmitter2) | Cross-Service Name (BullMQ) | Used In |
|---|---|---|
printjob.completed |
print-job.completed |
Print Service → Order Service |
printjob.failed |
print-job.failed |
Print Service → Order Service |
printjob.status-changed |
print-job.status-changed |
Print Service → Order Service |
printjob.cancelled |
print-job.cancelled |
Print Service → Order Service |
| (direct publish) | integration.simplyprint-changed |
Print Service → Order Service |
shipment.created |
shipment.created |
Shipping Service → Order Service |
sendcloud.shipment.status_changed |
shipment.status-changed |
Shipping Service → Order Service |
| (direct publish) | integration.sendcloud-changed |
Shipping Service → Order Service |
order.created |
order.created |
Order Service → Print Service |
order.ready-for-fulfillment |
order.ready-for-fulfillment |
Order Service → Shipping Service |
order.cancelled |
order.cancelled |
Order Service → Print Service, Shipping Service |
Cross-Service Events (BullMQ / Redis)¶
These 15 events travel between microservices via dedicated BullMQ queues. Each event type = one Redis queue.
Source: libs/service-common/src/lib/events/event-types.ts
Delivery Guarantees¶
| Property | Value |
|---|---|
| Delivery | At-least-once |
| Ordering | Per-queue FIFO (no cross-queue ordering) |
| Retries | 3 attempts with exponential backoff (1s, 2s, 4s) |
| Concurrency | 5 workers per queue per service instance |
| Dead letter | Failed events retained (removeOnFail: 5000) |
| Completed cleanup | removeOnComplete: 1000 |
| Idempotency | Required for all handlers |
| Transport | BullMQ (Redis) |
| Library | @forma3d/service-common BullMqEventBus |
Base Event Interface¶
All cross-service events extend this base:
interface ServiceEvent {
eventId: string; // UUID v4 — unique event identifier for idempotency
eventType: string; // Queue name (e.g. 'order.created')
source: string; // Source service (e.g. 'order-service')
tenantId: string; // Tenant identifier for multi-tenancy
timestamp: string; // ISO 8601
correlationId?: string; // For distributed tracing
}
Note: The
eventIdandsourcefields were added following a CloudEvents evaluation. While full CloudEvents compliance was deemed unnecessary for internal BullMQ transport, these two fields provide the practical benefits of unique event identification (for idempotency checks) and source tracing (for debugging cross-service flows). See the research document for the full analysis.
Order Events (cross-service)¶
Publisher: Order Service (apps/order-service)
Bridge: apps/order-service/src/events/event-publisher.service.ts
| BullMQ Queue | SERVICE_EVENTS Constant |
Internal Event Listened | Internal Constant |
|---|---|---|---|
order.created |
ORDER_CREATED |
order.created |
ORDER_EVENTS.CREATED |
order.ready-for-fulfillment |
ORDER_READY_FOR_FULFILLMENT |
order.ready-for-fulfillment |
ORDER_EVENTS.READY_FOR_FULFILLMENT |
order.cancelled |
ORDER_CANCELLED |
order.cancelled |
ORDER_EVENTS.CANCELLED |
order.created¶
Published when a new order is ingested from Shopify (including via backfill).
interface OrderCreatedEvent extends ServiceEvent {
eventType: 'order.created';
// eventId, source, tenantId, timestamp, correlationId inherited from ServiceEvent
orderId: string;
lineItems: Array<{
lineItemId: string;
productSku: string;
quantity: number;
}>;
}
Flow: OrdersService.createFromShopify() → EventEmitter order.created → EventPublisherService → BullMQ
Subscribers: Print Service (EventSubscriberService → re-emits as internal order.created)
order.ready-for-fulfillment¶
Published when all print jobs for an order are complete.
interface OrderReadyForFulfillmentEvent extends ServiceEvent {
eventType: 'order.ready-for-fulfillment';
orderId: string;
}
Flow: OrchestrationService.markOrderReadyForFulfillment() → EventEmitter order.ready-for-fulfillment → EventPublisherService → BullMQ
Subscribers: Shipping Service (EventSubscriberService → re-emits as internal orchestration.order.ready_for_fulfillment → SendcloudService)
order.cancelled¶
Published when an order is cancelled.
interface OrderCancelledEvent extends ServiceEvent {
eventType: 'order.cancelled';
orderId: string;
}
Flow: OrdersService.cancelOrder() → EventEmitter order.cancelled → EventPublisherService → BullMQ
Subscribers: Print Service (EventSubscriberService → re-emits as order.cancelled), Shipping Service (EventSubscriberService → re-emits as order.cancelled)
Print Job Events (cross-service)¶
Publisher: Print Service (apps/print-service)
Bridge: apps/print-service/src/events/event-publisher.service.ts
| BullMQ Queue | SERVICE_EVENTS Constant |
Internal Event Listened | Internal Constant |
|---|---|---|---|
print-job.completed |
PRINT_JOB_COMPLETED |
printjob.completed |
PRINT_JOB_EVENTS.COMPLETED |
print-job.failed |
PRINT_JOB_FAILED |
printjob.failed |
PRINT_JOB_EVENTS.FAILED |
print-job.status-changed |
PRINT_JOB_STATUS_CHANGED |
printjob.status-changed |
PRINT_JOB_EVENTS.STATUS_CHANGED |
print-job.cancelled |
PRINT_JOB_CANCELLED |
printjob.cancelled |
PRINT_JOB_EVENTS.CANCELLED |
Note: Internal event names use
printjob.*(no hyphen) while cross-service names useprint-job.*(with hyphen). TheEventPublisherServicelistens viaPRINT_JOB_EVENTS.*constants and translates toSERVICE_EVENTS.*constants for BullMQ.
print-job.completed¶
interface PrintJobCompletedEvent extends ServiceEvent {
eventType: 'print-job.completed';
printJobId: string;
orderId: string;
lineItemId: string;
}
Subscribers: Order Service (EventSubscriberService → fetches full PrintJob from DB → re-emits as internal printjob.completed with PrintJobCompletedEvent → OrchestrationService.handlePrintJobCompleted())
print-job.failed¶
interface PrintJobFailedEvent extends ServiceEvent {
eventType: 'print-job.failed';
printJobId: string;
orderId: string;
lineItemId: string;
errorMessage: string;
}
Subscribers: Order Service (EventSubscriberService → fetches full PrintJob from DB → re-emits as internal printjob.failed with PrintJobFailedEvent → OrchestrationService.handlePrintJobFailed())
print-job.status-changed¶
interface PrintJobStatusChangedEvent extends ServiceEvent {
eventType: 'print-job.status-changed';
printJobId: string;
orderId: string;
lineItemId: string;
previousStatus: string;
newStatus: string;
}
Subscribers: Order Service (EventSubscriberService → forwarded to orchestration)
print-job.cancelled¶
interface PrintJobCancelledEvent extends ServiceEvent {
eventType: 'print-job.cancelled';
printJobId: string;
orderId: string;
lineItemId: string;
}
Subscribers: Order Service (EventSubscriberService → fetches full PrintJob from DB → re-emits as internal printjob.cancelled with PrintJobCancelledEvent → OrchestrationService.handlePrintJobCancelled())
Shipment Events (cross-service)¶
Publisher: Shipping Service (apps/shipping-service)
Bridge: apps/shipping-service/src/events/event-publisher.service.ts
| BullMQ Queue | SERVICE_EVENTS Constant |
Internal Event Listened | Internal Constant |
|---|---|---|---|
shipment.created |
SHIPMENT_CREATED |
shipment.created |
SHIPMENT_EVENTS.CREATED |
shipment.status-changed |
SHIPMENT_STATUS_CHANGED |
sendcloud.shipment.status_changed |
SENDCLOUD_WEBHOOK_EVENTS.SHIPMENT_STATUS_CHANGED |
Note: The
shipment.status-changedcross-service event is triggered by the internalsendcloud.shipment.status_changedevent (emitted bySendcloudWebhookServiceandSendcloudReconciliationService). TheEventPublisherServicetranslates between the naming conventions.
shipment.created¶
interface ShipmentCreatedEvent extends ServiceEvent {
eventType: 'shipment.created';
shipmentId: string;
orderId: string;
trackingNumber: string | null;
trackingUrl: string | null;
carrier: string | null;
}
Flow: SendcloudService.createShipment() → EventEmitter shipment.created → EventPublisherService → BullMQ
Subscribers: Order Service (EventSubscriberService → FulfillmentService creates Shopify fulfillment)
shipment.status-changed¶
interface ShipmentStatusChangedEvent extends ServiceEvent {
eventType: 'shipment.status-changed';
shipmentId: string;
orderId: string;
previousStatus: string;
newStatus: string;
}
Flow: SendcloudWebhookService.processStatusChange() or SendcloudReconciliationService → EventEmitter sendcloud.shipment.status_changed → EventPublisherService → BullMQ
Subscribers: Order Service (EventSubscriberService → updates order shipment status)
GridFlock Events (cross-service)¶
Publisher: GridFlock Service (apps/gridflock-service)
Bridge: apps/gridflock-service/src/events/event-publisher.service.ts
| BullMQ Queue | SERVICE_EVENTS Constant |
Internal Event Listened |
|---|---|---|
gridflock.mapping-ready |
GRIDFLOCK_MAPPING_READY |
gridflock.mapping-ready |
gridflock.pipeline-failed |
GRIDFLOCK_PIPELINE_FAILED |
gridflock.pipeline-failed |
gridflock.mapping-ready¶
Published when the full pipeline completes (STL → slice → upload → mapping) or an existing mapping is found.
interface GridflockMappingReadyEvent extends ServiceEvent {
eventType: 'gridflock.mapping-ready';
orderId: string;
lineItemId: string;
sku: string; // e.g. "GRID-2x3-MAG-NONE"
}
Subscribers: Order Service (EventSubscriberService → OrchestrationService.handleGridflockMappingReady())
gridflock.pipeline-failed¶
interface GridflockPipelineFailedEvent extends ServiceEvent {
eventType: 'gridflock.pipeline-failed';
orderId: string;
lineItemId: string;
sku: string;
errorMessage: string;
failedStep: 'stl-generation' | 'slicing' | 'simplyprint-upload' | 'mapping-creation';
}
Subscribers: Order Service (EventSubscriberService → marks line item as FAILED)
Inventory Events (cross-service)¶
Publisher: Order Service (apps/order-service)
Bridge: apps/order-service/src/events/event-publisher.service.ts
| BullMQ Queue | SERVICE_EVENTS Constant |
Internal Event Listened | Internal Constant |
|---|---|---|---|
inventory.stock-replenishment-scheduled |
STOCK_REPLENISHMENT_SCHEDULED |
(direct publish) | — |
inventory.stock-batch-completed |
STOCK_BATCH_COMPLETED |
(direct publish) | — |
inventory.stock-replenishment-scheduled¶
Published when the stock replenishment scheduler creates a print job for stock building. One event per print job in the batch.
interface StockReplenishmentScheduledEvent extends ServiceEvent {
eventType: 'inventory.stock-replenishment-scheduled';
printJobId: string;
fileId: string | null;
purpose: 'STOCK';
stockBatchId: string;
}
Flow: StockReplenishmentService.evaluateAndSchedule() → BullMQEventBus.publish() → BullMQ
Subscribers: Print Service (EventSubscriberService → creates print job via SimplyPrint)
inventory.stock-batch-completed¶
Published when all print jobs in a stock batch have completed, triggering atomic stock increment.
interface StockBatchCompletedEvent extends ServiceEvent {
eventType: 'inventory.stock-batch-completed';
stockBatchId: string;
productMappingId: string;
totalJobs: number;
}
Flow: InventoryService.handleStockJobCompleted() → BullMQEventBus.publish() → BullMQ
Subscribers: (informational — consumed by monitoring/logging)
Integration Events (cross-service)¶
Purpose: Notify consumer services when third-party integration credentials change so they can re-initialize their API clients immediately, without requiring a container restart.
Unlike other cross-service events, these are published directly via BullMQEventBus.publish() from *ConnectionService classes (not bridged from EventEmitter2 via EventPublisherService).
| BullMQ Queue | SERVICE_EVENTS Constant |
Publisher | Subscriber(s) |
|---|---|---|---|
integration.simplyprint-changed |
INTEGRATION_SIMPLYPRINT_CHANGED |
Print Service | Order Service |
integration.sendcloud-changed |
INTEGRATION_SENDCLOUD_CHANGED |
Shipping Service | Order Service |
integration.simplyprint-changed¶
Published when a user connects or disconnects the SimplyPrint integration via Settings > Integrations.
interface IntegrationChangedEvent extends ServiceEvent {
eventType: 'integration.simplyprint-changed' | 'integration.sendcloud-changed';
action: 'connected' | 'disconnected';
}
Flow: SimplyPrintConnectionService.saveConnection() or .disconnect() → BullMQEventBus.publish() → BullMQ
Subscribers: Order Service (EventSubscriberService → SimplyPrintInitializerService.reinitialize() or .disable())
integration.sendcloud-changed¶
Published when a user connects or disconnects the Sendcloud integration via Settings > Integrations.
// Same IntegrationChangedEvent interface as above
Flow: SendcloudConnectionService.saveConnection() or .disconnect() → BullMQEventBus.publish() → BullMQ
Subscribers: Order Service (EventSubscriberService → SendcloudInitializerService.reinitialize() or .disable())
Architecture Note: These events bypass the EventEmitter2 → EventPublisherService bridge pattern used by other cross-service events. The
*ConnectionServicepublishes directly to BullMQ because the credential change is already being handled locally (the owning service re-initializes its own API client inline), so there is no need for an internal event — only downstream services need to be notified.
Internal Events (EventEmitter2)¶
These events stay within a single service process. They are not transmitted over Redis. Each service defines its own event constants and event classes.
Order Service — Internal Events¶
Source: apps/order-service/src/orders/events/order.events.ts
ORDER_EVENTS¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
CREATED |
order.created |
OrdersService.createFromShopify() |
OrchestrationService, EventPublisherService → BullMQ, EventsGateway → Socket.IO, PushService → Web Push |
STATUS_CHANGED |
order.status_changed |
OrdersService.updateStatus() |
EventsGateway → Socket.IO, PushService → Web Push |
CANCELLED |
order.cancelled |
OrdersService.cancelOrder() |
CancellationService, EventPublisherService → BullMQ, EventsGateway → Socket.IO |
READY_FOR_FULFILLMENT |
order.ready-for-fulfillment |
OrchestrationService.markOrderReadyForFulfillment() |
FulfillmentService, EventPublisherService → BullMQ, EventsGateway → Socket.IO |
FULFILLED |
order.fulfilled |
FulfillmentService |
EventsGateway → Socket.IO |
FAILED |
order.failed |
OrchestrationService |
EventsGateway → Socket.IO |
Source: apps/order-service/src/print-jobs/events/print-job.events.ts
PRINT_JOB_EVENTS (within Order Service)¶
These are re-emitted by the EventSubscriberService when print-job events arrive from BullMQ. The subscriber fetches the full PrintJob from the database and constructs proper domain events, ensuring internal handlers receive the complete entity.
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
CREATED |
printjob.created |
PrintJobsService.createSinglePrintJob() |
EventsGateway → Socket.IO |
STATUS_CHANGED |
printjob.status-changed |
PrintJobsService.updateStatus(), EventSubscriberService (from BullMQ) |
OrchestrationService, EventsGateway → Socket.IO, PushService → Web Push |
COMPLETED |
printjob.completed |
EventSubscriberService (from BullMQ print-job.completed) |
OrchestrationService, EventsGateway → Socket.IO |
FAILED |
printjob.failed |
EventSubscriberService (from BullMQ print-job.failed) |
OrchestrationService, NotificationsService, EventsGateway → Socket.IO |
CANCELLED |
printjob.cancelled |
EventSubscriberService (from BullMQ print-job.cancelled) |
OrchestrationService, EventsGateway → Socket.IO |
RETRY_REQUESTED |
printjob.retry-requested |
PrintJobsService.retryJob() |
(event log only) |
Source: apps/order-service/src/orchestration/orchestration.service.ts
ORCHESTRATION_EVENTS¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
ORDER_READY_FOR_FULFILLMENT |
order.ready-for-fulfillment |
OrchestrationService.markOrderReadyForFulfillment() |
FulfillmentService, EventPublisherService → BullMQ |
ORDER_PARTIALLY_COMPLETED |
order.partially-completed |
OrchestrationService.markOrderPartiallyCompleted() |
(logging only) |
ORDER_ALL_JOBS_FAILED |
order.all-jobs-failed |
OrchestrationService.markOrderFailed() |
(logging only) |
Order Revival: When
PRINT_JOB_EVENTS.STATUS_CHANGEDindicates a terminal→active transition (e.g., CANCELLED→QUEUED), theOrchestrationService.handlePrintJobStatusChanged()reverts the order from FAILED/PARTIALLY_COMPLETED/CANCELLED back to PROCESSING and logs anorder.revivedevent to the EventLog. This allows orders to recover automatically when print jobs are requeued.
Source: apps/order-service/src/sendcloud/events/shipment.events.ts
SHIPMENT_EVENTS (within Order Service)¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
CREATED |
shipment.created |
EventSubscriberService (from BullMQ shipment.created) |
FulfillmentService.handleShipmentCreated(), PushService |
LABEL_READY |
shipment.label-ready |
(not used — label events handled in Shipping Service) | PushService |
FAILED |
shipment.failed |
(not used — errors handled in Shipping Service) | (logging only) |
UPDATED |
shipment.updated |
(not used — updates come via BullMQ shipment.status-changed) |
(logging only) |
Source: apps/order-service/src/fulfillment/events/fulfillment.events.ts
FULFILLMENT_EVENTS (within Order Service)¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
CREATED |
fulfillment.created |
FulfillmentService.createFulfillment() |
(logging only) |
FAILED |
fulfillment.failed |
FulfillmentService.handleFulfillmentError() |
NotificationsService.handleFulfillmentFailed() |
RETRYING |
fulfillment.retrying |
FulfillmentService (on retry) |
(logging only) |
Print Service — Internal Events¶
Source: apps/print-service/src/print-jobs/events/print-job.events.ts
PRINT_JOB_EVENTS¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
CREATED |
printjob.created |
PrintJobsService.createSinglePrintJob() |
(internal logging) |
STATUS_CHANGED |
printjob.status-changed |
PrintJobsService.updateJobStatus() |
EventPublisherService → BullMQ (print-job.status-changed) |
COMPLETED |
printjob.completed |
PrintJobsService.updateJobStatus() |
EventPublisherService → BullMQ (print-job.completed) |
FAILED |
printjob.failed |
PrintJobsService.updateJobStatus() |
EventPublisherService → BullMQ (print-job.failed), NotificationsService |
CANCELLED |
printjob.cancelled |
PrintJobsService.cancelJob() |
EventPublisherService → BullMQ (print-job.cancelled) |
RETRY_REQUESTED |
printjob.retry-requested |
PrintJobsService.retryJob() |
(internal only) |
Source: apps/print-service/src/simplyprint/simplyprint.service.ts
SIMPLYPRINT_EVENTS¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
JOB_STATUS_CHANGED |
simplyprint.job-status-changed |
SimplyPrintService (webhook/polling), SimplyPrintReconciliationService |
PrintJobsService.handleSimplyPrintStatusChange() |
Reconciliation: The
SimplyPrintReconciliationService(print-service only) periodically polls SimplyPrint to catch missed status changes. It checks the printer's actual state (e.g.,OPERATIONAL= completed,PRINTING= still active) to correctly determine job status. Events from reconciliation flow through the samesimplyprint.job-status-changed→PrintJobsService→PRINT_JOB_EVENTS.*→EventPublisherService→ BullMQ pipeline.
Source: apps/print-service/src/fulfillment/events/fulfillment.events.ts
FULFILLMENT_EVENTS (consumed from cross-service)¶
| Constant | Event Name | Subscribers |
|---|---|---|
FAILED |
fulfillment.failed |
NotificationsService.handleFulfillmentFailed() |
Shipping Service — Internal Events¶
Source: apps/shipping-service/src/sendcloud/events/shipment.events.ts
SHIPMENT_EVENTS¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
CREATED |
shipment.created |
SendcloudService.createShipment() |
FulfillmentService.handleShipmentCreated(), EventPublisherService → BullMQ |
LABEL_READY |
shipment.label-ready |
SendcloudService.createShipment() |
(internal only) |
FAILED |
shipment.failed |
SendcloudService (on error) |
(logging only) |
UPDATED |
shipment.updated |
SendcloudService (on status update) |
(logging only) |
Source: apps/shipping-service/src/sendcloud/sendcloud-webhook.service.ts
SENDCLOUD_WEBHOOK_EVENTS¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
SHIPMENT_STATUS_CHANGED |
sendcloud.shipment.status_changed |
SendcloudWebhookService.processStatusChange(), SendcloudReconciliationService |
EventPublisherService → BullMQ (shipment.status-changed) |
Reconciliation: The
SendcloudReconciliationService(shipping-service only) periodically polls SendCloud to catch missed status changes. Events from reconciliation flow through the samesendcloud.shipment.status_changed→EventPublisherService→ BullMQ pipeline.
Source: apps/shipping-service/src/orchestration/orchestration.service.ts
ORCHESTRATION_EVENTS (Shipping Service)¶
| Constant | Event Name | Re-emitted From | Subscribers |
|---|---|---|---|
ORDER_READY_FOR_FULFILLMENT |
orchestration.order.ready_for_fulfillment |
BullMQ order.ready-for-fulfillment via EventSubscriberService |
SendcloudService.handleOrderReadyForFulfillment() |
Source: apps/shipping-service/src/orders/events/order.events.ts
ORDER_EVENTS (Shipping Service)¶
| Constant | Event Name | Re-emitted From | Subscribers |
|---|---|---|---|
CANCELLED |
order.cancelled |
BullMQ order.cancelled via EventSubscriberService |
(no active handler yet) |
Source: apps/shipping-service/src/fulfillment/events/fulfillment.events.ts
FULFILLMENT_EVENTS¶
| Constant | Event Name | Emitter | Subscribers |
|---|---|---|---|
COMPLETED |
fulfillment.completed |
FulfillmentService.createFulfillment() |
(logging only) |
FAILED |
fulfillment.failed |
FulfillmentService.handleFulfillmentError() |
NotificationsService.handleFulfillmentFailed() |
GridFlock Service — Internal Events¶
The GridFlock Service has no dedicated internal event constants file. Events are emitted directly by GridflockPipelineService:
| Event Name | Emitter | Subscribers |
|---|---|---|
gridflock.mapping-ready |
GridflockPipelineService.processOrderLineItem() |
EventPublisherService → BullMQ |
gridflock.pipeline-failed |
GridflockPipelineService.processOrderLineItem() |
EventPublisherService → BullMQ |
The service also uses a local BullMQ queue (gridflock-generation) for CPU-intensive STL generation jobs. This is a job queue, not an event queue — it processes generate-baseplate and generate-plate-set jobs via GridflockProcessor.
Service Ownership¶
Each external integration's reconciliation/backfill is owned by exactly one service:
| Integration | Owner Service | Reconciliation Service | Webhook Handler |
|---|---|---|---|
| Shopify | Order Service | ShopifyBackfillService |
ShopifyWebhookController |
| SimplyPrint | Print Service | SimplyPrintReconciliationService |
SimplyPrintWebhookController |
| SendCloud | Shipping Service | SendcloudReconciliationService |
SendcloudWebhookController |
Important: Reconciliation services must only exist in their owning service. Duplicate reconciliation services in other services can cause conflicting status updates (e.g., a completed job being reset to printing).
Gateway — Event Monitoring Only¶
The API Gateway (apps/gateway) does not publish or consume any events. It serves three roles:
- Bull Board Dashboard — read-only monitoring of all 13 BullMQ queues at
/admin/queues - Socket.IO Proxy — WebSocket proxy on namespace
/eventswith Redis adapter for horizontal scaling - HTTP Proxy — routes REST requests to downstream services
WebSocket Events (Socket.IO)¶
The EventsGateway in the Order Service listens to internal EventEmitter2 events and broadcasts them to connected frontend clients via Socket.IO.
Source: apps/order-service/src/gateway/events.gateway.ts
| Internal Event | Socket.IO Event | Description |
|---|---|---|
ORDER_EVENTS.CREATED |
order:created |
New order notification |
ORDER_EVENTS.STATUS_CHANGED |
order:updated |
Order status transition |
ORDER_EVENTS.READY_FOR_FULFILLMENT |
order:ready-for-fulfillment |
Order ready to ship |
ORDER_EVENTS.FULFILLED |
order:fulfilled |
Shopify fulfillment created |
ORDER_EVENTS.CANCELLED |
order:cancelled |
Order cancelled |
ORDER_EVENTS.FAILED |
order:failed |
Order processing failed |
PRINT_JOB_EVENTS.CREATED |
printjob:created |
Print job queued |
PRINT_JOB_EVENTS.STATUS_CHANGED |
printjob:updated |
Print job status update |
PRINT_JOB_EVENTS.COMPLETED |
printjob:completed |
Print job finished |
PRINT_JOB_EVENTS.FAILED |
printjob:failed |
Print job failed |
PRINT_JOB_EVENTS.CANCELLED |
printjob:cancelled |
Print job cancelled |
| (manual) | notification |
General notification |
Internal HTTP APIs (Non-Event Communication)¶
In addition to events, services communicate synchronously via internal HTTP APIs protected by X-Internal-Key header:
| Source | Target | Endpoint | Purpose |
|---|---|---|---|
| Order Service | Print Service | POST /internal/print-jobs |
Create print jobs for order |
| Order Service | Shipping Service | POST /internal/shipments |
Create shipment for order |
| Order Service | GridFlock Service | POST /internal/gridflock/generate-for-order |
Trigger GridFlock pipeline |
| Order Service | Print Service | DELETE /internal/print-jobs/:id |
Cancel print job |
| Order Service | Shipping Service | DELETE /internal/shipments/:orderId |
Cancel shipment |
Event Best Practices¶
- Use constants, not strings: Always use event constant objects (
PRINT_JOB_EVENTS,ORDER_EVENTS,SHIPMENT_EVENTS,SENDCLOUD_WEBHOOK_EVENTS,ORCHESTRATION_EVENTS) in@OnEvent()decorators andeventEmitter.emit()calls. Never use hardcoded strings — they cause silent mismatches. - Event Naming: Cross-service events use lowercase with dots as separators (e.g.,
order.created). Internal events may differ (e.g.,printjob.completedvsprint-job.completed) — the bridge services handle translation. - Payload Design: Include enough context for consumers to act without additional queries. For print job events, the Order Service subscriber enriches thin cross-service events by fetching the full entity from the database.
- Idempotency: All event handlers MUST be idempotent — use
eventIdto deduplicate replayed events, or check if work is already done before acting. - Error Handling: Always wrap event handlers in try-catch with proper logging.
- No duplicate processing: BullMQ at-least-once delivery means handlers may receive the same event twice.
- Dead letter monitoring: Monitor failed events in BullMQ dead letter queues via Bull Board (
/admin/queues). - Event ordering: Do not rely on cross-queue ordering — events from different queues may arrive in any order.
- Bridge pattern: When adding a new cross-service event:
- Define the event type in
libs/service-common/src/lib/events/event-types.ts - Add the publisher bridge in the owning service's
EventPublisherService - Add the subscriber bridge in consuming services'
EventSubscriberService - Use constants (not hardcoded strings) in both the publisher
@OnEvent()and subscribereventEmitter.emit() - Internal events stay internal: Not every internal event needs a cross-service counterpart. Only bridge events that other services need to react to.
- Single owner per integration: Each external integration (Shopify, SimplyPrint, SendCloud) must have its webhook handler and reconciliation service in exactly one microservice. Duplicate handlers/reconciliation in multiple services causes conflicting updates.
- Direct-publish pattern: Integration change events (
integration.simplyprint-changed,integration.sendcloud-changed) bypass the EventEmitter2 → EventPublisherService bridge and publish directly to BullMQ from the*ConnectionService. This is appropriate when the owning service handles the change locally and only needs to notify downstream consumers. - Live re-initialization: When integration credentials change, all services must update their API clients immediately. Never rely solely on startup initialization — use the
*InitializerService.reinitialize()/.disable()pattern triggered by integration change events.