Base.vn Prep

2026-05-19 · Middle-level

BACKEND

Backend Deep Dive

9 topic. Stack focus: NestJS + Prisma + Postgres + Redis + BullMQ. Bonus: Rust case study từ sales_agent.

1 · Client-Server Architecture

HTTP lifecycle: DNS → TCP → TLS → HTTP → response → close/keep-alive

REST

CRUD chuẩn, HTTP cache theo URL. Default pick.

GraphQL

Client chọn field. Trade: cache khó, dễ N+1.

WebSocket / SSE

Bidirectional / server push. Chat, presence, live.

Stateless: session vào Redis hoặc JWT → scale ngang dễ.

Pitfall: WS cho mọi real-time → tốn socket. Polling 30s đủ cho notification. Quên CORS + idempotency POST.

2 · Database Design (PostgreSQL)

Pick: SQL khi schema ổn định + JOIN/transaction. NoSQL khi schema linh hoạt + scale write cao.

-- Hot query: WHERE user_id = ? ORDER BY created_at DESC
CREATE INDEX idx_tasks_user_created ON tasks(user_id, created_at DESC);

-- Composite index thứ tự: equality TRƯỚC, range SAU
-- Bad:  ON tasks(created_at, user_id) — dùng user_id miss index
-- Good: ON tasks(user_id, created_at)

Normalization (3NF)

Không trùng data. Write nhanh, read cần JOIN. Default OLTP.

Denormalization

Duplicate cho read speed. Khi read » write (analytics).

Transaction: Multi-table write → BẮT BUỘC transaction. Prisma: prisma.$transaction([...])

Pitfall: Index quá nhiều → chậm write. Soft delete quên WHERE deleted_at IS NULL → leak. UUID v4 PK → fragment index → ULID/UUIDv7.

3 · Caching Strategies (Redis)

CDN

Static, edge. Cloudflare/Vercel.

App (Redis)

Session, hot query, rate limit, queue.

DB query cache

Prepared statement, materialized view.

// Cache-aside — default
async function getUser(id: string) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);
  const user = await db.user.findUnique({ where: { id } });
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300);
  return user;
}

// Invalidate khi update
async function updateUser(id: string, data) {
  const user = await db.user.update({ where: { id }, data });
  await redis.del(`user:${id}`); // KHÔNG set lại — race condition
  return user;
}

Strategy: TTL (simple, eventual) · Event-based (chính xác, phức tạp) · Write-through (consistent, slow write).

Pitfall: Cache stampede — 1000 req cùng miss → đánh DB. Fix: lock (single-flight) + stale-while-revalidate. Cache key thiếu tenant_id → leak cross-tenant.

4 · Load Balancing & Horizontal Scaling

   Internet
      │
      ▼
  [Load Balancer]   ── Round-robin / Least-conn / IP-hash
   │   │   │
   ▼   ▼   ▼
  app app app       ── Stateless instances (Docker/K8s)
   │   │   │
   └───┼───┘
       ▼
  [Redis]  [Postgres replica · read]  [Postgres primary · write]

Read replica: Tách read sang replica, write vào primary. Read throughput 3-5×. Trade: replication lag.

Pitfall: Sticky session → scale khó. Dùng Redis session. Quên health check → LB route vào instance chết.

5 · Async Processing (BullMQ)

Nguyên tắc: Đừng block HTTP. User click "Send 10k email" → trả 202 Accepted, push job.

const emailQueue = new Queue('email', { connection: redis });

@Post('campaigns/:id/send')
async sendCampaign(@Param('id') id: string) {
  await emailQueue.add('send-batch', { campaignId: id }, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 5000 }, // 5s, 25s, 125s
  });
  return { status: 'queued' }; // 202
}

new Worker('email', async (job) => {
  await sendEmailBatch(job.data.campaignId);
}, { connection: redis, concurrency: 10 });

Pitfall: Job không idempotent → retry tạo duplicate. Dedup key. Worker crash giữa chừng → state machine cho long job.

6 · Multi-tenancy Patterns ★ critical Base.vn

A · Shared DB / Shared Schema (column-based)

Mỗi table có tenant_id. Query filter.

✓ Cheap, scale dễ   ✗ Leak risk nếu quên WHERE

B · Shared DB / Separate Schema

Mỗi tenant 1 PG schema. SET search_path.

✓ Isolation tốt hơn   ✗ Migration n × schema

C · Separate DB per tenant

Mỗi tenant 1 DB. Enterprise, regulated.

✓ Strict isolation, custom backup   ✗ Cost cao

Enforce: Middleware inject tenantId từ JWT. Postgres RLS làm safety net.

Pitfall: Admin query quên scope → leak. JOIN qua table khác tenant. Cache key thiếu tenant_id.

7 · REST API + JWT Auth

[Login]   POST /auth/login → access (15m, body) + refresh (7d, httpOnly cookie)
[Auth]    GET /tasks  Authorization: Bearer <access> → verify sig + exp
[Refresh] 401 → POST /auth/refresh (cookie auto-sent) → rotate

Access token

Short (5-15m). Memory (NOT localStorage).

Refresh token

Long (7-30d). httpOnly + Secure + SameSite=Lax cookie. Rotate on use.

REST conventions: POST tạo · GET đọc · PUT replace · PATCH partial · DELETE. Status: 200/201/204 · 400 bad · 401 unauth · 403 forbidden · 404 not found · 409 conflict · 422 validation · 5xx.

Pitfall: JWT lưu localStorage → XSS đọc được. Không refresh rotation → hijack forever. Stack trace lộ secret trong error.

8 · Prisma ORM

// Transaction — atomic multi-table
await prisma.$transaction(async (tx) => {
  const order = await tx.order.create({ data: { userId, total } });
  await tx.stock.update({
    where: { productId },
    data: { quantity: { decrement: 1 } },
  });
  await tx.invoice.create({ data: { orderId: order.id } });
});

// Avoid N+1 — include / select
const tasks = await prisma.task.findMany({
  where: { tenantId },
  include: { assignee: true, comments: { take: 3 } },
}); // 1 query JOIN, không phải N

Pitfall: Quên include → N+1. Transaction dài → lock table. Migration conflict khi merge branch. Schema thay đổi nhưng quên regen client.

9 · 🦀 Backend in Rust — sales_agent

Real codebase

Talking point: "patterns transfer across languages". Module structure (handlers/repository/models) chạy được trên NestJS, FastAPI, Axum. Stack: Axum + sqlx + Postgres + Redis + JWT.

▶ Module structure (≡ NestJS feature module)

backend/src/
├── auth/        { mod, models, handlers, jwt, middleware }
├── drafts/      { mod, models, handlers, repository, cleanup }
├── leads/       { mod, models, handlers, repository }
├── error.rs     ← unified AppError enum
├── db.rs        ← PgPool init
└── routes.rs    ← wire handlers to paths

→ Map: auth/ = AuthModule · repository.rs = AuthRepository · handlers.rs = AuthController.

▶ JWT extractor (≡ NestJS Guard)

// auth/middleware.rs
#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthUser
where S: Send + Sync + AsRef<AppState> {
    type Rejection = AppError;
    async fn from_request_parts(parts: &mut Parts, state: &S)
        -> Result<Self, Self::Rejection> {
        let header = parts.headers.get("Authorization")
            .and_then(|v| v.to_str().ok())
            .ok_or_else(|| AppError::Unauthorized("Missing auth header".into()))?;
        let token = header.strip_prefix("Bearer ")
            .ok_or_else(|| AppError::Unauthorized("Invalid format".into()))?;
        let claims = decode_token(token, &state.as_ref().jwt_secret)?;
        Ok(AuthUser { user_id: claims.sub })
    }
}

→ Extractor Axum ≡ Guard+decorator NestJS ≡ Depends() FastAPI. Inject authed user, fail-fast nếu invalid.

▶ Repository + sqlx

// drafts/repository.rs
pub async fn create(pool: &PgPool, user_id: Uuid, source: &DraftSource, ...)
    -> Result<DraftContact, AppError>
{
    let draft = sqlx::query_as::<_, DraftContact>(r#"
        INSERT INTO draft_contacts (user_id, source, extracted_data,
            confidence_data, raw_input, telegram_chat_id, expires_at)
        VALUES ($1, $2, $3, $4, $5, $6, NOW() + INTERVAL '24 hours')
        RETURNING *
    "#)
    .bind(user_id).bind(source).bind(extracted_data)
    .fetch_one(pool).await?;
    Ok(draft)
}

→ Y hệt Prisma repository. Compile-time SQL check (sqlx macro) ≡ TS types từ Prisma. Same benefit, khác syntax.

▶ Async cleanup (không cần Redis queue)

// drafts/cleanup.rs — xóa draft hết hạn mỗi giờ
pub fn start_cleanup_task(pool: PgPool) {
    tokio::spawn(async move {
        let mut interval = tokio::time::interval(Duration::from_secs(3600));
        loop {
            interval.tick().await;
            match repository::delete_expired(&pool).await {
                Ok(count) if count > 0 => tracing::info!("Deleted {count} drafts"),
                Err(e) => tracing::error!("Cleanup error: {e}"),
                _ => {}
            }
        }
    });
}

→ Spawn cho periodic cleanup, metric. BullMQ khi cần retry, scale worker, observability. Đừng over-engineer.

▶ Unified error → HTTP mapping

// error.rs
pub enum AppError {
    BadRequest(String),         // 400
    Unauthorized(String),       // 401
    NotFound(String),           // 404
    Conflict(String),           // 409
    ValidationError(String),    // 422
    RateLimited(String),        // 429
    InternalError(anyhow::Error), // 500 — hide details
}
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, code, msg) = match &self { /* enum → tuple */ };
        (status, Json(json!({ "error": { "code": code, "message": msg }}))).into_response()
    }
}

→ NestJS có HttpException, FastAPI HTTPException. Rust dùng thiserror derive. Handler chỉ ? propagate.

▶ Migration với ENUM + index

-- migrations/002_create_contacts.sql
DO $$ BEGIN
    CREATE TYPE contact_source AS ENUM (
        'card_scan', 'text_input', 'telegram',
        'linkedin_search', 'linkedin_connection', 'manual'
    );
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;

CREATE TABLE IF NOT EXISTS contacts (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    source       contact_source NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_contacts_full_name_company
  ON contacts (full_name, company_name);

DO $$ ... duplicate_object = idempotent migration. ENUM rẻ hơn varchar + check constraint.

💡 Pitch sales_agent trong interview

"Em build 1 backend Rust với Axum + sqlx — hệ thống scan name card, parse contact, push lên HubSpot. Pattern giống NestJS: module-per-feature, repository tách khỏi handler, error enum map sang HTTP status qua trait. Rust ép em handle Option/Result tường minh, ít bug null. Em pick Rust vì có job xử ảnh + AI nặng — tokio handle concurrent tốt với footprint nhỏ."

← Phase 3 Next: Frontend →