Skip to main content

Booking session pipeline

The embeddable Scheduler widget produces a high-volume stream of session events — step-completed, question-answered, booking-abandoned, booking-completed, etc. These events flow from the widget through a unified pipeline: one entry path, one queue, one consumer.

This page covers the gateway pre-derivation of event ids, the SQS dedup layer, the processor idempotency contract, and the DLQ posture.

Why a unified pipeline

The Scheduler widget talks to the API over two transports — HTTP (booking-flow router events, snapshot writes) and Socket.IO (real-time session events). A single user action can produce events on both transports for the "same" logical step. Treating them as independent writers would let a duplicate slip past whichever path didn't already see it.

The unified pipeline collapses both transports onto a single queue and a single consumer. The dedup gate sits at SQS — keyed off a deterministic id derived from the event payload — so HTTP and Socket.IO producers of the same logical event collapse before reaching MongoDB.

End-to-end flow

deriveEventId — pre-derivation at the gateway

The dedup gate sits at SQS, but SQS needs a stable id at enqueue time. The platform derives it deterministically from the event payload before enqueueing.

  • Source: libs/api/services-api/src/lib/scheduler/booking-session/utils/derive-event-id.ts:23 (export function deriveEventId)
  • Test pin: libs/api/services-api/src/lib/scheduler/booking-session/utils/derive-event-id.spec.ts covers the determinism, payload-sensitivity, and absent-vs-empty-data equivalence cases.

The function hashes a canonical projection of the event (event moment, step id, data fields) into an opaque id. Two enqueues of the "same" event — regardless of which transport produced them — derive the same id and collapse at SQS.

The gateway and the HTTP controller both call deriveEventId before enqueue, so the dedup id is set on every produced message. This is the "unified" part of the unified pipeline.

SQS dedup

The queue is FIFO with a MessageDeduplicationId derived from the event id. SQS deduplicates within a 5-minute window — duplicates within that window are dropped by SQS itself, never reaching the consumer.

  • Producer: libs/api/services-api/src/lib/scheduler/booking-session/queue/booking-session-queue.service.ts
  • Consumer: libs/api/services-api/src/lib/scheduler/booking-session/queue/booking-session-event-processor.service.ts

Consumer-side idempotency

SQS dedup is the first line of defense. The second line is the consumer: every state mutation in BookingSessionEventProcessor is idempotent on the event id. Late deliveries and retries land on the existing record and short-circuit.

The id is the producer's contract — the consumer does not synthesize. The wire-format type declares eventId: string, but SQS bodies are erased JSON at the boundary; only the producer guarantee plus the pre-deploy queue-drain contract keep undefined out of production. The consumer forwards body.eventId unchanged — including undefined if it ever appears — so a missing id surfaces as a downstream failure instead of being papered over with a fresh derivation. A consumer-side body.eventId ?? deriveEventId(event) fallback would collapse two different events that happen to hash to the same id when the publisher's stamp was the load-bearing dedup signal.

Test pin: libs/api/services-api/src/lib/scheduler/booking-session/queue/booking-session-event-processor.service.spec.ts:97-115 (forwards undefined eventId unchanged — does not synthesize consumer-side) forces this decision into PR diff if a "resilience" change ever tries to add the fallback.

DLQ posture

A poison message — one the processor fails to handle after retries — moves to the dead-letter queue. The DLQ contract is documented in apps/dev-docs/docs/guides/deployment.md (the deployment runbook).

Releases that change an SQS message-body schema are also gated on queue depth: the affected queue (and its DLQ if any) must drain to zero before promote, so a stale pre-deploy message can't reach a post-deploy consumer that reads new fields. booking-session-events is currently under this contract because the consumer reads eventId directly from the message body and uses it as the dedup marker. See apps/dev-docs/docs/guides/deployment.md#api-pre-deploy-contracts for the operational check.

  • OpenSpec contracts: booking flow specs at openspec/specs/booking-flow-builder/spec.md, openspec/specs/booking-flow-router/spec.md, openspec/specs/booking-flow-triggers/spec.md
  • Idempotency: Cluster coordination pattern → idempotency-log — the per-side-effect dedup primitive used by the processor
  • Operational signals: redis.cron_fanout.* keys and CloudWatch alarms in copilot/api/addons/observability.yml