Table of Contents
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
