Skip to main content

bff-tenant-booking-service

Bounded Context: BFF (Tenant Booking) · Owner: Frontend Platform · Phase: 1 · Storage: No own write model. Memorystore (booking-draft + theme + tenant-bootstrap cache) + Cloud SQL Postgres (analytics outbox + draft snapshots + idempotency + handoff replay log) · Bundle: services/bff-tenant-booking-service/

bff-tenant-booking-service is the Backend-for-Frontend for the tenant-branded booking experience of Ghasi Melmastoon — the responsive web app and React Native mobile app served under each tenant's subdomain (https://{tenantSlug}.melmastoon.ghasi.io) or custom domain (https://booking.tenanthotel.com). A single shared codebase (@ghasi/app-web-tenant-booking, @ghasi/app-mobile-tenant-booking) is themed per tenant at runtime via the tenant bootstrap.

It is a thin orchestration layer. It owns no domain state, no domain mutations, and no domain events. Every page is composed from upstream services: theme-config-service for tenant theme + flow config, property-service for room types + photos + policies, inventory-service for availability, pricing-service for quotes, reservation-service for hold/confirm, payment-gateway-service for payment intents, tenant-service for slug → tenantId resolution. It owns only booking-flow ergonomics (draft state, step transitions, abandoned-cart hint, payment redirect dance, confirmation page composition) and conversion telemetry emitted as melmastoon.bff.tenant.* events.

The cloud is GCP. The desktop is Electron — but this BFF is never consumed by the desktop. It exists exclusively for tenant-facing guest browsers and mobile clients. Each request is tenant-scoped by subdomain, custom-domain mapping, or path prefix; the BFF rejects any request whose tenant context is unresolvable.

Purpose

  • Be the single composition surface for tenant-branded booking, returning per-screen view-models that the web and mobile clients render verbatim — themed, localized, and currency-aware.
  • Bootstrap the tenant brand + booking-flow config in a single round trip so the SPA can render the chrome before fetching availability.
  • Orchestrate the booking flow: search → quote → hold → guest details → payment intent → confirm → confirmation page — without owning any domain state. Every state transition is a write to upstream domain services; the BFF only mirrors a short-lived BookingDraft for UX ergonomics.
  • Honour the handoff token minted by bff-consumer-service so guests arriving from the meta layer land mid-flow with dates, occupancy, currency, and campaign attribution pre-populated.
  • Emit conversion-funnel telemetry (bootstrap.served, draft.created, draft.abandoned, draft.converted, flow.step_completed, flow.error_encountered, payment_intent.created, confirmation.viewed).
  • Apply per-tenant theming + locale end-to-end (RTL/LTR switch, currency display, date format).

Key responsibilities

  1. Tenant resolution — resolve (subdomain | custom domain | path prefix tenantSlug)tenantId via tenant-service with aggressive cache (TTL 1 h, invalidated on melmastoon.tenant.config_updated.v1 and melmastoon.theme.published.v1).
  2. Bootstrap compositionGET /bff/tenant-booking/v1/{tenantSlug}/bootstrap returns { tenant, theme, flowConfig, locales, currencies, paymentMethods, policies, handoffPayload? } from theme-config-service + tenant-service. Embeds CSP nonce + design-token CSS sheet for first-paint.
  3. Availability composition — fan out to inventory-service (per-date allocation) + property-service (room types + photos) + pricing-service (cheapest rate per room type for the stay window).
  4. Quote / hold orchestrationPOST /quote proxies to pricing-service; POST /hold proxies to reservation-service and persists a tenant-scoped BookingDraft blob in Memorystore (TTL 30 min) for UX continuity.
  5. Guest-details capture — accept and validate guest fields; persist into BookingDraft; pass through on /confirm to reservation-service (which is the source of truth for Guest).
  6. Payment intent dancePOST /payment-intent calls payment-gateway-service; the BFF holds the redirect URL and the local return path (/return). On return, BFF verifies state + invokes reservation-service /confirm.
  7. Confirmation page compositionGET /confirmation/{reservationId} composes the post-confirm view-model: confirmed reservation, folio summary, key-credential placeholder if not yet issued, post-stay info.
  8. Abandoned-cart marker (Phase 2+) — emit draft.abandoned.v1 after 30 min of inactivity; consumed by notification-service for opt-in email/SMS recovery.
  9. Themed error rendering — every error response carries the tenant's theme.errorPalette reference so the SPA renders branded errors.
  10. Currency display + FX hint — propagate the user's selected display currency; the BFF re-quotes in display currency only on explicit user-driven currency change events.

Aggregates owned (session / projection only — no domain state)

AggregateCardinalityPurposeIdentity prefix
TenantBootstrap (cache)per tenantComposed bootstrap blob (theme + flowConfig + locales + currencies + paymentMethods)(composite)
BookingDraftper bdr_Short-lived booking draft (TTL 30 min) — search params, selected room, quote, guest details, payment intent referencebdr_
BookingFlowStatechild of BookingDraftFlow step machine (search → select → quote → hold → details → payment → confirm)(n/a)
PaymentSelectionchild of BookingDraftPayment method + redirect outcome marker(n/a)
GuestProfile (booking-time only)child of BookingDraftGuest fields collected during booking; written through to reservation-service on confirm; not retained by BFF(n/a)
LoyaltyContext (Phase 2+)per sessionLoyalty hint passed to pricing/quote(n/a)
MarketingAttributionper sessionUTM + handoff campaign attribution(n/a)
BookingHandoffArrivalshort-livedVerified inbound handoff token (single-use, replay-protected)bha_

There is no domain aggregate owned by this BFF. Postgres rows are limited to outbox, booking_draft_snapshots (cold mirror for analytics + abandoned-cart), idempotency, handoff_arrival_log, and the standard inbox.

Key APIs (REST, /bff/tenant-booking/v1/{tenantSlug})

MethodPathPurpose
GET/bootstrapTenant bootstrap (theme + flowConfig + locales + currencies + paymentMethods)
GET/availabilityPer-room-type availability + price snapshot for stay window
GET/properties/{propertyId}/roomsRoom types + photos for the property
POST/quoteIssue PriceQuote via pricing-service
POST/holdCreate reservation hold via reservation-service (returns bdr_ and TTL)
PATCH/draft/{draftId}Update guest details / preferences on the BookingDraft
POST/draft/{draftId}/payment-intentCreate PaymentIntent via payment-gateway-service
POST/draft/{draftId}/returnPayment gateway redirect-return; confirms reservation if successful
POST/draft/{draftId}/confirmConfirm reservation via reservation-service (alternative path for non-redirect rails)
GET/confirmation/{reservationId}Post-confirm view-model
POST/handoff/consumeValidate inbound bff-consumer handoff token; primes session
POST/session/localeSet locale preference for the session
POST/session/currencySet display currency for the session
GET/sessionRead sanitized session
GET/policiesTenant-published booking + cancellation + privacy policies

All routes resolve tenantSlug first; unknown slugs return MELMASTOON.BFF.TENANT.SLUG_UNKNOWN. Suspended tenants return MELMASTOON.TENANT.SUSPENDED.

Key events published

EventTriggerSample rate
melmastoon.bff.tenant.bootstrap.served.v1Bootstrap successfully composed100% (one per session, debounced)
melmastoon.bff.tenant.booking.draft.created.v1First POST /hold succeeds for a session100%
melmastoon.bff.tenant.booking.draft.abandoned.v1Draft inactive > 30 min and not converted100%
melmastoon.bff.tenant.booking.draft.converted.v1Draft → confirmed reservation100%
melmastoon.bff.tenant.payment_intent.created.v1PaymentIntent created via gateway100%
melmastoon.bff.tenant.confirmation.viewed.v1/confirmation/{id} rendered100%
melmastoon.bff.tenant.flow.step_completed.v1Any flow step transitioned forward25% (high-cardinality; sampled)
melmastoon.bff.tenant.flow.error_encountered.v1A flow step returned an error visible to the user100%
melmastoon.bff.tenant.handoff.consumed.v1Inbound handoff token verified + consumed100%
melmastoon.bff.tenant.locale.changed.v1Locale changed mid-session100%
melmastoon.bff.tenant.currency.changed.v1Display currency changed100%

All events carry tenantId (always non-null on this BFF), bookingDraftId when in flow, sessionId, requestId, traceId, and marketingAttribution.

Key events consumed

The BFF reads upstream services synchronously via REST. It consumes a small set of platform events solely for cache invalidation:

EventEffect
melmastoon.theme.published.v1Invalidate tenant bootstrap cache for that tenant
melmastoon.tenant.config_updated.v1Invalidate tenant bootstrap cache and slug → tenantId map for that tenant
melmastoon.tenant.suspended.v1Soft-block tenant slug; bootstrap returns 503 + MELMASTOON.TENANT.SUSPENDED
melmastoon.pricing.rate_plan.published.v1Invalidate cheapest-rate cache for affected (tenantId, propertyId)
melmastoon.inventory.allocation.committed.v1Invalidate light availability cache for (propertyId, dateRange)
melmastoon.iam.session.revoked.v1 (Phase 2+)Drop loyalty context for matching authenticated session

Upstream / downstream

Upstream (we read): tenant-service (slug resolution), theme-config-service (theme + flow config), property-service (room types + photos + policies), inventory-service (availability), pricing-service (quote), reservation-service (hold + confirm), payment-gateway-service (intent + return), iam-service (Phase 2+: loyalty), bff-consumer-service (handoff verify by HMAC of token).

Downstream (we publish for): analytics-service (funnel projection), notification-service (abandoned-cart Phase 2+, confirmation triggers), audit-service (handoff arrival trail).

Non-functional requirements

NFRTarget
/bootstrap latency p95< 350 ms (cache hit), < 800 ms cold
/availability latency p95< 600 ms (3-way fanout)
/quote latency p95< 250 ms (1 upstream proxy)
/hold latency p95< 600 ms
/confirm latency p95< 1.2 s (writes through gateway + reservation)
Confirmation page first-byte< 800 ms p95
Availability99.95% monthly (stricter than consumer BFF — money is at stake)
Cache hit ratio/bootstrap ≥ 95%; /availability ≥ 50%; /policies ≥ 99%
ReplicasMin 3 Cloud Run instances per region; auto-scale to 30 on flash sale
Multi-tenant rate limitper-tenant: 200 req/s/IP normalized; per-IP: 50 req/s; flash-sale override per tenant via flag
Booking-draft TTL30 min (matches pricing-service quote TTL)
Sync footprintNone — this BFF is never replicated to any client

Where to go next