====== 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