====== Flight Booking BFF — Microservices Architecture & Implementation Guide ====== ===== Overview ===== This document describes the **Backend-for-Frontend (BFF)** microservices architecture for the Flight Booking. The BFF exposes secure APIs to the React TypeScript frontend and integrates with: * Token-based authentication * Third-party Flight Search APIs * Redis Cache (5-minute TTL) * Booking Service (MySQL) * Payment Gateway ===== Architecture Summary ===== ==== Key Services / Core Components ==== ^Service^Responsibility^Local Port| |**API Gateway / BFF (Node.js)** |Single entry point for UI|4000| |**Auth Service** |Issues & validates tokens|4001| |**Flight Service** |Calls supplier API + Redis cache|4002| |**Booking Service (MySQL)** |Stores booking data|4003| |**Payment Service** |Payment initiation + webhooks|4004| ==== Observability Stack ==== * **Pino** * **Pino-Http** * **OpenTelemetry** * **Cloud Logging (ELK / CloudWatch / Loki)** === Token Flow Diagram === [Frontend] → login → [Auth Service] \ ← JWT Token \ [Frontend] → API calls with Bearer Token → [BFF] Tokens are validated per request. ==== Flight Caching Strategy (Redis — 5 mins TTL) ==== * Supplier API calls are expensive and time-sensitive * Data is cached in Redis for **300 seconds** * UI retrieves additional details from cache (“Show More”) ==== Redis Key Naming Convention ==== flights:{origin}:{destination}:{date}:{userID}:{searchHash} Examples: flights:BOM:DXB:2025-03-21:user123:8f3c0e1a76 ===== Booking Workflow — Sequence ===== Search Flights → Cache Results Select Flight → Create booking Initiate Payment → Payment Gateway Webhook Received → Confirm booking Persist in MySQL → Send Confirmation ===== Node.js Folder Structure (Clean Architecture) ===== ^Layer^Purpose^Examples^Should define errors?| |**Domain** |Business rules|Entities, aggregates|**Business errors ** | |**Domain/Application** |Use-cases / orchestration|Services, DTOs|**App errors ** | |**Infrastructure** |Technical adapters|DB, Redis, HTTP|**Adapter-only errors ** | |**Interface/API** |Controllers|REST/GraphQL|no core errors| src/ ├ api/ │ ├ controllers/ │ ├ routes/ │ ├ middlewares/ │ └ errors/ ├ domain/ │ ├ entities/ │ ├ services/ │ ├ repositories/ │ └ errors/ ├ infrastructure/ │ ├ db/ │ ├ redis/ │ ├ logger/ │ ├ http/ │ └ errors/ └ tests/ ===== MySQL — Connection Pool ===== ts import mysql from 'mysql2/promise'; export const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, database: process.env.DB_NAME, password: process.env.DB_PASS, connectionLimit: 10, }); ===== DTO Design Example ===== json { "searchId": "abc123", "key": "flights:BOM:DXB:2025-03-21:hash", "flights": [ { "flightNo": "EK501", "origin": "BOM", "destination": "DXB", "duration": "3h 10m", "price": 450, "currency": "USD", "airline": "Emirates" } ] } ===== Redis Caching (5-Minute TTL) ===== ts const FLIGHT_TTL_SECONDS = 300; await redis.set(key, JSON.stringify(dto), { EX: FLIGHT_TTL_SECONDS, NX: true }); ==== Expiry UX ==== On Expiry → user resubmits search. ===== Swagger / OpenAPI Spec ===== Documented endpoints: * ''/api/flights/search'' * ''/api/flights/details'' openapi.yaml openapi: 3.0.3 info: title: Flight Search API version: 1.0.0 description: Backend-for-frontend service for searching & retrieving cached flight results. servers: - url: /api paths: /flights/search: post: summary: Search flights (cached for 5 minutes) tags: - Flights requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/FlightSearchRequest' responses: '200': description: Search results DTO content: application/json: schema: $ref: '#/components/schemas/FlightSearchResponse' '500': description: Internal server error /flights/details: get: summary: Retrieve flight details from Redis cache tags: - Flights parameters: - name: key in: query required: true schema: type: string description: Redis cache key returned by search API responses: '200': description: Cached flight result content: application/json: schema: $ref: '#/components/schemas/FlightSearchResponse' '404': description: Cache expired or key not found '500': description: Internal server error components: schemas: FlightSearchRequest: type: object required: - origin - destination - date properties: origin: type: string example: BOM destination: type: string example: DXB date: type: string format: date example: 2025-03-21 pax: type: integer example: 1 cabinClass: type: string example: ECONOMY FlightSearchResponse: type: object properties: searchId: type: string example: abc123 key: type: string example: flights:BOM:DXB:2025-03-21:8f3c0e1a76 flights: type: array items: $ref: '#/components/schemas/FlightItem' FlightItem: type: object properties: flightNo: type: string example: EK501 origin: type: string example: BOM destination: type: string example: DXB duration: type: string example: 3h 10m price: type: number example: 450 currency: type: string example: USD airline: type: string example: Emirates Swagger UI: app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc)); Swagger UI Setup (Express) ts import swaggerUi from 'swagger-ui-express'; \ import yaml from 'yamljs'; const swaggerDoc = yaml.load('./openapi.yaml'); \ app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc)); Docs available at: bash /docs ====== Jest Unit Tests ====== ===== Cache Hit / Miss ===== Covers: * Cache hit * Cache miss * TTL set ====== Centralized Logging & Observability Strategy ====== ==== Goals ==== * Trace every request end-to-end * JSON structured logs * Query by: * user * booking * redis key * trace id * Support dashboards & SLIs ===== Correlation ID Strategy ===== Every inbound HTTP request receives: ^Field^Purpose| |''requestId'' |Per-request UUID| |''traceId'' |OpenTelemetry Trace| |''spanId'' |Span identifier| |''userId'' |user id| |''bookingId'' |booking id| |''redisKey'' |Record Key| These flow through: Frontend → BFF → Redis → MySQL → Payment → Logs ===== Redis + MySQL Log Correlation ===== ^Attribute^Example| |redisKey|flights:BOM:DXB:2025-03-21:user123:8f3c0e1a76| |BookingId|''Bkg-9834233'' | |mysqlTx|UUID| |ttl|300 sec| ===== Logging Standards ===== ==== Log Levels ==== ^Level^Usage| |trace|dev deep debug| |debug|engineering debug| |info|business events| |warn|recoverable risk| |error|failure| |fatal|crash events| ==== Required JSON Fields ==== bash timestamp service env requestId traceId spanId userId BookingId redisKey durationMs status message ===== Pino Implementation ===== ==== Install ==== bash npm i pino pino-http pino-pretty ==== Logger Factory ( /src/infrastructure/logger/logger.ts ) ==== ts import pino from 'pino'; export const logger = pino({ level: process.env.LOG_LEVEL || 'info', base: { service: 'bff-gateway', env: process.env.NODE_ENV }, timestamp: pino.stdTimeFunctions.isoTime }); ==== HTTP Logger ( /src/infrastructure/logger/http-logger.ts ) ==== ts import pinoHttp from 'pino-http'; import { randomUUID } from 'crypto'; import { logger } from './logger'; export const httpLogger = pinoHttp({ logger, genReqId: (req) => req.headers['x-request-id']?.toString() || randomUUID(), customProps: (req) => ({ userId: req.user?.id, }) }); ==== Express Usage ==== ts app.use(httpLogger); app.use((req, res, next) => { req.log.info({ path: req.path }, 'request received'); next(); }); ==== Business Logging Example ==== ts req.log.info({ bkgId, redisKey, step: 'booking-confirmation' }, 'Booking confirmed'); ==== OpenTelemetry Tracing ==== Should attach: * traceId * spanId ==== Benefits ==== * distributed tracing * flame graphs * error attribution Code to link trace → logs → DB query ===== Security & Compliance ===== **Never log:** * passwords * card numbers * tokens * PII ==== Jest Logger Tests ==== ts describe('logger', () => { it('logs JSON structure', () => { const log = logger.info({ test: true }, 'hello'); expect(log).toBeDefined(); }); }); ====== Error Handling Strategy ====== **1. Client Errors (4xx)** ^Code^Meaning| |400|Bad Request/validation failure| |401|unauthorized| |404|cache expired| |409|idempotency conflict| |500|internal error| **2. Server Errors (5xx)** ^Type^Source^Action| |Dependency Failure|Redis/MySQL|Retry / Circuit Breaker| |Timeout|3rd-party API|Fail fast| |Internal Crash|unexpected bug|Log & alert| **3. Business Errors** ^Example^Handling| |Flight unavailable|409| |Payment declined|422| |Invalid state transition|409| ===== Recommended Placement ===== **1. ****Business / Domain Errors** When business rule is violeted - /src/domain/errors ts export class FlightUnavailableError extends Error { code = 'FLIGHT_UNAVAILABLE'; } **2. ****Application Errors** Use-case level\\ Validation / workflow / orchestration /src/application/errors ts export class ValidationError extends Error { status = 400; code = 'VALIDATION_ERROR'; } **3. ****Infrastructure Errors** ONLY for wrapping technical failures /src/infrastructure/errors ts export class RedisConnectionError extends Error { code = 'REDIS_CONNECTION_ERROR'; } BUT — these should usually be **mapped upward** into application-level errors. Example mapping: ts try { await redis.get(key); } catch(e) { throw new CacheUnavailableError(); } So the **API layer never leaks Redis internals.** ===== Error Flow (Best Practice) ===== Infrastructure error ⬇ wrap / map Application error ⬇ serialized to client HTTP response final structure should look like /src ┣ /domain ┃ ┗ /errors ← Business rule errors ┣ /application ┃ ┗ /errors ← Validation + workflow errors ┣ /infrastructure ┃ ┗ /errors ← Technical adapter errors (wrapped) ┗ /api (interfaces) ┗ error-middleware.ts ← converts to HTTP response **What to AVOID** Defining all errors in ''infrastructure'' \\ → couples business logic to technology Throwing raw DB/Redis errors upward\\ → leaks internal details Mixed styles (sometimes returning strings, sometimes errors)\\ → debugging nightmare