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
BookingDraftfor UX ergonomics. - Honour the handoff token minted by
bff-consumer-serviceso 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
- Tenant resolution — resolve
(subdomain | custom domain | path prefix tenantSlug)→tenantIdviatenant-servicewith aggressive cache (TTL 1 h, invalidated onmelmastoon.tenant.config_updated.v1andmelmastoon.theme.published.v1). - Bootstrap composition —
GET /bff/tenant-booking/v1/{tenantSlug}/bootstrapreturns{ tenant, theme, flowConfig, locales, currencies, paymentMethods, policies, handoffPayload? }fromtheme-config-service+tenant-service. Embeds CSP nonce + design-token CSS sheet for first-paint. - 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). - Quote / hold orchestration —
POST /quoteproxies topricing-service;POST /holdproxies toreservation-serviceand persists a tenant-scopedBookingDraftblob in Memorystore (TTL 30 min) for UX continuity. - Guest-details capture — accept and validate guest fields; persist into
BookingDraft; pass through on/confirmtoreservation-service(which is the source of truth forGuest). - Payment intent dance —
POST /payment-intentcallspayment-gateway-service; the BFF holds the redirect URL and the local return path (/return). On return, BFF verifies state + invokesreservation-service /confirm. - Confirmation page composition —
GET /confirmation/{reservationId}composes the post-confirm view-model: confirmed reservation, folio summary, key-credential placeholder if not yet issued, post-stay info. - Abandoned-cart marker (Phase 2+) — emit
draft.abandoned.v1after 30 min of inactivity; consumed bynotification-servicefor opt-in email/SMS recovery. - Themed error rendering — every error response carries the tenant's
theme.errorPalettereference so the SPA renders branded errors. - 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)
| Aggregate | Cardinality | Purpose | Identity prefix |
|---|---|---|---|
TenantBootstrap (cache) | per tenant | Composed bootstrap blob (theme + flowConfig + locales + currencies + paymentMethods) | (composite) |
BookingDraft | per bdr_ | Short-lived booking draft (TTL 30 min) — search params, selected room, quote, guest details, payment intent reference | bdr_ |
BookingFlowState | child of BookingDraft | Flow step machine (search → select → quote → hold → details → payment → confirm) | (n/a) |
PaymentSelection | child of BookingDraft | Payment method + redirect outcome marker | (n/a) |
GuestProfile (booking-time only) | child of BookingDraft | Guest fields collected during booking; written through to reservation-service on confirm; not retained by BFF | (n/a) |
LoyaltyContext (Phase 2+) | per session | Loyalty hint passed to pricing/quote | (n/a) |
MarketingAttribution | per session | UTM + handoff campaign attribution | (n/a) |
BookingHandoffArrival | short-lived | Verified 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})
| Method | Path | Purpose |
|---|---|---|
GET | /bootstrap | Tenant bootstrap (theme + flowConfig + locales + currencies + paymentMethods) |
GET | /availability | Per-room-type availability + price snapshot for stay window |
GET | /properties/{propertyId}/rooms | Room types + photos for the property |
POST | /quote | Issue PriceQuote via pricing-service |
POST | /hold | Create reservation hold via reservation-service (returns bdr_ and TTL) |
PATCH | /draft/{draftId} | Update guest details / preferences on the BookingDraft |
POST | /draft/{draftId}/payment-intent | Create PaymentIntent via payment-gateway-service |
POST | /draft/{draftId}/return | Payment gateway redirect-return; confirms reservation if successful |
POST | /draft/{draftId}/confirm | Confirm reservation via reservation-service (alternative path for non-redirect rails) |
GET | /confirmation/{reservationId} | Post-confirm view-model |
POST | /handoff/consume | Validate inbound bff-consumer handoff token; primes session |
POST | /session/locale | Set locale preference for the session |
POST | /session/currency | Set display currency for the session |
GET | /session | Read sanitized session |
GET | /policies | Tenant-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
| Event | Trigger | Sample rate |
|---|---|---|
melmastoon.bff.tenant.bootstrap.served.v1 | Bootstrap successfully composed | 100% (one per session, debounced) |
melmastoon.bff.tenant.booking.draft.created.v1 | First POST /hold succeeds for a session | 100% |
melmastoon.bff.tenant.booking.draft.abandoned.v1 | Draft inactive > 30 min and not converted | 100% |
melmastoon.bff.tenant.booking.draft.converted.v1 | Draft → confirmed reservation | 100% |
melmastoon.bff.tenant.payment_intent.created.v1 | PaymentIntent created via gateway | 100% |
melmastoon.bff.tenant.confirmation.viewed.v1 | /confirmation/{id} rendered | 100% |
melmastoon.bff.tenant.flow.step_completed.v1 | Any flow step transitioned forward | 25% (high-cardinality; sampled) |
melmastoon.bff.tenant.flow.error_encountered.v1 | A flow step returned an error visible to the user | 100% |
melmastoon.bff.tenant.handoff.consumed.v1 | Inbound handoff token verified + consumed | 100% |
melmastoon.bff.tenant.locale.changed.v1 | Locale changed mid-session | 100% |
melmastoon.bff.tenant.currency.changed.v1 | Display currency changed | 100% |
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:
| Event | Effect |
|---|---|
melmastoon.theme.published.v1 | Invalidate tenant bootstrap cache for that tenant |
melmastoon.tenant.config_updated.v1 | Invalidate tenant bootstrap cache and slug → tenantId map for that tenant |
melmastoon.tenant.suspended.v1 | Soft-block tenant slug; bootstrap returns 503 + MELMASTOON.TENANT.SUSPENDED |
melmastoon.pricing.rate_plan.published.v1 | Invalidate cheapest-rate cache for affected (tenantId, propertyId) |
melmastoon.inventory.allocation.committed.v1 | Invalidate 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
| NFR | Target |
|---|---|
/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 |
| Availability | 99.95% monthly (stricter than consumer BFF — money is at stake) |
| Cache hit ratio | /bootstrap ≥ 95%; /availability ≥ 50%; /policies ≥ 99% |
| Replicas | Min 3 Cloud Run instances per region; auto-scale to 30 on flash sale |
| Multi-tenant rate limit | per-tenant: 200 req/s/IP normalized; per-IP: 50 req/s; flash-sale override per tenant via flag |
| Booking-draft TTL | 30 min (matches pricing-service quote TTL) |
| Sync footprint | None — this BFF is never replicated to any client |
Where to go next
- Implementation-grade detail:
services/bff-tenant-booking-service/SERVICE_OVERVIEW.mdand the rest of the 17-doc bundle. - Booking saga detail:
docs/03-microservices/reservation-service.mdand bundle. - Theme bootstrap shape:
docs/03-microservices/theme-config-service.md. - Payments redirect rails:
docs/10-payments-architecture.md. - Handoff handshake spec:
services/bff-consumer-service/SECURITY_MODEL.mdandservices/bff-tenant-booking-service/SECURITY_MODEL.md. - BFF contracts in the cross-cutting API doc:
docs/05-api-design.md§9.2.