Backend Thực Chiến: 20 Tình Huống FE Cần Hiểu Về BE

Đây là tổng hợp các case study backend phổ biến trong dự án thật, đặc biệt phù hợp khi bạn làm FE nhưng muốn hiểu sâu hơn cách BE nên xử lý. Mỗi case gồm: Tình huống → Nguyên nhân → Giải pháp → Bài học.


1. Đăng nhập bằng Google (Google OAuth)

Tình huống

FE tích hợp Google Login, user click “Đăng nhập bằng Google” là xong. Nhưng BE cần xử lý rất nhiều thứ phía sau.

Nguyên nhân vấn đề

  • Token Google FE gửi lên có thể bị giả mạo
  • User login lần đầu cần tạo account mới
  • 1 email có thể đăng nhập bằng nhiều phương thức (Google, Facebook, email/password)
  • Access token hết hạn
  • Refresh token lưu không an toàn

Giải pháp

FE                         BE                        Google
 |--[Google ID Token]------>|                           |
 |                          |--[Verify token]---------->|
 |                          |<--[user info: email, id]--|
 |                          |
 |                          |-- Check user tồn tại chưa?
 |                          |   ├── Chưa có → Tạo mới
 |                          |   └── Có rồi → Map vào account cũ
 |                          |
 |                          |-- Phát hành JWT hệ thống
 |<--[access_token, refresh_token]--|

Nguyên tắc quan trọng:

  • BE phải verify Google token thay vì tin hoàn toàn FE gửi lên
  • Sau verify, hệ thống phát hành JWT riêng của hệ thống, không dùng token Google để authorize nội bộ
  • Refresh token nên: mã hóa khi lưu DB, có expiry, có cơ chế revoke khi logout

Bài học

Không nên để FE quyết định toàn bộ identity. BE luôn là lớp xác thực cuối cùng.


2. FE gọi 1 API nhưng BE phải gọi nhiều API khác

Tình huống

FE gọi /generate-report → BE phải gọi AI API, DB, dịch vụ chấm điểm, dịch vụ lưu file.

Nguyên nhân vấn đề

Nếu xử lý đồng bộ (synchronous):

  • Request rất lâu, dễ timeout
  • FE đóng tab → mất kết quả
  • Khó retry nếu một bước lỗi
  • Lịch sử chưa kịp lưu

Giải pháp: Async Processing

FE                     BE                      Worker
 |--[POST /generate]--->|                         |
 |                      |-- Tạo job record        |
 |                      |   status: PENDING        |
 |<--[taskId: abc123]----|                         |
 |                      |-- Đẩy job vào Queue ---->|
 |                                                 |-- Gọi AI API
 |                                                 |-- Gọi DB
 |                                                 |-- Lưu file
 |                                                 |-- Update status: SUCCESS
 |
 |--[GET /task/abc123]-->|
 |<--[status: SUCCESS]---|

Nên dùng gì?

Trường hợp Công cụ
Cần scale worker, retry, fail queue BullMQ / RabbitMQ / SQS
Event stream lớn Kafka
Task nhanh, nội bộ nhẹ Background job đơn giản

Bài học

Tách nghiệp vụ nặng ra khỏi vòng đời HTTP request. Tạo record lịch sử trước khi xử lý.


3. FE đóng tab giữa chừng làm mất lịch sử

Tình huống

User đang chờ AI generate, đóng tab trình duyệt. Khi mở lại không thấy kết quả đâu.

Nguyên nhân vấn đề

  • Xử lý nằm trong lifecycle của HTTP request
  • FE đóng tab → request bị hủy
  • Lịch sử không được lưu đầy đủ

Giải pháp: Task-based Architecture

Khi FE gửi request:
  ┌─────────────────────────────┐
  │  1. Tạo record NGAY lập tức │
  │     { taskId, userId,       │
  │       status: PENDING }     │
  └─────────────┬───────────────┘
                │
  ┌─────────────▼───────────────┐
  │  2. Đẩy vào Queue           │
  └─────────────┬───────────────┘
                │
  ┌─────────────▼───────────────┐
  │  3. Worker xử lý độc lập    │
  │     (không cần FE còn đó)   │
  └─────────────┬───────────────┘
                │
  ┌─────────────▼───────────────┐
  │  4. Update status khi xong  │
  │     SUCCESS / FAILED        │
  └─────────────────────────────┘

Vòng đời trạng thái job:

PENDING → PROCESSING → SUCCESS
                    └→ FAILED → RETRYING → SUCCESS
                                        └→ DEAD

Bài học

Đừng để business flow quan trọng phụ thuộc vào việc browser còn mở hay không.


4. Gọi AI API chậm, đắt, dễ fail

Tình huống

AI API timeout, rate limit, output không ổn định, chi phí cao, user spam nhiều request giống nhau.

Nguyên nhân vấn đề

  • AI API là external service, không kiểm soát được latency
  • Không có timeout → request treo mãi
  • Không có retry → mất kết quả vì lỗi tạm thời
  • Không có cache → cùng input gọi nhiều lần tốn tiền

Giải pháp

FE Request
    │
    ▼
[Idempotency Check] ──── Trùng? ──── Trả kết quả cũ
    │ Mới
    ▼
[Redis Queue]
    │
    ▼
[Worker]
    ├── Set timeout (VD: 30s)
    ├── Gọi AI API
    │     ├── OK → lưu kết quả + log usage
    │     └── Fail → Retry (max 3 lần, exponential backoff)
    │                └── Vẫn fail → Dead Letter Queue
    └── Update status

Nên log gì cho mỗi AI call?

{
  "jobId": "abc123",
  "model": "gpt-4o",
  "inputTokens": 512,
  "outputTokens": 1024,
  "duration_ms": 4200,
  "cost_usd": 0.023,
  "status": "success",
  "failReason": null
}

Bài học

Log đủ để sau này trả lời: “Vì sao job này chậm?”, “Vì sao tốn tiền bất thường?”, “Vì sao kết quả khác nhau?”


5. Tìm kiếm text trong DB chậm

Tình huống

Dùng LIKE %keyword% để tìm kiếm nhưng chậm, không hỗ trợ ranking, khó tìm tiếng Việt.

Nguyên nhân vấn đề

  • LIKE %text% không dùng được index
  • Không hỗ trợ fuzzy search, autocomplete, highlight
  • Khó search nhiều field cùng lúc

Giải pháp: Elasticsearch + DB

Write flow:
  ┌────────┐   save    ┌──────────┐   sync event  ┌───────────────┐
  │  FE    │ ────────> │  DB      │ ─────────────> │ Elasticsearch │
  └────────┘           │(source   │   (outbox/     │(search index) │
                       │ of truth)│  event-driven) └───────────────┘

Read flow:
  ┌────────┐  search   ┌───────────────┐  get detail  ┌──────────┐
  │  FE    │ ────────> │ Elasticsearch │ ────────────> │    DB    │
  └────────┘           └───────────────┘               └──────────┘

Xử lý eventual consistency:

  • Dùng outbox pattern: ghi event vào DB cùng transaction → worker đọc event → sync sang ES
  • Có cơ chế reindex khi ES bị lệch
  • Chấp nhận ES có thể chậm hơn DB vài giây

Bài học

Elasticsearch không phải source of truth. DB mới là nơi lưu dữ liệu gốc, ES chỉ là search index.


6. Retry khi gọi external API

Tình huống

Gọi API thanh toán, SMS, email, AI bị lỗi → retry bừa tạo nhiều lần thanh toán.

Nguyên nhân vấn đề

  • Retry không phân biệt lỗi tạm thời vs lỗi business
  • Không có giới hạn số lần retry
  • Không có backoff → spam liên tục vào API đang lỗi

Giải pháp: Exponential Backoff

Lần 1: gọi ngay
  Fail (timeout)
Lần 2: chờ 1s → gọi lại
  Fail (503)
Lần 3: chờ 2s → gọi lại
  Fail
Lần 4: chờ 4s → gọi lại
  OK ✓

Nếu vẫn fail sau max retry → Dead Letter Queue

Khi nào nên/không nên retry:

Nên Retry Không nên Retry
Timeout 400 Bad Request
502/503/504 401/403 Auth sai
Network fail tạm thời Lỗi business logic
Rate limit (sau cooldown) Dữ liệu input sai

Bài học

Retry thông minh: phân biệt lỗi tạm thời và lỗi vĩnh viễn. Luôn có max retry và dead-letter queue.


7. User bấm nhiều lần tạo trùng request

Tình huống

User click “Đặt hàng” nhiều lần → tạo 5 đơn hàng giống nhau. Hoặc mất mạng, FE resend → tạo 2 payment.

Nguyên nhân vấn đề

  • HTTP request có thể gửi nhiều lần do FE retry hoặc user double-click
  • BE không nhận ra đây là cùng một hành động

Giải pháp: Idempotency Key

FE:
  ┌──────────────────────────────────┐
  │ Sinh requestId = UUID            │
  │ Gửi cùng header:                 │
  │   Idempotency-Key: <uuid>        │
  └──────────────────────────────────┘

BE:
  ┌────────────────────────────────────────────┐
  │ Nhận request                               │
  │   ├── Kiểm tra key trong Redis/DB          │
  │   │     ├── Đã tồn tại → Trả kết quả cũ   │
  │   │     └── Chưa có → Xử lý + Lưu kết quả │
  └────────────────────────────────────────────┘

Các API nên có idempotency key:

  • Payment / Tạo đơn hàng
  • Generate AI result
  • Gửi email/SMS
  • Tạo tài khoản

Bài học

FE có thể gửi nhiều lần, BE phải xử lý đúng một lần. Idempotency là bảo hiểm cho critical action.


8. API chậm vì gọi DB quá nhiều

Tình huống

API load trang mất 3–5 giây. Debug ra thấy có 50–100 query DB cho một request.

Nguyên nhân vấn đề

  • N+1 query: load danh sách rồi loop gọi DB cho từng item
  • Query không có index
  • SELECT * thay vì chọn đúng field
  • Không paginate

Giải pháp

N+1 Problem:
  ✗ Sai:
     for user in users:
       orders = db.query("SELECT * FROM orders WHERE user_id = ?", user.id)

  ✓ Đúng:
     user_ids = [u.id for u in users]
     orders = db.query("SELECT * FROM orders WHERE user_id IN (?)", user_ids)
     # Gom lại thành 1 query

Index:
  ✗ Không có index:  WHERE email = 'x'  → Full table scan
  ✓ Có index:        CREATE INDEX idx_email ON users(email)  → O(log n)

Checklist tối ưu DB:

  • Thêm index cho field thường xuyên WHERE/JOIN
  • SELECT chỉ field cần dùng
  • Paginate thay vì load all
  • Tránh query trong vòng lặp
  • Batch query khi cần nhiều record

Bài học

Theo dõi số query mỗi request, slow query log. Tối ưu DB trước khi dùng cache.


9. Cache để giảm tải

Tình huống

Một số data bị gọi lặp đi lặp lại: config hệ thống, permission của user, danh sách tĩnh.

Nguyên nhân vấn đề

  • Mỗi request đều query DB dù dữ liệu không đổi
  • DB chịu tải không cần thiết

Giải pháp: Cache-aside Pattern với Redis

Request đến
    │
    ▼
[Check Redis cache]
    ├── HIT → Trả về ngay (nhanh)
    └── MISS
          │
          ▼
      [Query DB]
          │
          ▼
      [Lưu vào Redis với TTL]
          │
          ▼
      [Trả về kết quả]

Khi data thay đổi:
  [Update DB] → [Xóa / Update cache]

Chỉ cache khi:

  • Đọc nhiều, thay đổi ít
  • Chấp nhận dữ liệu lệch ngắn hạn (eventual consistency)
  • VD: config, permission, danh sách category, search phổ biến

Không cache khi:

  • Dữ liệu nhạy cảm, cần realtime (số dư tài khoản, tồn kho)

Bài học

Cache sai còn nguy hiểm hơn không cache. Luôn có TTL và cơ chế invalidate rõ ràng.


10. Quản lý trạng thái background job

Tình huống

Job đang chạy hay đã chết? Job fail có ai biết không? 2 worker cùng xử lý 1 job?

Nguyên nhân vấn đề

  • Không có trạng thái job rõ ràng
  • Không có lock → duplicate processing
  • Không có monitoring → job fail âm thầm

Giải pháp

Schema job table:

CREATE TABLE jobs (
  id          UUID PRIMARY KEY,
  type        VARCHAR(100),
  payload     JSONB,
  status      ENUM('PENDING','PROCESSING','SUCCESS','FAILED','RETRYING'),
  retry_count INT DEFAULT 0,
  error_msg   TEXT,
  created_at  TIMESTAMP,
  updated_at  TIMESTAMP
);

Cơ chế tránh duplicate:

Worker A lấy job:
  BEGIN TRANSACTION
    SELECT * FROM jobs WHERE status = 'PENDING' LIMIT 1 FOR UPDATE SKIP LOCKED
    UPDATE jobs SET status = 'PROCESSING' WHERE id = ?
  COMMIT

Worker B cũng lấy job:
  → SKIP LOCKED đảm bảo Worker B không lấy cùng job với Worker A

Bài học

BullMQ, RabbitMQ, SQS đều cần thêm lớp monitoring. Đẩy job vào queue chưa phải là “xong”.


11. File upload và xử lý file nặng

Tình huống

Upload file lớn qua app server làm server chậm. File chưa được validate. Tên file trùng nhau.

Nguyên nhân vấn đề

  • Upload trực tiếp qua app server → chiếm băng thông, bộ nhớ
  • Xử lý file nặng (resize ảnh, parse PDF) trong sync request → timeout

Giải pháp: Presigned URL + Background Job

FE                    BE                    S3 (Object Storage)
 │                     │                           │
 │--[Request upload]--->│                           │
 │                     │-- Tạo Presigned URL ------>│
 │<--[presignedUrl]-----|                           │
 │                                                  │
 │--[Upload trực tiếp file]------------------------->│
 │                                                  │
 │--[Notify BE: upload done]-->│                    │
 │                             │-- Tạo job xử lý   │
 │                             │   (validate, scan) │
 │<--[OK, đang xử lý]----------|                    │

Validate file:

  • MIME type (không tin phần mở rộng)
  • Size limit
  • Scan virus (nếu cần)
  • Rename theo UUID để tránh trùng

Bài học

App server không nên giữ file lâu hoặc xử lý file nặng trong request sync.


12. Logging và tracing không đủ

Tình huống

Lỗi production nhưng không biết: user nào bị, request nào lỗi, lỗi từ đâu, mất bao lâu ở bước nào.

Nguyên nhân vấn đề

  • Log thiếu context (chỉ log “Error occurred”)
  • Không có request ID → không trace được flow
  • Log không có cấu trúc → khó query

Giải pháp: Structured Logging + TraceId

Mỗi request vào hệ thống:
  ┌───────────────────────────────────────────┐
  │ Gắn traceId (UUID) vào mọi log của request│
  └───────────────────────────────────────────┘

Log format (JSON):
{
  "traceId": "abc-123",
  "userId": "user-456",
  "endpoint": "POST /generate-report",
  "duration_ms": 3200,
  "status": 200,
  "errorCode": null,
  "timestamp": "2026-04-18T10:00:00Z"
}

Với lỗi:
{
  "traceId": "abc-124",
  "step": "call_ai_api",
  "errorCode": "TIMEOUT",
  "retryCount": 2,
  "duration_ms": 30000
}

Tools phổ biến:

  • ELK Stack (Elasticsearch + Logstash + Kibana)
  • Grafana + Loki
  • Datadog / New Relic

Bài học

Code tốt mà không có log tốt thì production rất khó cứu. Đây là phần nhiều team làm thiếu nhất.


13. Phân quyền chưa chặt (Authorization)

Tình huống

FE ẩn nút Delete cho user thường, nhưng user biết endpoint vẫn gọi được API Delete của người khác.

Nguyên nhân vấn đề

  • Chỉ kiểm tra quyền ở FE (ẩn nút)
  • BE không validate: user có quyền trên resource này không?

Giải pháp: RBAC

Mỗi API request BE phải kiểm tra:
  1. Authentication: token hợp lệ?
  2. Authorization: user có quyền với action này?
  3. Resource ownership: resource này thuộc user này không?

Ví dụ:
  DELETE /orders/:id
    ├── Có token? → ✓
    ├── Role = ADMIN hoặc là chủ order? → ✓
    └── Order thuộc user này? → ✓

RBAC vs ABAC:

  RBAC ABAC
Phân quyền theo Role (admin, staff, user) Attribute (dept, time, location)
Phù hợp Nghiệp vụ đơn giản, rõ ràng Nghiệp vụ phức tạp, nhiều điều kiện

Bài học

FE ẩn nút chỉ là UX. BE check quyền mới là bảo mật thật sự.


14. Đồng bộ dữ liệu giữa nhiều service

Tình huống

Tạo user → gửi email → tạo profile → sync CRM. Nếu CRM lỗi, dữ liệu giữa các service bị lệch.

Nguyên nhân vấn đề

  • Không thể dùng DB transaction cho nhiều service
  • Nếu một bước fail giữa chừng → trạng thái inconsistent

Giải pháp: Outbox Pattern + Saga

Outbox Pattern (đảm bảo event không bị mất):
  ┌──────────────────────────────────────────┐
  │ Trong cùng 1 DB transaction:             │
  │   INSERT INTO users ...                  │
  │   INSERT INTO outbox_events              │
  │     (type: USER_CREATED, payload: ...)   │
  └──────────────────────────────────────────┘
         │
         ▼
  [Outbox Worker] đọc event và publish
         │
         ├──> Email Service (gửi email)
         ├──> Profile Service (tạo profile)
         └──> CRM Service (sync)

Nếu một service fail → retry riêng, không ảnh hưởng bước khác

Bài học

Không phải lúc nào cũng cần microservice. Nhưng nếu đã nhiều service, phải nghĩ đến consistency.


15. Không kiểm soát Rate Limit

Tình huống

Bot spam login, user spam OTP, AI generate bị lạm dụng, search bị DDoS.

Nguyên nhân vấn đề

  • Không giới hạn số request từ một IP/user trong khoảng thời gian

Giải pháp

Rate Limit theo nhiều chiều:
  - Theo IP: 100 req/phút
  - Theo User: 10 AI generate/giờ
  - Theo Endpoint: /otp tối đa 5 lần/10 phút

Sliding window với Redis:
  KEY: rate_limit:{userId}:{endpoint}:{minute}
  INCR → nếu > limit → reject 429

Circuit Breaker:
  Nếu downstream đang lỗi liên tục
    → Tự động ngắt, trả lỗi ngay
    → Không spam request vào service đang chết

Bài học

Rate limit bảo vệ cả hệ thống lẫn user. Đặt ở API gateway hoặc middleware, không phải từng service.


16. API versioning và backward compatibility

Tình huống

BE “sửa nhỏ thôi” đổi tên field userName thành fullName → app mobile bản cũ vỡ.

Nguyên nhân vấn đề

  • Không có versioning
  • Không nghĩ đến client cũ vẫn đang chạy

Giải pháp

URL versioning:
  /api/v1/users   ← Giữ nguyên, không đổi
  /api/v2/users   ← Version mới với schema mới

Hoặc header versioning:
  Accept: application/vnd.api+json;version=2

Quy tắc backward compatible:
  ✓ Thêm field mới (optional)
  ✓ Thêm endpoint mới
  ✗ Đổi tên field đang dùng
  ✗ Đổi kiểu dữ liệu field
  ✗ Xóa field

Bài học

Nhiều lỗi production đến từ BE “sửa nhỏ thôi” nhưng FE cũ không chịu được.


17. Validation đầu vào không kỹ

Tình huống

User gửi email=”” hoặc age=”abc” hoặc text dài 10MB → server crash hoặc lưu data rác vào DB.

Nguyên nhân vấn đề

  • Chỉ validate ở FE, không validate lại ở BE
  • Không sanitize input

Giải pháp

Các lớp validation:
  FE Validation    → UX tốt hơn, phản hồi nhanh
  BE Validation    → Bảo mật thật sự, không thể bỏ qua

BE dùng DTO / Schema validation:
  // NestJS ví dụ
  class CreateUserDto {
    @IsEmail()
    email: string;

    @IsString()
    @MaxLength(100)
    name: string;

    @IsInt()
    @Min(0) @Max(150)
    age: number;
  }

Sanitize:
  - Trim whitespace
  - Escape HTML để chống XSS
  - Giới hạn length để chống DoS

Bài học

Không bao giờ tin FE validate là đủ. BE là last line of defense.


18. Xử lý transaction không chặt

Tình huống

Trừ tiền xong nhưng fail khi tạo order → mất tiền nhưng không có hàng.

Nguyên nhân vấn đề

  • Update nhiều bảng không trong cùng transaction
  • Một bước fail → dữ liệu bị lệch

Giải pháp

Dùng DB transaction cho các thao tác liên quan:

BEGIN TRANSACTION
  -- Trừ balance
  UPDATE wallets SET balance = balance - 100 WHERE user_id = ?
  -- Tạo order
  INSERT INTO orders (user_id, amount, status) VALUES (?, 100, 'PAID')
  -- Ghi lịch sử
  INSERT INTO transactions (user_id, type, amount) VALUES (?, 'DEBIT', 100)
COMMIT  -- Tất cả thành công
-- hoặc
ROLLBACK  -- Nếu bất kỳ bước nào fail → không có gì thay đổi

Nhiều service: Dùng Saga pattern thay vì distributed transaction.

Bài học

Atomic operations quan trọng nhất với financial data. Thà fail toàn bộ còn hơn dữ liệu lệch.


19. Gửi email/notification làm chậm API

Tình huống

API tạo đơn hàng mất 3 giây vì phải đợi gửi email xác nhận.

Nguyên nhân vấn đề

  • Gọi email service đồng bộ trong request
  • Email service chậm hoặc lỗi → API chậm hoặc fail theo

Giải pháp

✗ Sai:
  POST /orders
    → Tạo order
    → Gọi email service (đợi 2s)
    → Trả response

✓ Đúng:
  POST /orders
    → Tạo order
    → Đẩy job "send_email" vào Queue
    → Trả response NGAY (< 100ms)

  [Background Worker]
    → Lấy job từ Queue
    → Gửi email
    → Retry nếu fail

Bài học

Request chính chỉ cần đảm bảo nghiệp vụ chính thành công. Side effects (email, SMS, log analytics) chạy nền.


20. Monitoring và alert thiếu

Tình huống

Service chết từ 2 giờ sáng, đến 8 giờ sáng mới biết vì user báo.

Nguyên nhân vấn đề

  • Không có monitoring
  • Không có alert tự động

Giải pháp

Cần theo dõi:
  Infrastructure:  CPU, RAM, Disk, Network
  Application:     API latency, Error rate, Request/s
  Queue:           Queue length, Processing rate, Failed jobs
  External:        AI API timeout rate, Email delivery rate

Alert khi:
  Error rate > 5% trong 5 phút
  API p95 latency > 2s
  Queue pending > 1000 jobs
  Failed jobs tăng đột biến
  AI API fail liên tục

Stack phổ biến:
  Prometheus + Grafana   ← metrics & dashboard
  ELK / Loki             ← logs
  PagerDuty / OpsGenie   ← alert & on-call
  Datadog / New Relic    ← all-in-one (tốn tiền hơn)

Bài học

Monitoring không phải “nice to have”. Không có alert = mù với production.


Tóm tắt theo nhóm

Nhóm Auth & Security

Case Giải pháp
Google Login OAuth 2.0 + BE verify + JWT hệ thống
Phân quyền RBAC/ABAC ở BE, không tin FE
Validation DTO/Schema validation ở BE
Rate limit Sliding window Redis, Circuit breaker

Nhóm Async Processing

Case Giải pháp
API tạo nhiều tác vụ nặng Queue + Worker + Polling/WebSocket
FE đóng tab mất lịch sử Tạo record trước, worker chạy độc lập
Retry an toàn Exponential backoff + Dead Letter Queue
User bấm nhiều lần Idempotency key
Email làm chậm API Background job

Nhóm Dữ liệu

Case Giải pháp
API chậm do DB Index + batch query + tránh N+1
Cache Cache-aside + TTL + invalidate rõ ràng
Tìm kiếm Elasticsearch + outbox sync
Transaction DB transaction, Saga pattern
Sync nhiều service Outbox pattern + Event-driven

Nhóm External Integration

Case Giải pháp
AI API Queue + timeout + retry + cost logging
File upload Presigned URL + S3 + background job
API versioning /v1, /v2 + backward compatible rules

Nhóm Vận hành

Case Giải pháp
Debug production Structured log + traceId
Job management Job table với status lifecycle
Monitoring Prometheus/Grafana + Alert

Cách mô tả kinh nghiệm trong CV/phỏng vấn

✓ Implemented Google OAuth login with backend token verification
  and internal JWT issuance to prevent token forgery.

✓ Designed asynchronous processing for long-running AI tasks using
  Redis queue/background workers — preserving history even when
  client disconnected mid-request.

✓ Built task-based workflow with persisted job status
  (pending → processing → success/failed) for AI-related operations.

✓ Integrated external AI APIs with timeout, retry (exponential backoff),
  idempotency, and structured logging for cost observability.

✓ Applied Elasticsearch for full-text search with outbox pattern
  to sync data from PostgreSQL to search index.

✓ Used Redis for caching and rate limiting to reduce DB load
  and prevent API abuse.

✓ Implemented idempotency and retry-safe patterns for critical APIs
  (payment, order creation, AI generation).