diff --git a/learn/senior-web-dev/00-nang-luc-senior.md b/learn/senior-web-dev/00-nang-luc-senior.md new file mode 100644 index 0000000..849f84c --- /dev/null +++ b/learn/senior-web-dev/00-nang-luc-senior.md @@ -0,0 +1,70 @@ +# L0 — Mô hình năng lực senior (thang đo) + +Dùng file này để tự chấm. Chấm 0–3: 0 = chưa biết, 1 = từng dùng, 2 = giải thích được, 3 = từng dạy lại cho người khác. + +## 0.1 Kỹ thuật: sâu một stack, rộng mọi tầng + +Định nghĩa: với NestJS/Next.js/TS, bạn trả lời được "cái này hoạt động thế nào" ở mọi tầng (runtime → HTTP → DB). Không cần expert mọi thứ, cần không im lặng ở tầng nào. + +Ví dụ tự test — chuỗi câu hỏi xuyên tầng: +``` +"Câu await fetch() trong service Nest chạy ở đâu?" +→ TS: promise suspension (L2) +→ Node: microtask queue (L1) +→ HTTP: keep-alive socket (L3) +→ Prisma: pool connection (L9) +``` +Trả lời được cả 4 = đạt 0.1. + +## 0.2 Phán đoán trade-off + +Định nghĩa: mỗi lựa chọn kỹ thuật nói được 2 cột: được gì / mất gì. + +Ví dụ: JWT vs session +| | JWT | Session | +|---|---|---| +| Được | không cần lookup mỗi request | logout tức thì, revocation dễ | +| Mất | thu hồi khó (phải blacklist) | 1 round-trip Redis mỗi request | +Senior kết luận bằng ngữ cảnh: "internal tool, logout gấp → session. Mobile stateless → JWT + refresh rotation." + +## 0.3 Hệ quả dài hạn + +Định nghĩa: thấy quyết định hôm nay ở điểm 12–24 tháng. + +Ví dụ: chọn `enum` trong Postgres: +- hôm nay: sạch, rẻ +- 1 năm sau: thêm giá trị = migration lock table lớn, rollback migration khó +- hệ quả: nhiều team chốt "enum ở app layer + check constraint" — quyết định dựa trên chi phí tương lai, không phải sự tiện hôm nay. + +## 0.4 Communicate risk + +Ba câu senior phải nói được và nói sớm: +1. "Con số này là estimate, sai số ±30%, em cần thêm X để siết lại." +2. "Em chưa làm cái này bao giờ. Em ước lượng dựa trên Y, nhưng cần người review." +3. "Cái này nổ to nếu Q tăng gấp 10. Đề xuất chặn bằng rate limit trước, refactor sau." + +Ví dụ anti-pattern (mid hay làm): im lặng nhận deadline → 2 tuần trước release mới báo trễ. Senior báo rủi ro ở ngày thứ 2. + +## 0.5 Nâng người khác + +Đơn vị đo không phải "em giúp được nhiều người" mà là "codebase dễ vào hơn cho người sau". + +Ví dụ review comment: +- ❌ mid: "Sai rồi, sửa đi." +- ✅ senior: "Guard này chạy DB query mỗi request cho route public. Route /health bị chậm theo. Đề xuất: bỏ guard khỏi public route, hoặc cache 60s.case." +Kèm: ADR + CONTEXT.md glossary (org bạn đang làm sẵn — dùng nó làm công cụ). + +## 0.6 Ownership + +Định nghĩa: vào = yêu cầu mơ hồ ("khách phàn nàn trang chậm"), ra = hệ thống chạy + có dashboard + có alert + có doc + người khác vận hành được không cần bạn. + +Ví dụ checklist tự chấm cho 1 feature bạn đã làm: +- [ ] đo được effect bằng metric trước/sau +- [ ] có alert khi nó hỏng +- [ ] có runbook 10 dòng cho người khác oncall +- [ ] viết ADR cho quyết định không hiển nhiên + +4/4 = 0.6 đạt. + +--- +Tự chấm xong: gửi kết quả cho tôi, tôi cắt bớt các tầng đã ≥2 trong LEARNING-PATH. diff --git a/learn/senior-web-dev/01-javascript-runtime.md b/learn/senior-web-dev/01-javascript-runtime.md new file mode 100644 index 0000000..6446aa3 --- /dev/null +++ b/learn/senior-web-dev/01-javascript-runtime.md @@ -0,0 +1,159 @@ +# L1 — JavaScript runtime + +## 1.1 Primitives vs object, tham chiếu vs giá trị + +Primitives (string, number, boolean, null, undefined, symbol, bigint) copy theo giá trị. Object/array/function copy theo tham chiếu. + +```js +const a = { n: 1 }; +const b = a; // b trỏ cùng object +b.n = 2; +console.log(a.n); // 2 — không phải bản copy + +const c = [1, [2]]; +const d = [...c]; // shallow copy: d[1] vẫn cùng array với c[1] +d[1].push(3); +console.log(c[1]); // [2,3] — mutate xuyên qua shallow copy +``` +Deep copy thật: `structuredClone(c)` (không clone được function/DOM node). + +## 1.2 `this` — quyết định tại call site, không phải lúc khai báo + +```js +function who() { return this; } +who(); // undefined (strict mode) / globalThis +const obj = { who }; +obj.who(); // obj — call site là obj +const f = obj.who; +f(); // undefined — đã mất call site +const g = f.bind(obj); +g(); // obj — bind cố định +[1].map(obj.who); // window/undefined — map gọi hàm trần, không qua obj +``` +Arrow function: `this` lấy từ scope lexical chứa nó — vì vậy arrow không dùng làm method cần `this` của object. + +Closure ví dụ interview kinh điển: +```js +for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3,3,3 — 1 biến i +for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 0,1,2 — mỗi loop 1 binding +``` + +## 1.3 Prototype chain + +`class` là sugar. Bản chất: object này delegate lên object kia. + +```js +const animal = { eats() { return 'yum'; } }; +const dog = Object.create(animal); +dog.barks = () => 'woof'; +dog.eats(); // 'yum' — tìm không thấy ở dog → leo lên animal +Object.getPrototypeOf(dog) === animal; // true +``` +`instanceof` = đi ngược prototype chain tìm `prototype` của constructor. Vì vậy `class` kế thừa = nối chain, không copy method. + +## 1.4 Scope & hoisting / TDZ + +```js +console.log(x); // undefined — var được "hoist" nhưng chưa gán +var x = 1; +console.log(y); // ReferenceError — TDZ: let tồn tại trong scope nhưng chưa tới dòng khai báo +let y = 2; +``` +TDZ tồn tại để bắt lỗi dùng biến trước khi khởi tạo — `var` không có bảo vệ đó = nguồn bug kinh điển. + +## 1.5 Event loop: microtask thắng macrotask + +```js +console.log('1'); +setTimeout(() => console.log('timeout'), 0); +Promise.resolve().then(() => console.log('promise')); +queueMicrotask(() => console.log('microtask')); +console.log('2'); +// 1, 2, promise, microtask, timeout +``` +Quy tắc: sau mỗi macrotask, drain HẾT microtask queue rồi mới sang việc tiếp. `setTimeout` là macrotask; `.then` là microtask. + +## 1.6 Vì sao 1 thread chịu tải lớn + +Vì I/O không chiếm thread: Node đăng ký socket với kernel (epoll), kernel báo khi có dữ liệu. Thread rảnh đi làm request khác. + +Bug vì hiểu sai: +```js +app.get('/report', (req, res) => { + const data = fs.readFileSync('/big.csv'); // BLOCK cả process ~3s + res.send(data); // mọi request khác chờ 3s +}); +// fix: await fs.promises.readFile(...) hoặc stream +``` +`process.nextTick` chạy TRƯỚC cả microtask promise — dùng nội bộ Node, app code hiếm khi cần. + +## 1.7 Libuv thread pool + +`fs`, DNS, crypto (scrypt/bcrypt) KHÔNG chạy trên event loop — chúng mượn thread pool, mặc định 4. + +Hệ quả interview: chạy bcrypt (CPU-bound) trên 20 req/s → pool 4 thread nghẽn → fs cũng chậm theo (chung pool). Fix: `UV_THREADPOOL_SIZE=16`, hoặc đẩy hashing sang worker/service riêng. + +## 1.8 Async: error propagation & combinators + +```js +// await trong loop = tuần tự CÓ Ý THỨC +for (const id of ids) results.push(await getUser(id)); // tổng = sum latency +await Promise.all(ids.map(id => getUser(id))); // tổng = max latency, nhưng 1 fail → fail hết +await Promise.allSettled(ids.map(id => getUser(id))); // không fail, tự xử lý từng result +``` +```js +// unhandled rejection: promise không ai bắt +fetchUser(id).catch(handle); // đúng +try { await fetchUser(id); } catch(e) {} // đúng +fetchUser(id); // SAI — crash Node 15+ default +``` + +## 1.9 GC & memory leak kinh điển + +GC mark-sweep: object không còn reference từ root (stack, global) mới bị thu dọn. Leak = bạn còn giữ reference mà không biết. + +```js +// leak 1: listener không remove (thường gặp khi component re-render/register lặp) +bus.on('tick', handler); // mỗi lần setup thêm 1 handler +return () => bus.off('tick', handler); // cleanup mới là fixes + +// leak 2: closure giữ object lớn +function makeCache() { + const huge = loadBigData(); // 500MB + return () => huge.version; // huge không được GC vì closure giữ +} + +// leak 3: Map làm cache không eviction +cache.set(req.url, res); // không bao giờ xoá → fix: Map có TTL / lru-cache +``` + +## 1.10 JSON giới hạn & structuredClone + +```js +JSON.parse(JSON.stringify({ d: new Date(), m: new Map(), fn: () => {} })); +// Date → string "2026-...", Map → {}, fn bị LOẠI BỎ. JSON không có kiểu này. +const cycle = {}; cycle.self = cycle; +JSON.stringify(cycle); // TypeError +structuredClone(cycle); // OK +``` + +## 1.11 Errors + +```js +class DomainError extends Error { + constructor(msg, public code) { super(msg); this.name = 'DomainError'; } +} +try { throw new DomainError('out of stock', 'OUT_OF_STOCK'); } +catch (e) { + if (e instanceof DomainError) return handle(e.code); // lỗi nghiệp vụ: map code + throw e; // lỗi lạ: ném tiếp, đừng swallow +} +finally { release(); } // finally chạy kể cả có return trong try +``` +Quy tắc senior: lỗi nghiệp vụ dự đoán được → Result/code; lỗi bất thường → exception + crash-loud + boundary transform ở L8. + +**Check cuối tầng:** giải thích được vì sao +```js +for (const x of [1,2,3]) { await fetch(x); } // chậm tổng = 3 lần +``` +và khi nào bạn CHỌN giữ tuần tự (rate limit phía server, transaction thứ tự). diff --git a/learn/senior-web-dev/02-typescript.md b/learn/senior-web-dev/02-typescript.md new file mode 100644 index 0000000..69c56b4 --- /dev/null +++ b/learn/senior-web-dev/02-typescript.md @@ -0,0 +1,137 @@ +# L2 — TypeScript + +## 2.1 Structural typing, `unknown` vs `any` vs `never` + +TS so sánh HÌNH DẠNG, không so tên. Object có đủ field thì nhận, không cần cùng "class". + +```ts +type Point = { x: number; y: number }; +const p: Point = { x: 1, y: 2, z: 3 }; // OK — thừa field khi gán trực tiếp mới bị chặn +declare const q: { x: number; y: number; z: number }; +const r: Point = q; // OK — structural: q "to hơn" Point vẫn nhận + +any // tắt type-check, lan sang mọi thứ chạm vào → cấm dùng +unknown // giá trị chưa biết: buộc phải narrow trước khi dùng +never // không có giá trị nào: return của function throw, exhaustive check +``` +Dùng `never` để ép exhaustive: +```ts +function shapeArea(s: Circle | Square): number { + switch (s.kind) { + case 'circle': return Math.PI * s.r ** 2; + case 'square': return s.side ** 2; + default: { const _: never = s; return _; } // thêm Shape mới → compile error, không lọt + } +} +``` + +## 2.2 Narrowing & type predicate + +```ts +type Result = { ok: true; value: T } | { ok: false; error: string }; + +function unwrap(r: Result): T { + if (r.ok) return r.value; // sau if: r được narrow còn nhánh ok:true + throw new Error(r.error); +} + +function isUser(x: unknown): x is { id: string } { // type predicate + return typeof x === 'object' && x !== null && 'id' in x; +} +``` + +## 2.3 Generics & conditional types + +```ts +function last(arr: T[]): T | undefined { return arr.at(-1); } + +type Unwrap = T extends Promise ? Unwrap : T; // recursive infer +type A = Unwrap>>; // string + +type First = T extends [infer F, ...unknown[]] ? F : never; +``` + +## 2.4 Utility types — phải tự viết lại được + +```ts +type MyPartial = { [K in keyof T]?: T[K] }; +type MyPick = { [P in K]: T[P] }; +type MyOmit = MyPick>; +type MyAwaited = T extends PromiseLike ? MyAwaited : T; +``` +Interview hay hỏi: "Omit và Pick khác gì, viết Pick từ `keyof`". + +## 2.5 Mapped & template literal types + +```ts +type Keys = keyof T; +type Val = T[K]; // indexed access +type Getters = { [K in keyof T as `get${Capitalize}`]: () => T[K] }; +type G = Getters<{ name: string }>; // { getName: () => string } +``` + +## 2.6 strict flags — từng flag giải thích được + +- `strictNullChecks`: `string` không nhận `null`. Bug nó chặn: `user.profile.name` khi profile undefined. +- `noUncheckedIndexedAccess`: `arr[i]` là `T | undefined`. Chặn giả định array đủ dài. +- `exactOptionalPropertyTypes`: `?` nghĩa là "có thể không có key", KHÔNG phải "được gán undefined". +- Bật cả 4 trong tsconfig base của monorepo — org rule, interview thấy bạn bật = điểm. + +## 2.7 interface vs type + +```ts +interface Cfg { url: string } +interface Cfg { retries?: number } // merge tự động — type không làm được +type Cfg2 = { url: string }; +// type Cfg2 = Cfg2 | string // recursive union: type làm được +``` +Rule of thumb: public API của package = interface (người khác merge được); nội bộ = type. + +## 2.8 ESM vs CJS + +```jsonc +// package.json +{ "type": "module" } // .js = ESM; cần CJS thì đặt .cjs +``` +```ts +import fs from 'node:fs'; // ESM +const fs = require('fs'); // CJS — không có trong "type":"module" +``` +Dual package hazard: lib ship cả 2 bản, 2 instance của cùng class → `instanceof` fail. Senior đọc `exports` map trong package.json lib trước khi dùng. + +## 2.9 tsconfig: 3 khóa không nhầm nhau + +- `target` / `lib`: code sinh ra ES mấy / có sẵn type API nào (DOM, ES2022) +- `module`: cú pháp import/module nào được preserve +- `moduleResolution`: TÌM file trên đĩa bằng chiến lược nào (`bundler` cho Next, `node16` cho Nest ESM) + +Project references: `composite: true` để build từng package độc lập + incremental. + +## 2.10 Type erasure: type biến mất lúc chạy + +```ts +enum Role { Admin } // runtime sinh object Role — tree-shaking khó, prefer const object +const as = data as User; // KHÔNG kiểm tra gì runtime. `as` không phải validator. +// Input thật từ HTTP: dùng zod/class-validator — org dùng class-validator pipeline (L8.5) +``` + +## 2.11 Declaration & augmentation + +```ts +declare module 'fastify' { interface FastifyRequest { user?: User } } // thêm field vào lib +``` + +## 2.12 tsc vs transpile-only + +SWC/esbuild chỉ XÓA type (nhanh, không đọc type). Type lỗi vẫn chạy được → CI bắt buộc chạy `tsc --noEmit` riêng, không tin "build pass". + +## 2.13 Pattern thực chiến + +```ts +type Brand = T & { __brand: B }; +type OrderId = Brand; +function getOrder(id: OrderId) {} +getOrder('abc' as OrderId); // chỗ cast duy nhất: ranh giới DB/HTTP +``` + +**Check:** viết `type Paths` sinh mọi đường `"user.address.city"` cho object lồng nhau, không tra Google. diff --git a/learn/senior-web-dev/03-http-web-platform.md b/learn/senior-web-dev/03-http-web-platform.md new file mode 100644 index 0000000..c941f83 --- /dev/null +++ b/learn/senior-web-dev/03-http-web-platform.md @@ -0,0 +1,99 @@ +# L3 — HTTP & web platform + +## 3.1 HTTP/1.1 semantics + +- safe: GET/HEAD — không đổi trạng thái server +- idempotent: PUT/DELETE — gọi N lần = 1 lần (POST không) +- vì sao quan trọng: browser/proxy CHỈ tự retry khi idempotent. Payment bằng GET = thảm họa. + +```http +POST /orders HTTP/1.1 +201 Created +Location: /orders/42 ← thiếu cái này = API nghiệp dư +``` +400 = request sai cú pháp. 422 = cú pháp đúng, nghiệp vụ sai (email hợp lệ nhưng đã tồn tại). 409 = xung đột trạng thái (double-submit). Interview hỏi 400 vs 422 vs 409: trả lời bằng ví dụ trên. + +## 3.2 HTTP/2 &/3 + +HTTP/1.1: 1 connection = 1 response tại một thời điểm → browser mở 6 socket, vẫn head-of-line blocking. +HTTP/2: 1 socket, nhiều stream xen kẽ — fix ở tầng app, nhưng mất packet → stall cả socket (TLS nằm dưới). +HTTP/3/QUIC: UDP, mỗi stream độc lập mất packet. Không cần "làm gì thêm" — bật ở edge/CDN, hiểu để nói. + +## 3.3 TLS 1.3 + +ClientHello → ServerHello+cert+key-share → ClientFinished. 1-RTT. +Certificate chain: leaf → intermediate → root (root nằm trong OS store, không ship). +Private key không rời server — proof danh tính bằng chữ ký, không bao giờ gửi key. +SNI: server nhiều cert trên 1 IP, chọn cert theo tên trong ClientHello (hiện bằng plaintext — lý do có ECH). + +## 3.4 Cache — nơi senior bị soi + +```http +Cache-Control: public, max-age=31536000, immutable # /assets/logo.3f2a1c.png (hashed filename) +Cache-Control: private, no-store # API trả HTML có tên user +Vary: Accept-Language, Cookie # CDN cache riêng theo 2 header +stale-while-revalidate=60 # phục vụ bản cũ trong lúc làm mới +``` +Bài check (đề bài ở LEARNING-PATH): ảnh `Vary: Cookie` → mọi request có Cookie header khác nhau = cache key khác nhau → CDN miss gần 100%. Fix: không set cookie trên domain asset, hoặc bỏ Vary: Cookie. +ETag vs Last-Modified: ETag chính xác cả khi nội dung đổi cùng giây; 304 không có body. + +## 3.5 Content negotiation & multipart + +```http +Accept: application/json # client muốn gì +Content-Type: multipart/form-data; boundary=----xyz +``` +Multipart = nhiều part (field text + file binary) trong 1 body, phân cách bằng boundary — đây là format upload của org rule (interceptor mediaParse đọc từ đây, L8.7/L10.7). + +## 3.6 Cookie + +```http +Set-Cookie: sid=abc; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400 +``` +- `HttpOnly`: JS không đọc được — chặn 90% hậu quả XSS +- `SameSite=Lax`: cookie KHÔNG gửi khi user bị site khác redirect tới (POST cross-site chặn hẳn) +- `SameSite=None` bắt buộc kèm `Secure` +- Vì sao SameSite phá CORS-with-credentials: fetch phải `credentials:'include'` + server trả `Access-Control-Allow-Credentials: true` + KHÔNG được `Allow-Origin: *`. + +## 3.7 CORS + +Prefight nổ khi: method không phải GET/HEAD/POST, header tùy chỉnh (Authorization, Content-Type: application/json), hoặc credentials. + +``` +OPTIONS /api/orders +Origin: https://admin.example.com +Access-Control-Request-Method: POST + +204 +Access-Control-Allow-Origin: https://admin.example.com +Access-Control-Allow-Headers: authorization, content-type +Access-Control-Max-Age: 86400 # cache preflight, giảm latency +``` +Sửa ở edge (CDN/nginx) hay app: nhiều service sau 1 gateway → sửa gateway 1 lần; nhưng app vẫn nên validate origin riêng cho route nhạy cảm. + +## 3.8 URL encoding + +```js +const q = encodeURIComponent('a&b'); // 'a%26b' — encode CHỖ VALUE, không encode cả query string +new URL('/search?q=' + q, 'https://x.com').searchParams.get('q'); // 'a&b' +``` +Bug kinh điển: nối string URL thay vì `URL` API → `&` trong dữ liệu = tham số giả = injection điểm. + +## 3.9 SSE vs WebSocket vs long-poll + +| Hướng | Proxy-friendly | Kết nối | Dùng khi | +|---|---|---|---| +| SSE (1 chiều server→client) | HTTP thường | 1, tự reconnect | notification, token stream LLM | +| WebSocket (2 chiều) | cần upgrade | 1, giữ trạng thái | collaborative editor, game | +| long-poll | HTTP thường | mỗi lần 1 req | fallback, polling dữ liệu ít đổi | + +```js +// SSE — server Node +res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }); +res.write(`event: tick\ndata: ${JSON.stringify(payload)}\n\n`); +``` + +## 3.10 Range & compression + +`Range: bytes=0-1023` → 206 Partial Content — resume download, video seek. +`Content-Encoding: br` (Brotli) nhỏ hơn gzip ~15%; `Accept-Encoding` để client declares; CDN quyết định. diff --git a/learn/senior-web-dev/04-browser-frontend.md b/learn/senior-web-dev/04-browser-frontend.md new file mode 100644 index 0000000..a982ff1 --- /dev/null +++ b/learn/senior-web-dev/04-browser-frontend.md @@ -0,0 +1,87 @@ +# L4 — Browser & frontend foundations + +## 4.1 Critical rendering path + +HTML parse → dựng DOM; CSS (render-blocking) → dựng CSSOM; ghép → render tree → layout (vị trí/kích thước) → paint (vẽ pixel) → composite (GPU ghép layer). + +Ví dụ: `` ở head chặn render — browser không vẽ gì cho tới khi CSSOM xong (nếu vẽ trước = flash của style sai). JS ở head không defer chặn PARSER (nó có thể document.write). + +## 4.2 Reflow vs repaint vs composite + +- reflow (layout lại): đổi width/height/top/left, đọc `offsetHeight` sau khi ghi style +- repaint: đổi màu, shadow — không đổi hình học +- composite-only: `transform: scale()`, `opacity` — chạy trên compositor thread, không chạm layout/paint + +```js +// layout thrashing — đọc/ghi xen kẽ từng dòng = N lần reflow +rows.forEach(r => { r.style.height = r.offsetHeight * 2 + 'px'; }); +// fix: đọc hết, rồi ghi hết +const hs = rows.map(r => r.offsetHeight); +rows.forEach((r, i) => r.style.height = hs[i] * 2 + 'px'); +``` +`will-change: transform` tạo layer riêng NHƯNG ăn GPU memory — thêm bừa = slower, không faster. + +## 4.3 Script loading + +```html + + + +``` +Inline handler trong module: `el.addEventListener` trong code, không `onclick=""` trong HTML (CSP, L8/L11). + +## 4.4 Core Web Vitals — mỗi cái kể được 3 nguyên nhân + +**LCP** ( Largest Contentful Paint < 2.5s): ① server chậm (TTFB) ② ảnh LCP lazy-load sai (`loading=lazy` trên ảnh đầu trang = tự phá) ③ font/text render chậm. +**INP** (< 200ms): task JS > 50ms trên main thread; click handler gọi code nặng đồng bộ; hydration của Next làm đóng băng main thread. Fix: `useTransition`, `scheduler.yield`, đẩy Web Worker. +**CLS** (< 0.1): ① ảnh không có width/height ② banner chèn trên nội dung ③ `font-display: swap` làm text nhảy — fix bằng `size-adjust`. + +## 4.5 Resource loading + +```html + + + + + +``` +Quy tắc một câu: LCP element = `fetchpriority=high` + `priority` (Next), mọi thứ khác lazy/prefetch. + +## 4.6 Storage + +| | dung lượng | async | gửi lên server? | dùng khi | +|---|---|---|---|---| +| cookie | ~4KB | sync | CÓ, mỗi request | session token (HttpOnly) | +| localStorage | ~5MB | sync, chặn main thread | không | theme, flag không nhạy | +| IndexedDB | hàng trăm MB | async | không | offline data, file lớn | +| Cache API | lớn | async | không | service worker cache response | + +Cấm: JWT vào localStorage (XSS đọc sạch) — token nằm cookie HttpOnly hoặc memory. + +## 4.7 Accessibility + +```html + +
Gửi
+
Ít nhất 8 ký tự
+``` +Focus management: mở dialog → focus vào phần tử đầu; đóng → trả focus về nút mở (`` + `showModal()` làm sẵn cả hai). + +## 4.8 Security browser + +```http +Content-Security-Policy: default-src 'self'; script-src 'nonce-{R}' +``` +```html + +``` +`unsafe-inline` = CSP để trang trí. X-Frame-Options cũ → `frame-ancestors 'none'`. `` → `rel="noopener"` (trang mới không giữ `window.opener`). + +## 4.9 Navigation + +History API: `pushState` đổi URL không reload — React Router build trên `popstate`. +PRG (Post/Redirect/Get): sau POST → redirect 303 → GET. Refresh không resubmit form. Server Actions của Next làm đúng pattern này mặc định. + +## 4.10 Modern APIs nên biết mặt + +`` native (backdrop, ESC, focus trap), popover API, anchor positioning (`anchor-name` + `position-anchor` — tooltip không cần JS tính toạ độ), container queries (`@container (min-width: 40rem)`), `:has()` ("parent selector": `form:has(:invalid)`), View Transitions API, `Intl.DateTimeFormat`/`Intl.NumberFormat` cho i18n khỏi thư viện. diff --git a/learn/senior-web-dev/05-react.md b/learn/senior-web-dev/05-react.md new file mode 100644 index 0000000..06eb7c3 --- /dev/null +++ b/learn/senior-web-dev/05-react.md @@ -0,0 +1,127 @@ +# L5 — React + +## 5.1 Render model & keys + +UI = f(state). State đổi → React gọi lại f với state mới → diff cây mới/cũ → sửa DOM tối thiểu. + +```jsx +// key sai: reorder list = React tưởng đổi nội dung +items.map((it, i) => ) // index-as-key +items.map(it => ) // id ổn định +``` +Với `key={i}`: thêm item vào ĐẦU → mọi item lệch key → React re-render + reuse DOM sai chỗ, state của input trong Row dính nhầm dòng. Key = danh tính, không phải vị trí. + +## 5.2 Fiber (đủ để nói chuyện senior) + +React chia render thành các unit nhỏ (fiber = 1 unit công việc), rendering có thể bị CẮT GIỮA khi có update ưu tiên cao (typing thắng data fetch). 2 phase: +- render phase: tính toán, CÓ THỂ bị bỏ/hủy → vì vậy KHÔNG được có side effect trong render body +- commit phase: áp dụng lên DOM, đồng bộ, không bị ngắt + +Đây là gốc của "render phải thuần khiết" — không phải lời khuyên thẩm mỹ. + +## 5.3 State: không phải biến + +```jsx +setCount(count + 1); setCount(count + 1); // +1, vì count là GIÁ trị tại render này +setCount(c => c + 1); setCount(c => c + 1); // +2 — updater nhận giá trị mới nhất +``` +Batch: cả 2 lời gọi gộp 1 render. `useState` không "sửa" gì — nó xếp lịch render kế tiếp với giá trị mới. + +## 5.4 Effect — hiểu đúng trong 1 ví dụ + +Effect = đồng bộ app với hệ thống ngoài (socket, DOM API, subscription). KHÔNG dùng cho: biến đổi dữ liệu, fetch dữ liệu server (dùng react-query/server component), "chạy sau render". + +```jsx +// ĐÚNG: có cleanup vì effect chạy lại mỗi deps đổi +useEffect(() => { + const id = setInterval(() => setT(t => t + 1), 1000); + return () => clearInterval(id); // cleanup là PHẦN CỦA effect +}, []); + +// SAI kinh điển: derived state trong effect +useEffect(() => { setFullName(first + ' ' + last); }, [first, last]); // 2 render, 1 giá trị cũ +const fullName = first + ' ' + last; // tính ngay trong render — hết effect +``` + +## 5.5 Ref + +`ref.current` thay đổi không gây render. Dùng đúng: handle DOM (focus, scroll), giữ giá trị "phi render" (id của interval, giá trị trước để so sánh). + +```jsx +const inputRef = useRef(null); + +``` +React 19: function component nhận `ref` như prop thường — `forwardRef` hết bắt buộc. + +## 5.6 Memoization — đo trước, đừng mù quáng + +```jsx +const sorted = useMemo(() => heavySort(items), [items]); // thắng khi items ổn định (referential equality) +``` +Thua khi: `items` là array mới mỗi render (`props.items.filter(...)`) → memo recompute mọi lần + phí so sánh. `React.memo` thua khi prop là object/function khai báo inline. Rule: mặc định KHÔNG memo; thêm khi profiler chỉ ra render đắt. + +## 5.7 Context + +Context đổi = re-render MỌI consumer. Tách theo tần suất: + +```jsx + // theme đổi ít + // user đổi nhiều — tách 2 context +``` +State đổi liên tục (cursor, drag): dùng zustand với selector (`useStore(s => s.x)`) thay vì context — selector chỉ re-render khi lát cắt đó đổi. + +## 5.8 Suspense & concurrent + +```jsx +const Chat = lazy(() => import('./Chat')); +}> // fallback thay khi chưa xong + + +const [isPending, start] = useTransition(); // update background: UI cũ vẫn click được +start(() => setTab('settings')); +``` +`use()` (React 19): unwrap promise/context ngay trong render, suspend tới khi resolve. + +## 5.9 Forms với Actions + +```jsx +function Search() { + const [state, action] = useActionState(search, null); + return ( +
+ + {state?.pending && } + {state?.error &&

{state.error}

} + + ); +} +// server action / useActionState: form hoạt động cả khi JS chưa hydrate +``` +Controlled khi cần validate từng keystroke; uncontrolled + FormData khi chỉ cần giá trị lúc submit (đỡ re-render mỗi phím). + +## 5.10 Component API design (điểm senior) + +Thay vì 20 boolean props: + +```jsx +// props explosion + +// composition: children-as-slot + + </Panel.Header> + <Panel.Body/> + {onClose && <Panel.Footer onClose={onClose}/>} // logic nằm chỗ NGƯỜI DÙNG quyết định +</Panel> +``` + +## 5.11 Anti-patterns nhận diện nhanh + +- sync props vào state lúc mount (`useState(props.x)` — đổi props không theo) → stateless hoặc `key` reset +- fetch trong useEffect + loading state tự chế → server-state library hoặc RSC +- "prop drilling" sâu 5 tầng → context đúng loại (thường xuyên đổi? tách context) hoặc composition + +## 5.12 Server components — ranh giới tư duy + +Server component = chạy 1 lần lúc render server, output là serialized tree, KHÔNG gửi code xuống client. Client component = chạy trên browser. Đây là cầu nối sang L6. + +**Check cuối tầng:** giải thích vì sao code chạy trong render body (fetch, log, random) là bug chứ không phải "code chạy mỗi render" bình thường — dựa trên 5.2. diff --git a/learn/senior-web-dev/06-nextjs.md b/learn/senior-web-dev/06-nextjs.md new file mode 100644 index 0000000..d64fbcf --- /dev/null +++ b/learn/senior-web-dev/06-nextjs.md @@ -0,0 +1,131 @@ +# L6 — Next.js (App Router) + +## 6.1 Rendering spectrum + +| Kiểu | dữ liệu dựng ở đâu | revalidate | chọn khi | +|---|---|---|---| +| SSG | build time | không bao giờ | blog, docs | +| ISR | build + nền | `revalidate: 3600` hoặc by-tag | trang sản phẩm (ngàn trang, đổi chậm) | +| SSR | mỗi request | luôn | trang cá nhân hoá, có auth | +| CSR | browser | tuỳ client | dashboard sau login, SEO không cần | +| PPR | tĩnh + hole streaming | lai | trang có phần chung + phần theo user | + +Một trang có CẢ hai phần: marketing (tĩnh) + giỏ hàng (động) → static shell + Suspense hole cho phần động (đây là bản chất PPR). + +## 6.2 App Router routing + +``` +app/ +├── layout.tsx # giữ nguyên khi route con đổi — không remount +├── loading.tsx # Suspense fallback tự động cho cấp này +├── (marketing)/page.tsx # route group: không tạo URL segment +├── dashboard/page.tsx +└── @modal/(.)photos/[id]/route.ts # intercepting route: modal không navigation mới +``` +Parallel routes (`@slot`) render nhiều layout song song; intercepting route (`(.)`) bắt link nội bộ để hiển thị modal thay vì sang trang. + +## 6.3 Server vs Client components + +```tsx +// page.tsx (server) — code này KHÔNG xuống browser: query, secret đều an toàn +import 'server-only'; +export default async function Page() { + const products = await db.product.findMany(); // Prisma chạy thẳng + return <ProductList products={products} onBuy={addToCart} />; // props phải serialize được +} +// 'use client' ở ĐÂU thì JS bundle bắt đầu từ ĐÓ +``` +Boundary rules: Server component KHÔNG dùng `useState`, không chạy `onClick`. props truyền xuống là snapshot tại thời điểm render server — không phải reactive binding. + +Quy tắc tổ chức: giữ "use client" càng sâu càng tốt; page là server → push tương tác xuống lá. + +## 6.4 Server Actions + +```tsx +<form action={async (formData) => { + 'use server'; + const data = createOrderSchema.parse(formData.get('q') ?? ''); // validate như public API + await createOrder(data); + revalidateTag('orders'); + redirect('/orders'); +}}> + <input name="q" /> +</form> +``` +Server Action = POST endpoint công khai đội lốt form. Interview điểm cao: nói rõ nó phải auth + rate-limit + validate y hệt REST. Progressive enhancement: form gửi được khi JS chưa load. + +## 6.5 Caching layers — chỗ 90% hiểu sai + +4 tầng, từ trong ra ngoài: +1. **Data Cache**: kết quả `fetch()` được cache XUYÊN request (next build, ISR, action revalidate) — `unstable_cache` cache cho hàm không phải fetch +2. **Full Route Cache**: cả rendered HTML của route tĩnh — cache lúc build, chỉ cho route không động (không dùng `cookies()`/`headers()`) +3. **Router Cache** (client): router giữ route đã visit trong session (SPA nav nhanh, quay lại instant) +4. CDN/browser headers bạn tự đặt + +Next 15 đổi default: `fetch` không còn cache tự động (default `no-store`) — nói được chi tiết tiết đổi này = biết trend. +`revalidatePath('/p/1')` vs `revalidateTag('product:1')` vs `cacheLife/profile`: độ hạt của tag nhỏ hơn, là cách chính xác cho "1 sản phẩm đổi, đừng dựng lại 1000 trang khác". + +## 6.6 Data fetching + +```tsx +// trong 1 request: các await tuần tự = TTFB cộng dồn → dùng Suspense để stream sớm +export default function Page() { + return <Suspense fallback={<Skeleton/>}><Banner/></Suspense> // banner render ngay + <Suspense fallback={...}><Recommends/></Suspense> // chậm → stream sau +} +``` +Cùng level nhiều fetch độc lập: `await Promise.all([a, b])` hoặc để mỗi component tự await. Dedupe chỉ ăn trong CÙNG request khi URL+options giống hệt. + +## 6.7 Middleware + +```ts +export const config = { matcher: '/((?!_next/|api/).*)' }; // trừ asset — chạy middleware cho asset = tiền triệu mỗi ngày +export function middleware(req: NextRequest) { + const locale = req.cookies.get('lng') ?? req.headers.get('accept-language'); + return NextResponse.rewrite(new URL(`/${locale}${req.nextUrl.pathname}`, req.url)); +} +``` +Middleware chạy edge runtime, MỖI request, trước cache. Đừng query DB trong middleware — thêm 1 round-trip mọi asset. + +## 6.8 next/image & next/font + +```tsx +<Image src="/p.jpg" width={800} height={600} priority alt="..."/> +// width/height trên image => browser reserve chỗ => CLS = 0 (chính là 4.4) +// src=/p.jpg 800w, .../p.jpg?w=384 384w... tự sinh từ config image +// next/font: fetch font lúc build, self-host, font-display: swap + size-adjust tự động => không layout shift +``` +Trade-off: `qualities` config để chặn abuse (image optimizer là endpoint chạy code — nếu mở mọi URL ngoài = SSRF + CPU DoS → bật `remotePatterns` chặt). + +## 6.9 Streaming & dynamic import + +```tsx +const HeavyChart = dynamic(() => import('./HeavyChart'), { ssr: false }); +// ssr:false = client-only. Dùng khi component CẦN window/chart lib. +// LƯU Ý: ssr:true mặc định — dùng dynamic cho component nặng nhưng CÓ SSR mới đáng. +// Dynamic không phải công cụ "tối ưu" nếu component không lớn: bundle đã split tự nhiên theo route. +``` + +## 6.10 Error handling + +`error.tsx` = error boundary cho cấp segment, `global-error.tsx` khi cả root layout hỏng, `not-found.tsx` + `notFound()` throw. Server error không lộ message — `error.digest` (hash) để trace log. + +## 6.11 i18n + +`next-intl`: middleware locale detect → `[locale]` segment → message catalog JSON per-locale. Cache: `Cache-Control` phải `Vary: Accept-Language` hoặc encode locale trong URL (đổi URL = cache key khác — an toàn hơn Vary). Org rule: không hardcode string trong component. + +## 6.12 Deploy & cache trên serverless + +ISR cache mặc định nằm TRÊN instance. 10 lambda → mỗi lambda cache riêng → user A thấy trang revalidate, user B thấy trang cũ. Fix: `use cache` + cache handler (Redis/S3) để cache tầng data dùng chung, hoặc tự nhận "chấp nhận được" — trade-off nói được = điểm. + +## 6.13 Migration Pages → App Router + +| Pages | App tương đương | +|---|---| +| `getStaticProps` | RSC + `export const revalidate` | +| `getServerSideProps` | RSC `await` | +| `_app.tsx` | `app/layout.tsx` | +| `next/head` | metadata export | +| `useRouter` (pages) | từ `next/navigation`, API khác (`push` → `router.push`, query phải qua `useSearchParams` + Suspense — vì nó đọc ở runtime) | + +**Check cuối tầng:** vẽ đường đi request: Edge middleware → cache hit/miss → RSC render (Promise chain, Suspense boundary) → HTML+RSC payload → hydration. Nói rõ tầng nào trả HTML, tầng nào trả payload. diff --git a/learn/senior-web-dev/07-nodejs-runtime.md b/learn/senior-web-dev/07-nodejs-runtime.md new file mode 100644 index 0000000..25f55ea --- /dev/null +++ b/learn/senior-web-dev/07-nodejs-runtime.md @@ -0,0 +1,82 @@ +# L7 — Node.js runtime (backend) + +## 7.1 Process model + +Event loop 6 phase: timers → pending → idle → poll (I/O) → check (setImmediate) → close. Microtask queue drain GIỮA MỖI callback. + +```js +// CPU-bound giết cả loop — 1 thread phục vụ MỌI request +app.get('/pdf', (req, res) => res.send(hugeSyncParse(buf))); // 800ms block → mọi request khác chờ +// đúng: +const worker = new Worker('./parse.js', { workerData: buf }); // worker_threads: thread riêng + event loop riêng +``` +Cluster = N process × 1 core, chia qua IPC. Multi-core không tự có — 1 process Node = 1 core. + +## 7.2 Streams & backpressure + +Backpressure = tốc độ đọc > tốc độ ghi; stream cho phép bên chậm bảo bên nhanh CHỜ. + +```js +import { pipeline } from 'node:stream/promises'; +await pipeline( + fs.createReadStream('huge.csv'), + csvParser(), + dbInsertStream(), // mỗi batch await, không nạp hết RAM +); +// pipe() cũ: error giữa đường = leak fd. pipeline() luôn cleanup. +``` +Interview: "vì sao export 1GB CSV không được `res.send(rows)`" → RAM spike + không có backpressure → OOM. Stream = RAM không đổi theo kích thước file. + +## 7.3 Buffer + +```js +Buffer.from('abc'); // OK +Buffer.alloc(1024); // zero-filled — an toàn +Buffer.allocUnsafe(1024); // nhanh hơn nhưng chứa RÁC bộ nhớ cũ — đọc được nếu quên ghi = leak dữ liệu người khác +``` + +## 7.4 fs + +```js +fs.readFileSync(path); // block loop — production path: cấm +await fs.promises.readFile(path); // async thread pool (L1.7: pool 4 thread) +fs.watch(path, ...) // OS-level, cần debounce +``` +Upload file: KHÔNG ghi tạm rồi đọc lại — `pipeline(req.file.stream, fs.createWriteStream(dest))` từ stream multipart (L8.5/L10.7). + +## 7.5 Child process + +`spawn` (stream output, long-running) vs `exec` (buffer output, có giới hạn, command injection nếu nối chuỗi input!) vs `fork` (Node↔Node + IPC channel). +Khi nào thoát Node: CPU-heavy persist (ffmpeg, resize ảnh hàng loạt) → service Python/C hoặc queue worker, không phải thread nào trong API. + +## 7.6 Memory trong container + +Container limit 512MB, Node thấy HOST 16GB → heap lớn → OOMKilled bởi kernel, không phải V8. + +``` +node --max-old-space-size=450 server.js hoặc NODE_OPTIONS=--max-old-space-size=450 +``` +Chẩn đoán OOM: `node --inspect` → DevTools heap snapshot → so 2 snapshot sau 200 request → object tăng không giảm = leak (khớp L1.9). + +## 7.7 Offload pattern + +Metric quyết định: event loop lag (metric của Node itself: `perf_hooks.monitorEventLoopDelay`). lag p99 > 100ms = có task dài trong loop → profile tìm thủ phạm, đẩy ra worker/queue. + +## 7.8 Observability runtime + +```js +// async context — Nest dùng cái này cho per-request logger +import { AsyncLocalStorage } from 'node:async_hooks'; +const als = new AsyncLocalStorage(); +als.run({ reqId: crypto.randomUUID() }, () => handle(req)); +als.getStore().reqId; // đọc được ở BẤT KỲ đâu trong chain, kể cả hàm sâu không nhận tham số +``` +`process.env` đọc mỗi request trong hot path: chậm + config đổi âm thầm giữa chừng — đọc 1 lần lúc boot (L8.9). + +## 7.9 Security runtime + +- Prototype pollution: `Object.assign({}, JSON.parse(userJson))` với `{"__proto__":{"isAdmin":true}}` — guard: không merge JSON không tin cậy vào object dùng chung; bật `Object.freeze(Object.prototype)` nếu cần belt. +- `exec(`convert ${userInput}`)` = command injection. Dùng `execFile` + args array. +- Dependency có postinstall script = thực thi code lúc `npm install` (L11.8). + +**Check:** giải thích vì sao `bcrypt.hashSync` trong request handler là bug ngay cả khi máy mạnh — trả lời bằng L1.7 (chung thread pool với fs). diff --git a/learn/senior-web-dev/08-nestjs.md b/learn/senior-web-dev/08-nestjs.md new file mode 100644 index 0000000..34c7e3d --- /dev/null +++ b/learn/senior-web-dev/08-nestjs.md @@ -0,0 +1,154 @@ +# L8 — NestJS (DI + kiến trúc ứng dụng) + +## 8.1 DI & lifetime + +```ts +@Injectable({ scope: Scope.DEFAULT }) // singleton — mặc định, nhanh nhất +@Injectable({ scope: Scope.REQUEST }) // 1 instance / request — chậm, lý do: phải tạo mới + resolve cả chain +@Injectable({ scope: Scope.TRANSIENT }) // mỗi nơi @Inject được 1 bản mới +``` +Bẫy scope explosion: `OrderService(request)` dùng `Logger(default)` → Logger vẫn singleton; NHƯNG chiều ngược lại: service singleton dùng request-scoped repo thì Nest không cho — bạn sẽ phải scope-request CẢ CHUỖI. Hệ quả: mọi provider upstream thành request-scoped, chết hiệu năng. +`forwardRef` = báo động đỏ circular dependency: module A↔B nghĩa là ranh giới sai → refactor (tách phần chung thành module C). + +Custom provider: +```ts +{ provide: CONFIG, useFactory: (cfg: ConfigService) => loadConfig(cfg), inject: [ConfigService] } +``` + +## 8.2 Module boundaries + +Module = 1 đơn vị triển khai + 1 public API. `exports` là interface; thứ không export = private. + +```ts +@Module({ imports: [PaymentModule], providers: [OrdersService], exports: [OrdersService] }) +export class OrdersModule {} +// module khác chỉ chạm OrdersService — KHÔNG import OrderEntity hay OrdersRepository +``` +Anti-pattern org audit soi: barrel file export hết mọi thứ (`export * from './internal'`) → interface = 0, refactor không được vì ai cũng import lung tung. + +## 8.3 Layering đúng lý do + +```ts +OrdersController // HTTP: parse body, map error → status. Không logic. + └─ OrdersService // use case: điều phối, invariant, transaction + └─ OrdersRepository // Prisma raw queries +``` +Không phải vì sách nói vậy — vì: test service không cần HTTP, đổi transport (REST→gRPC) không đổi service, đổi ORM chỉ đụng repository. Controller import thẳng repository = test service đòi HTTP server. + +## 8.4 Request lifecycle (thứ tự phải đọc làu bàu) + +``` +Middleware → Guards → Interceptors(before) → Pipes → Controller + → Interceptors(after/tail) → ExceptionFilter (nếu nổ) +``` +- Middleware: việc không cần DI metadata (cors, body-limit, request-id) +- Guard: CÓ/KHÔNG vào được (auth, role). Chạy trước khi biết body hợp lệ. +- Interceptor: bọc quanh handler — timing, transform, cache. KHÔNG quyết định authz (đừng giấu if-else auth trong interceptor) +- Pipe: validate/transform input (chỗ duy nhất DTO validation đứng) +- Filter: dịch error → HTTP response + +## 8.5 Validation pipeline (org: class-validator) + +```ts +export class CreateOrderDto { + @IsString() @IsUuid() productId!: string; + @IsInt() @Min(1) qty!: number; +} +// main.ts: app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })) +``` +`whitelist: true` = strip field lạ (chặn mass-assignment). Org twist: mediaUpload interceptor nhận multipart, validate file, ghi file, RÓT URL vào `req.body` TRƯỚC khi pipe chạy → DTO thấy URL như field thường. Nắm để không viết pipe tự parse file (vi phạm upload rule). +Trade-off class-validator vs zod: decorator = schema phân tán trên class, runtime metadata; zod = schema 1 chỗ, sinh type từ schema. Org chốt decorator — biết cả hai để defend lựa chọn. + +## 8.6 Guards + +```ts +@Injectable() +export class JwtAuthGuard implements CanActivate { + canActivate(ctx: ExecutionContext) { /* verify token → req.user */ } +} +@UseGuards(JwtAuthGuard, RolesGuard('admin')) // 2 concern, 2 guard — đừng gộp 1 +``` +`canActivate` chạy mỗi request: query DB trong guard mà không cache → guard thành bottleneck. Pattern: guard chỉ đọc, dữ liệu permission cache trong Redis/req. + +## 8.7 Interceptors + +```ts +@Injectable() +export class TimingInterceptor implements NestInterceptor { + intercept(ctx, next: Observable<any>) { + const t0 = performance.now(); + return next.handle().pipe(tap(() => this.metrics.observe(..., performance.now() - t0))); + } +} +``` +RxJS ở Nest còn đáng không: interceptor/guard API giữ Observable nhưng handler thường trả plain promise — nói được "giữ Observable chỉ khi thật sự cần operator (timeout, retry, cache), còn lại promise" = điểm depth. + +## 8.8 Exception strategy + +```ts +// domain error (L16.8): service KHÔNG nghĩ tới HTTP +export class InsufficientStock extends Error {} +@Catch(InsufficientStock) +export class InsufficientStockFilter implements ExceptionFilter { + catch() { throw new ConflictException({ code: 'OUT_OF_STOCK', message: 'Hết hàng' }); } +} +// hoặc 1 filter trung tâm: switch instanceof → status code; lỗi lạ → 500 + log stack, response KHÔNG leak stack +``` + +## 8.9 Config fail-fast + +```ts +const EnvSchema = z.object({ DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32) }); +// ConfigFactory parse lúc boot → thiếu env = process chết ngay tại startup, không phải ở request đầu tiên +``` +Interview: "vì sao không đọc `process.env.X` trong service?" → lỗi lộ lúc runtime, sai 1 env sập 1/10 endpoint, không reproduce được ở test. + +## 8.10 Background work + +- `@nestjs/schedule` (@Cron): việc chạy trên 1 instance — chạy trên 3 replica = 3 lần. Cần lock hoặc tách 1 service. +- BullMQ (Redis): job queue thật — retry/backoff/DLQ. Email, resize ảnh, webhook fan-out vào đây. +- `@nestjs/event-emitter`: in-process, KHÔNG durable (process chết = mất event). Cần đảm bảo giao → queue (L15.7 outbox). + +## 8.11 Prisma integration (org: Prisma 7) + +```ts +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { await this.$connect(); } + // singleton — 1 client cho cả app, pool bên trong (L9.5) +} +// transaction scope nằm ở SERVICE, không phải repository method lẻ: +await this.prisma.$transaction(async (tx) => { + const stock = await tx.product.update({ where: {...}, data: { stock: { decrement: qty } } }); // atomic + await tx.order.create({ data: {...} }); +}); +``` +Repository ẩn Prisma hoàn toàn hay cho service dùng thẳng Prisma types? Org nghiêng thẳng-thắn + có ranh giới: PrismaClient qua PrismaService, model type chỉ lộ ở boundary mapper. + +## 8.12 Auth module + +Passport strategy chain: `JwtStrategy.validate(payload)` → attach `req.user`. Access token 15', refresh token rotation + reuse detection (L11.2). Password: argon2id (L11.3). + +## 8.13 Testing Nest + +```ts +// unit: mock provider +const mod = await Test.createTestingModule({ providers: [OrdersService, { provide: OrdersRepository, useValue: fakeRepo }] }).compile(); + +// e2e: override PrismaTestContainer, giữ nguyên guard/pipe +@Module({ overrides: [{ provide: PrismaService, useValue: testContainer.prisma }] }) +request(app.getHttpServer()).post('/orders').send(dto).expect(201); +``` +Org rule: KHÔNG mock DB — testcontainers PG thật (L13.3). + +## 8.14 Performance Nest + +- Gọi service ngoài: `https.Agent({ keepAlive: true })` — không thì TLS handshake mọi request +- `@nestjs/platform-fastify`: schema-based response serialization (`ClassSerializerInterceptor` + DTO = loại field nhạy tự động), nhanh hơn express cỡ 2× +- Pool DB per instance: 3 replica × pool 20 = 60 conn — Postgres default max 100 → tính trước khi scale (L9.5) + +## 8.15 Packages = deep module (org rule) + +`packages/` chỉ export qua entrypoint (`index.ts` chọn lọc), implementation ẩn trong subfolder. Có dependency-cruiser config để enforce. Interview: vẽ module graph của app, chỉ ra public API của mỗi module nằm ở file nào. + +**Check cuối tầng:** thiết kế module `Wallet` (balance, debit, credit) — nói rõ: public API (interface) là gì, request-scoped cái nào (không cái nào, nếu bạn có idempotency ở service), transaction boundary nằm đâu. diff --git a/learn/senior-web-dev/09-database.md b/learn/senior-web-dev/09-database.md new file mode 100644 index 0000000..65da190 --- /dev/null +++ b/learn/senior-web-dev/09-database.md @@ -0,0 +1,100 @@ +# L9 — Database (PostgreSQL + Prisma) + +## 9.1 Relational model + +- 1:N `Order` → `OrderItem` (FK ở phía N) +- M:N `Product` ↔ `Tag` → join table `ProductTag(product_id, tag_id, PRIMARY KEY(cả 2))` +- FK: `ON DELETE CASCADE` (order item mất theo order) vs `RESTRICT` (không xoá được khi còn tham chiếu — mặc định an toàn cho dữ liệu tiền) +- Soft delete (`deletedAt`) phá UNIQUE: 2 bản ghi xoá trùng email → unique vẫn nổ. Fix: partial unique index `WHERE deleted_at IS NULL`. + +## 9.2 Index + +B-tree: tìm theo thứ tự sắp xếp, O(log n). Composite index quan trọng ở THỨ TỰ cột: + +```sql +CREATE INDEX idx_order_user_created ON orders (user_id, created_at DESC); +-- chạy: WHERE user_id=? ORDER BY created_at DESC → index scan thẳng +-- KHÔNG chạy cho: WHERE created_at=? (cột đầu không xuất hiện) +-- (user_id=?, created_at=?) OK nhưng (created_at=?, user_id=?) vẫn dùng được index này — query planner tự hoán vị +``` +```sql +EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42; +-- Seq Scan cost=... actual time → chưa có index hoặc table quá nhỏ +``` +Covering index: index chứa cả cột SELECT → không phải về table heap. Cái index KHÔNG sửa được: query trả `SELECT *` 80 cột — index scan vẫn phải fetch row → chọn cột thật cần (Prisma `select`). + +## 9.3 Transactions & isolation + +Mặc định Postgres: READ COMMITTED. + +Race condition kinh điển — check-then-insert: +```sql +-- 2 request cùng lúc đều thấy "chưa có coupon" → cả 2 insert +-- ❌ app lock (Redis) = thêm phụ thuộc, vẫn race nếu quên +-- ✅ DB unique constraint: INSERT lần hai nhận lỗi 23505 → app bắt lỗi = đã tồn tại +``` +Lost update: +```sql +-- ❌ SELECT balance → update trong app (mất 1 trong 2 giao dịch) +-- ✅ UPDATE accounts SET balance = balance - 100 WHERE id = ? -- atomic ngay trong SQL +-- hoặc SELECT ... FOR UPDATE khi logic phức tạp hơn 1 câu +``` + +## 9.4 N+1 + +```ts +// log: 1 query cha + N query con (bật Prisma log: ['query']) +const orders = await prisma.order.findMany(); +for (const o of orders) o.items = await prisma.orderItem.findMany({ where: { orderId: o.id } }); // N+1 +// fix 1: include (join) — 1-2 query +// fix 2: batch — findMany({ where: { orderId: { in: ids } } }) rồi group bằng Map +``` + +## 9.5 Connection pool + +- PrismaClient pool default: `num_cpus * 2 + 1` connections — NHƯNG serverless: mỗi lambda 1 client → 500 lambda × pool = phá DB +- Fix: PgBouncer (transaction mode) trước DB; pool size per instance = `max_connections / số instance − headroom` +- vì sao không 1 connection/request: TLS+auth ~vài ms mỗi request × QPS = latency + phí + +## 9.6 Prisma specifics + +- PrismaClient 1 singleton/process (L8.11). Trong Next.js: global singleton chống hot-reload spawn hàng chục pool. +- `$transaction([...])` = atomic (batch, không đọc kết quả giữa các câu); `$transaction(async tx => ...)` = interactive (cần read-modify-write) +- Migrations: `prisma migrate dev` (local) vs `migrate deploy` (CI/CD — không generate lại SQL ở prod) + +## 9.7 Senior modeling + +- Money: `INTEGER` minor unit (cents) hoặc `NUMERIC(18,2)` — không bao giờ `FLOAT` (0.1+0.2 ≠ 0.3) +- Timezone: `timestamptz` (lưu UTC), display theo user TZ ở app +- Status: enum Postgres (an toàn, mạnh về query) vs varchar + app enum (thêm giá trị không cần migration) — trade-off đã chốt ở L0.3 +- ID: UUIDv7 (sortable, không lộ số lượng) vs bigserial (nhỏ hơn, index nhanh hơn, lộ "mình có 1024 user") + +## 9.8 Cache–DB consistency + +Cache-aside: đọc cache → miss → DB → set cache. Problem: record đổi giữa lúc đọc DB và set cache → cache cũ vĩnh viễn. +Fix thực dụng: TTL ngắn (60s) + invalidation by tag khi ghi (`revalidateTag` L6.5 chấp nhận stale cửa sổ nhỏ). Viết-through: ghi DB xong update cache luôn — chậm hơn 1 chút, stale ít hơn. Write-behind: ghi cache + flush DB sau — chỉ dùng khi đo thấy DB bottleneck (phức tạp, tránh). + +## 9.9 Search + +`LIKE '%term%'` không dùng được B-tree, full scan. Bậc thang: +1. `pg_trgm` + GIN index — fuzzy vừa phải +2. `tsvector` + GIN — full-text tiếng Anh có ranking; tiếng Việt không có word boundary → cần pg_bigm/pg_trgm hoặc tokenizer ngoài +3. Meilisearch/Typesense — khi cần typo tolerance, faceting, instant search + +## 9.10 Scale out + +- Read replica: tách báo cáo/dashboard; LƯU Ý replication lag — vừa ghi xong đọc replica thấy dữ liệu cũ (bug production thật) +- Sharding bằng hash: chỉ khi 1 node thật sự chạm trần (đo, rồi hãy nói) +- Partition table theo `created_at`: phù hợp log/events, query luôn kèm time range + +## 9.11 Zero-downtime migration (expand–contract) + +``` +1. thêm cột nullable (release 1) → code cũ+code mới đều chạy +2. backfill + dual-write → release 2 +3. đọc cột mới, bỏ cột cũ → release 3 +4. DROP cột cũ → release 4 +``` +Never: rename/drop cột cùng release với code dùng nó — window deploy = sập. + +**Check:** cho table `orders` 50M dòng, query `WHERE status='pending' AND user_id=? ORDER BY created_at LIMIT 20` chạy 4s. Viết câu trả lời: index nào, tại sao thứ tự cột như vậy, EXPLAIN sẽ đổi Seq→Index như thế nào. diff --git a/learn/senior-web-dev/10-api-design.md b/learn/senior-web-dev/10-api-design.md new file mode 100644 index 0000000..84362ca --- /dev/null +++ b/learn/senior-web-dev/10-api-design.md @@ -0,0 +1,90 @@ +# L10 — API design + +## 10.1 REST core + +``` +GET /orders?cursor=eyJpZCI6MTIzfQ&limit=20 # cursor = base64(opaque) +POST /orders → 201 + Location +PUT /orders/42 → idempotent (replace) +PATCH /orders/42 → partial, JSON Merge Patch hoặc JSON Patch +``` +Pagination: OFFSET 100000 = scan+vứt 100k dòng → chậm dần. Cursor: +```sql +WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT 20 +-- keyset: ổn định khi có insert giữa trang, không nhảy/trùng record +``` + +## 10.2 Idempotency + +``` +POST /payments +Idempotency-Key: 7d3a-... # client sinh (UUID), gửi lại khi retry +``` +Server: `UNIQUE(idempotency_key)` — request trùng trả về KẾT QUẢ ĐÃ LƯU của request đầu, không tạo payment mới. Bắt buộc cho: tiền, đơn hàng, webhook consumer. + +## 10.3 Versioning + +Additive-first: thêm optional field = không break ai → không cần version. Break (đổi type, xoá field) → version. URL (`/v2`) dễ debug + CDN cache riêng; header sạch sẽ nhưng debug khó hơn. Org thường: URL. +Deprecation policy: trả `Deprecation` + `Sunset` header, báo trước ≥ 1 chu kỳ release. + +## 10.4 Error contract (RFC 7807) + +```json +{ "type": "https://api.x.com/errors/out-of-stock", "title": "Out of stock", + "status": 409, "code": "OUT_OF_STOCK", "detail": "Sản phẩm SP1 còn 2, bạn đặt 3" } +``` +Client switch trên `code` (ổn định), hiển thị `detail` (thay đổi được). Đừng bắt client parse message. + +## 10.5 Type-first API + +Nest + `@nestjs/swagger`: DTO decorator → OpenAPI JSON → client type tự sinh. +tRPC (Next↔Nest cùng monorepo): type suy ra từ server, không schema file. Trade-off: tRPC lock 2 đầu vào TS/Nest, mất OpenAPI cho team ngoài → tổ chức 2 audience (internal + public) thì cả hai, mỗi loại cho mỗi audience. + +## 10.6 GraphQL + +Chọn khi: nhiều frontend khác nhau cần shape dữ liệu khác nhau trên cùng graph; tránh khi: auth phức tạp theo field, hoặc bạn chưa xử lý được N+1 (DataLoader) + persisted query + depth limit. Chi phí ẩn: 1 endpoint = rate limit khó hơn, caching CDN mất gần hết (vì POST /graphql một URL). + +``` +// N+1 GraphQL: products → 50 resolver con gọi 50 query — fix bằng DataLoader batch +``` + +## 10.7 Upload (org standard: inline multipart) + +``` +POST /orders (multipart/form-data) + fields: dto (JSON string) + files: receipt[] (binary parts) +→ mediaUpload interceptor: validate mime/size → ghi storage → thay parts bằng URL trong req.body → DTO validation thấy url +``` +Vì sao anti-pattern presign-PUT ở đây: thêm service + thêm round-trip + client tự quản lý expiry, trong khi mọi endpoint đã auth sẵn. Nêu được 1 lý do presign THẮNG (upload 5GB thẳng lên S3 không đi qua app) = trung thực trade-off. + +## 10.8 Rate limiting + +``` +Sliding-window (Redis sorted set): chính xác, cần Redis +Token bucket: cho phép burst (bucket size) + refill rate — chọn mặc định +Key: user:{id} (chặn theo tài khoản — đúng hơn IP vì NAT), fallback IP +→ 429 + Retry-After: 30 +``` +Multi-instance: limiter phải ở Redis/edge, không phải in-memory counter (mỗi instance 1 sổ = limit × N). + +## 10.9 Webhook outbound + +``` +POST khách hàng, headers: + X-Webhook-Signature: t=1712345,v1=hmac_sha256(secret, t + "." + raw_body) +receiver: so sánh timing-safe; body + timestamp chống replay +retry: 1m, 5m, 30m... 5 lần → DLQ + cảnh báo; receiver phải idempotent (delivery id) +``` + +## 10.10 Realtime + +SSE: notification, log tail, LLM tokens. WebSocket: cùng vẽ, chat, presence. +Multi-instance: socket kết nối vào instance A, event publish từ instance B → Redis pub/sub làm bus (L15.7). Auth WS: cookie/ticket lúc upgrade, verify trước khi accept. + +## 10.11 Public API thinking + +- mọi field là hợp đồng: xoá = break +- changelog + deprecation header (10.3), sandbox key, pagination bắt buộc cho list, `Idempotency-Key` cho mọi POST tiền + +**Check:** thiết kế API "chuyển khoản" — trên giấy: method, status codes, idempotency, rate limit key, error contract. 5 phút, không tra. diff --git a/learn/senior-web-dev/11-auth-security.md b/learn/senior-web-dev/11-auth-security.md new file mode 100644 index 0000000..117130d --- /dev/null +++ b/learn/senior-web-dev/11-auth-security.md @@ -0,0 +1,76 @@ +# L11 — Auth & security + +## 11.1 Session vs JWT + +Web app có logout: +- Session server: `sid` cookie HttpOnly → lookup Redis/DB → revocable tức thì. Giá: 1 lookup/request (Redis p99 < 1ms — nói được con số). +- JWT stateless: không lookup, NHƯNG logout phải blacklist (lại thành stateful). JWT còn lộ vấn đề rotation key (JWKS) và token sống hết TTL dù user bị khoá. + +Kết luận senior: browser session → session cookie. JWT cho: service-to-service (mTLS hoặc JWT + short TTL), refresh token, mobile. + +## 11.2 OAuth2 / OIDC + +Authorization Code + PKCE (SPA/mobile = public client, không giấu được secret): +``` +1. redirect → authorize?client_id=..&code_challenge=SHA256(verifier)&method=S256 +2. callback: ?code=abc +3. POST /token: code + code_verifier (rò ra network cũng dùng được gì? không — verifier khác challenge) +4. access (15') + refresh (rotation: mỗi lần dùng trả refresh MỚI, refresh cũ dùng lại = reuse → revoke cả family) +``` +`scope` = client xin gì; permission = user được gì — 2 trục khác nhau, đừng gộp. + +## 11.3 Password + +- hash ≠ encrypt: không có đường ngược lại +- argon2id (memory-hard: GPU/ASIC không brute-force rẻ như với bcrypt) +- pepper (secret riêng app, khác salt per-user) khi DB rò vẫn thiếu 1 mảnh +- reset: token random 256bit, HASH lưu DB, single-use, TTL 15', gửi qua email kèm invalidate các token cũ + +## 11.4 OWASP top hits (kèm fix cụ thể) + +```js +// SQLi: ORM không tự miễn khi bạn ghép chuỗi +prisma.$queryRawUnsafe(`... WHERE name = '${name}'`) // ❌ +prisma.$queryRaw`... WHERE name = ${name}` // ✅ parameterized +// SSRF: user truyền imageUrl, server fetch → attacker thử http://169.254.169.254/ (metadata), http://localhost:9200 +// fix: allowlist domain, chặn private IP ranges, không theo redirect +// Path traversal: filename từ user: "../../etc/passwd" → basename + uuid rename, không dùng tên gốc +// Mass assignment: whitelist:true pipe (L8.5) +``` + +## 11.5 Security headers baseline + +``` +Content-Security-Policy: default-src 'self'; script-src 'nonce-…' (Next: next/headers nonce hoặc config CSP) +Strict-Transport-Security: max-age=63072000; includeSubDomains +X-Frame-Options: DENY / CSP frame-ancestors 'none' +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: geolocation=(), camera=() // tắt những gì không dùng +``` + +## 11.6 Secrets + +- `NEXT_PUBLIC_*` = BUNDLED VÀO JS — client thấy. Secret thật: server-only (env runtime, không build arg) +- runtime env từ secret manager (SSM/Vault), không nằm trong image +- CI: secret scanning trong pipeline (org audit rule soi đúng mục này), rot khi nghi ngờ rò + +## 11.7 Authorization + +RBAC: `user.role IN ('admin')` trong guard — đủ khi permission đơn giản. +ABAC/policy: "chỉ sửa order CỦA MÌNH TRỪ khi manager" — đặt ở service vì cần context record. + +Confused deputy — lỗi số 1 của mid-level: +```ts +// ❌ req.user chỉ để check đăng nhập, lấy id TỪ BODY +updateOrder(req.user.id, dto.orderId) → sửa được order của người khác nếu DTO.orderId của ai đó +// ✅ query ràng buộc owner: WHERE id = dto.orderId AND user_id = req.user.id +// hoặc policy so sánh owner TRƯỚC khi mutate +``` + +## 11.8 Supply chain + +- commit `package-lock.json`, CI dùng `npm ci` +- `npm audit` + Dependabot/Renovate; pin major version +- postinstall script là vector (event: sự cố package bị chiếm → script cài miner): `ignore-scripts=true` cho deps không cần build, hoặc allowlist qua `onlyBuiltDependencies` (npm 10+) + +**Check:** user báo "tôi logout rồi mà tài khoản vẫn bị truy cập qua link cũ". Bạn điều tra theo thứ tự nào? (token TTL → refresh rotation có bật không → cookie scope/`SameSite` → session store revocation → log phát hiện reuse detection) diff --git a/learn/senior-web-dev/12-performance.md b/learn/senior-web-dev/12-performance.md new file mode 100644 index 0000000..6852833 --- /dev/null +++ b/learn/senior-web-dev/12-performance.md @@ -0,0 +1,60 @@ +# L12 — Performance engineering + +## 12.1 Nguyên tắc: không số liệu = không claim + +Quy trình 4 bước lặp lại mọi lúc: ① chọn metric (p95 latency, không phải median — user nằm ở đuôi) ② đo hiện trạng ③ sửa 1 thứ ④ đo lại, giữ nếu thắng. + +## 12.2 Backend diagnosis + +```bash +# event loop lag metric (prom-client + monitorEventLoopDelay) tăng → tìm task dài +node --cpu-prof server.js # .cpuprofile → DevTools flame graph: đỉnh rộng = hàm ngốn CPU +clinic.js # autocannon -c 100 http://localhost:3000 (tải giả lập) +``` +Đỉnh flame graph thường là: `JSON.parse` body 10MB, synchronous crypto, regex backtrack, deep clone. + +## 12.3 N+1 ở tầng API + +Trang dashboard gọi 40 endpoint (40 × TLS + 40 × auth + 40 × RTT). Fix sai: "thêm React Query cache". Fix đúng: composition endpoint — `GET /dashboard` server tổng hợp 1 response. Client cache chỉ che triệu chứng, không giảm work. + +## 12.4 Bundle frontend + +```bash +ANALYZE=true next build # next/bundle analyzer: nhìn biểu đồ tree-map +``` +Thủ phạm lặp lại: moment (dùng `Intl`), lodash cả con (dùng `lodash-es` hoặc import hàm lẻ), chart lib nạp cả bundle cho 1 widget, client component ở GỐC kéo theo mọi thứ (L6.3 boundary). + +## 12.5 TTFB chain + +`DNS → TCP → TLS → TTFB → streaming` — với Next: TTFB = middleware + server render + DB. +KPI nói trong interview: "p95 TTFB 300ms → 80ms bằng cách bỏ query DB khỏi middleware (đổi sang edge config) + cache header theo user." + +## 12.6 Image & media + +`srcset` đúng (điện thoại không tải 2560px), CDN cache ảnh resize (`Cache-Control: public, max-age=31536000, immutable`), text/CSS/JS nhỏ. Chi tiết nằm ở 4.5 + 6.8. + +## 12.7 Cache pyramid + +``` +in-process Map (1 instance, bay khi deploy) → chỉ cho dữ liệu hot, TTL ngắn, chấp nhận miss khi scale +Redis (dùng chung, p99 ~1ms) → session, rate limit, cache query +CDN (edge, gần user) → asset, trang cache được +browser cache → free, kiểm soát qua Cache-Control +``` +Mỗi tầng invalidate KHÁC nhau: in-process không có cách broadcast invalidation → dùng Redis pub/sub hoặc chấp nhận TTL. Interview: "cache user profile của bạn invalidate khi user đổi tên ở instance khác bằng cách nào?" + +## 12.8 Queue offload + +Câu hỏi quyết định: request CÓ CẦN kết quả để trả lời không? +- có → giữ trong request, thêm timeout +- không (email, resize, notification, đối soát) → job queue, trả 202 Accepted +Quy tắc 202 + polling/webhook thay vì giữ connection 30s. + +## 12.9 DB performance + +```sql +log_min_duration_statement = 200ms → log query chậm +SELECT * FROM pg_stat_activity WHERE state = 'active' AND now()-query_start > '5s'; -- long-running +pg_locks: lock contention khi UPDATE cùng dòng nóng → thiết kế lại (decrement atomic) +``` +Index covering (9.2) sửa được "quét nhiều"; không sửa được "row quá rộng + fetch tất" — đó là việc của `select` columns. diff --git a/learn/senior-web-dev/13-testing.md b/learn/senior-web-dev/13-testing.md new file mode 100644 index 0000000..68ce394 --- /dev/null +++ b/learn/senior-web-dev/13-testing.md @@ -0,0 +1,60 @@ +# L13 — Testing + +## 13.1 Phân bổ thực dụng + +``` +unit (nhiều, ms): logic thuần — pricing rules, state machine, parser, validator +integration (vừa): 1 service + testcontainer PG/Redis — repository, transaction, authz +e2e Playwright (ít): 5–15 kịch nghiệp vụ quan trọng nhất, chạy API THẬT (org rule) +``` +Tỉ lệ sai điển hình: 80% e2e chậm chạp + flaky, hoặc unit test mock hết tới mức refactor là chết hết. + +## 13.2 Testable = thiết kế đúng + +```ts +// không test được: phụ thuộc ẩn, Date.now() randomness, global singleton mới trong hàm +// test được: ranh giới rõ, truyền qua DI +@Injectable() class ExpiryService { constructor(private clock: Clock, private repo: CouponRepo) {} } +// test: fake clock → coupon hết hạn lúc 23:59:59 test được deterministic +``` +Code khó test = thiết kế sai, không phải "test viết khó". + +## 13.3 Mock đúng chỗ (org rule) + +``` +mock: payment gateway, email provider, OCR, thời gian +KHÔNG mock: DB (testcontainers), HTTP layer của chính app (supertest), queue (BullMQ dùng memory Redis) +``` +Mock-echo test = slop: +```ts +it('gọi create', () => { svc.create(x); expect(repo.create).toHaveBeenCalledWith(x); }); // test rỗng — chỉ assert mock với chính nó +``` + +## 13.4 TDD ở đâu + +Đáng: business rule có branching (pricing, phân quyền, state transition), parser, thuật toán sync dữ liệu. +Không đáng: CRUD scaffold, wiring DI, component presentational. +Vòng đỏ-xanh-nâu: viết test fail bằng đúng hành vi mong đợi → code tối thiểu cho pass → refactor. + +## 13.5 Flaky + +```ts +// ❌ await new Promise(r => setTimeout(r, 2000)); // đoán thời gian +// ✅ +await expect(page.getByText('Chào mừng')).toBeVisible(); // auto-wait condition +await poll(() => db.order.count({ where: { status: 'done' } }) === 1, { timeout: 5000 }); +``` +Nguồn flaky #1: state chia sẻ giữa test (cùng DB row, cùng Redis key) → mỗi test 1 dataset riêng hoặc rollback transaction. + +## 13.6 Coverage + +Dùng để tìm vùng tối (branch nào chưa ai đi qua), KHÔNG chạy theo %. Test có assert về hành vi sai khi code sai; test tautology `expect(true).toBe(true)` / snapshot 500 dòng không ai đọc = nợ, không phải tài sản. + +## 13.7 Playwright tổ chức + +```ts +// storageState: login 1 lần, tái dùng cho các test sau (auth setup project) +// fixtures: test.extend({ order: async ({ db }, use) => { const o = await seed(); await use(o); await cleanup(); } }) +// CI: --retries=0 cho main branch (flaky lộ rõ), artifact video+trace khi fail +npx playwright test --project=chromium --workers=4 // song song mặc định theo file +``` diff --git a/learn/senior-web-dev/14-devops-infra.md b/learn/senior-web-dev/14-devops-infra.md new file mode 100644 index 0000000..041dba6 --- /dev/null +++ b/learn/senior-web-dev/14-devops-infra.md @@ -0,0 +1,75 @@ +# L14 — DevOps / infra + +## 14.1 Container + +```dockerfile +# syntax=docker/dockerfile:1 +FROM node:22-alpine AS build +WORKDIR /app +COPY package*.json ./ # layer riêng: không đổi deps → cache ăn được +RUN npm ci +COPY . . +RUN npm run build + +FROM node:22-alpine # image chạy: KHÔNG có devDeps, không có source +USER node # non-root +COPY --from=build /app/dist /app +CMD ["node", "main.js"] # exec form: PID 1 nhận SIGTERM → graceful shutdown +``` +cgroup: `NODE_OPTIONS=--max-old-space-size` khớp limit (L7.6). Healthcheck: `/healthz` nhẹ, không query DB (health check phụ thuộc DB = báo động nhầm khi DB chậm). + +## 14.2 CI/CD + +Pipeline chuẩn: `lint + typecheck → unit → build (1 lần) → integration → e2e → push image (promote cùng image qua các env)`. +Build once, deploy the same artifact — không "build lại ở staging". +Preview env per PR (pattern Vercel): mỗi PR 1 URL, data dev DB. + +```yaml +# cache đúng cách: key theo lockfile +- uses: actions/setup-node@v4 + with: { node-version: 22, cache: npm } +``` + +## 14.3 Environment & flags + +12-factor: config = env, secret = secret manager. Feature flag tách DEPLOY khỏi RELEASE: code lên prod tối nay, bật cho 5% user sáng mai, tắt không cần rollback (org: kill-switch cho tính năng rủi ro). + +## 14.4 Observability 3 trụ + +```jsonc +// LOG: structured + correlation id xuyên hệ thống +{"level":"warn","msg":"payment failed","reqId":"abc","userId":"u1","code":"GATEWAY_TIMEOUT","durationMs":1523} +``` +- METRIC: RED (Rate, Errors, Duration) per endpoint + USE per resource (CPU/mem/pool saturation) +- TRACE: OpenTelemetry, cùng `traceId` xuyên Nest → queue → worker → Next +`reqId` ở header `x-request-id` → sinh/tại edge, propagate mọi tầng — dashboard search 1 reqId ra toàn cảnh. + +## 14.5 Alerting & SLO + +Alert trên SYMPTOM: "p95 latency > 500ms trong 5p", "error rate > 1%", "checkout success < 99%". +KHÔNG alert trên cause: "CPU 90%" (CPU cao mà user không sao = không page). +SLO 99.9%/tháng = 43 phút error budget — hết budget = lockdown tính năng mới, trả reliability. Nói được câu này trong phỏng vấn = senior signal mạnh. + +## 14.6 Incident + +Thứ tự: detect → declare (gọi người, chỉ huy 1 người) → MITIGATE (rollback / feature-flag off / scale — chưa cần biết vì sao) → thông báo → sau đó mới root cause → blameless postmortem: "system cho phép lỗi đó xảy ra", không phải "ai gõ lỗi". + +## 14.7 Deploy an toàn + +- readiness probe: container chỉ nhận traffic khi app listen xong; drain connection cũ khi scale down +- Migration trước code (expand-contract L9.11) — deploy code đọc cột mới SAU khi cột tồn tại +- Canary 5% → xem metric RED 10' → promote; rollback = redeploy image cũ (đã có sẵn = không build vội) + +## 14.8 K8s/Terraform tối thiểu để tranh luận + +K8s: Deployment (N replica + rollout), Service (stable IP trước pods), Ingress (L7 route), HPA (auto-scale theo CPU/RPS), ConfigMap/Secret. Biết vì sao "pod CrashLoopBackOff" ≠ app sai (probe/limit/env). +Terraform: module = hàm có input/output, state = source of truth (lock khi team), `plan` trước `apply`, không sửa tay resource có trong state. + +## 14.9 Cost + +Serverless đắt khi: baseline traffic cao đều (trả cho mỗi ms liên tục) → server/containers rẻ hơn. Rẻ khi: traffic trồi thất thường. +Egress (data ra ngoài cloud) là hóa đơn bất ngờ số 1. DB storage: log/table sự kiện → partition + TTL delete. + +## 14.10 GitHub Actions (org standard) + +Reusable workflow: `uses: ./.github/workflows/build-test.yml` gọi từ CI/release/deploy (rule org: DRY). `/autofix`, `/ecosystem-ci` comment-triggered, gated permission (org rule). changesets/action release lib tự động khi merge. diff --git a/learn/senior-web-dev/15-system-design.md b/learn/senior-web-dev/15-system-design.md new file mode 100644 index 0000000..53fa624 --- /dev/null +++ b/learn/senior-web-dev/15-system-design.md @@ -0,0 +1,114 @@ +# L15 — System design (vòng quyết định) + +## 15.1 Framework 7 bước (45 phút) + +``` +1 (5'): Requirement. Functional: gì? Non-functional: QPS? p99? data volume? team? đọc/ghi tỉ lệ? +2 (5'): Estimation (15.2). Chốt con số to đùng nào ảnh hưởng quyết định. +3 (5'): API + data model. +4 (10'): High-level diagram. Client → LB → app → cache/DB/queue. +5 (10'): Deep dive theo interviewer chỉ (thường: 1 thành phần nổ to nhất). +6 (5'): Bottleneck + scale path: đọc replica, cache, sharding, queue. +7 (5'): Trade-off tổng: bạn đã đổi gì lấy gì. +``` +Luật vàng: mọi quyết định có câu "vì X, đổi lại là Y". Im lặng chọn công nghệ = fail. + +## 15.2 Estimation — số nên nhớ + +``` +1 ngày ≈ 10^5 giây +1k req/s × 1KB = 100MB/s ≈ 8TB/ngày +100M user × 1 req/ngày = 1.2k req/s trung bình → peak ×5 = 6k req/s +1 ngày 1M đơn × 2KB = 2GB/ngày → 700GB/năm (DB chính vẫn sống tốt) +RAM 1 máy 32GB ~ chứa 16M row × 2KB nếu cache nóng 50% +``` +Bài tập: "YouTube 300 giờ video upload/phút, mỗi video 1080p ~4GB" → bao nhiêu TB lưu trữ, bao nhiêu transcode giờ/ngày. (Estimate ra con số, không cần đúng, cần lộ cách nghĩ.) + +## 15.3 Load balancing + +L4: route theo IP/port (nhanh, không hiểu HTTP). L7: route theo path/host/header — cần cho canary, A/B, TLS termination. +Algorithm: round-robin (mọi thứ stateless), least-conn (request độ lệch lớn), consistent hash (khi cần sticky có lý do — cache warmth). +Sticky session là smell: giải pháp thật = state ra ngoài (session store Redis, JWT). + +## 15.4 Horizontal scaling app + +Điều kiện tiên quyết: app vô state. Việc phải làm: +- session → Redis/JWT (11.1) +- cache in-process → Redis (12.7) +- background job trong process → queue (15.7) +- local upload disk → object storage (S3) +Nói được 4 thứ trên = trả lời xong "scale từ 1 → 10 instance làm gì". + +## 15.5 Caching chiến lược + +- cache-aside (mặc định), write-through, write-behind (9.8) +- invalidate by tag: sản phẩm đổi → xoá `product:42` + mọi `page:*` gắn tag đó +- thundering herd: 10k request cùng miss 1 key → 10k query DB. Fix: **single-flight** (1 request đi về nguồn, số còn lại chờ kết quả đó) hoặc jitter TTL ±10%. + +```ts +// single-flight: mọi request cùng key dùng chung 1 promise đang chạy +const inflight = new Map<string, Promise<any>>(); +async function get(key: string) { + if (inflight.has(key)) return inflight.get(key)!; + const p = loadFromDB(key).finally(() => inflight.delete(key)); + inflight.set(key, p); return p; +} +``` + +## 15.6 Database scale + +Bậc thang (leo từng bậc, đừng nhảy): +``` +1 instance to hơn → index/query tối ưu (L9) → read replica (báo cáo, đọc) +→ cache giảm tải đọc → partition table sự kiện → sharding theo tenant/user → CQRS tách write model +``` +Replication lag: "vừa đặt hàng, trang confirm đọc replica → chưa thấy đơn" → pattern: đọc từ primary trong session của chính user đó (read-your-writes). +Sharding key: `user_id` (thời điển hình: mọi query của 1 user nằm 1 shard) vs hash(order_id) — đo access pattern trước khi chọn. + +## 15.7 Async / messaging + +``` +BullMQ (Redis, <10k job/s, dễ — org đang có): retry/backoff/DLQ có sẵn +Kafka: event log nhiều consumer, replay, >100k msg/s +SQS: serverless, không thứ tự tuyệt đối +``` +Delivery semantics: network không có exactly-once — "exactly-once" thật = at-least-once + **idempotent consumer** (dedupe key, upsert). +Outbox pattern — khi DB write + event phải nguyên tử: +```sql +BEGIN; UPDATE orders SET status='paid'; INSERT INTO outbox(event, payload); COMMIT; +-- worker: đọc outbox → publish Kafka/queue → đánh dấu sent (at-least-once, consumer idempotent) +``` +Vì sao không publish thẳng sau commit: commit xong, crash trước khi publish = event mất vĩnh viễn, downstream lệch. + +## 15.8 Resilience patterns + +- timeout ở MỌI tầng gọi ra ngoài (app→DB, app→payment); không timeout = thread/connection leak khi chậm +- circuit breaker: 5 lỗi liên tiếp → mở mạch 30s → thử lại nửa_open_ — chặn cascade, cho dịch vụ bệnh nghỉ +- bulkhead: pool riêng cho payment và report (1 cái nghẽn không giết cái kia) +- backpressure: 429 + Retry-After (10.8), queue có giới hạn + drop policy + +## 15.9 Real-time hệ lớn + +Feed/chat: fan-out on write (post → ghi N inbox người follow, đắt khi follow 1M — celebrity problem) vs fan-out on read (đọc gộp realtime, rẻ khi ghi nhiều). +Giải celebrity: hybrid — sao dùng on-read, người thường on-write; cache timeline. +Presence: Redis + heartbeat TTL, socket multi-instance qua pub/sub (L10.10). + +## 15.10 Sáu bài kinh điển — outline decisions (luyện mỗi bài 30', vẽ + nói to) + +**URL shortener**: POST /{code} lưu `code→url` (hash 7 ký tự base62, unique constraint + retry). Đọc: cache Redis 90% hit, DB sharding theo code. Vấn đề: collision, read:write 100:1 → cache là chính. +**Notification system**: API nhận → queue → worker per kênh (email/push) với retry backoff + idempotency key (gửi trùng = bug user-visible). Preference + rate cap per user chống spam. DLQ khi 5 lần fail. +**News feed**: 15.9 fan-out hybrid. Feed cache theo user, TTL ngắn, cập nhật bằng append. +**Chat**: WebSocket + heartbeat; lưu message per-conversation shard; "message được gửi" ≠ "đã đọc" → ack 2 tầng (delivered, read); history = DB, realtime = pub/sub. +**Web rate limiter**: 15.8 + Redis sliding window (sorted set: `ZADD key now id; ZREMRANGEBYSCORE`), atomic bằng Lua script (client nào đến trước cũng thấy cùng bức tranh). +**YouTube-lite upload**: client → presigned S3 (upload 4GB không qua app — trường hợp presign THẮNG 10.7) → event upload.done → queue → transcode farm (worker riêng) → nhiều bitrate → CDN. Storage: original + derivatives; cost: egress lớn nhất. + +**Distributed lock**: Redis `SET key token NX PX 30000` + release bằng Lua so token. Nói được vì sao NGUY HIỂM: lock expiry trong khi việc chưa xong → 2 holder; fencing token (số tăng dần, resource từ chối token cũ) mới là fix thật. Interview bẫy: "dùng lock phân tán để decrement stock" — sai, dùng DB atomic update (9.3). + +## 15.11 Trade-off vocabulary (dùng thành câu) + +- consistency vs availability: chọn C cho balance (double-spend không chấp nhận), chọn A cho view count +- latency vs throughput: batch (throughput↑, latency↓) cho báo cáo; realtime từng dòng cho checkout +- monolith → microservice khi: team >2 sprint-autonomous teams, cần scale/scale team khác nhau — và recovery path khi sai: modular monolith với module boundary thật (L8.2, L16) là đường quay lại dễ hơn chia tay rồi hàn +- build vs buy: auth (Clerk/Auth0) vs domain core (không bao giờ buy) + +**Check cuối tầng:** làm lại 1 bài trong 15.10 bằng cách ghi âm 30 phút tự thuyết trình, nghe lại đếm được bao nhiêu câu "vì... đổi lại...". diff --git a/learn/senior-web-dev/16-architecture-craft.md b/learn/senior-web-dev/16-architecture-craft.md new file mode 100644 index 0000000..1054729 --- /dev/null +++ b/learn/senior-web-dev/16-architecture-craft.md @@ -0,0 +1,63 @@ +# L16 — Architecture & craft + +## 16.1 Deep module (org rule) + +Depth = hành vi che được bao nhiêu trên mỗi đơn vị interface. + +``` +Shallow module: 10 hàm export / 50 dòng logic → interface gần bằng implementation → vô dụng +Deep module: 1 hàm `sendOtp(phone)` → giấu: rate limit, retry, template, provider fallback +``` +Ví dụ org: `packages/otp` chỉ export `sendOtp`, `verifyOtp`. Consumer không biết provider là Twilio hay FalconSMS. Đổi provider = sửa bên trong, không đổi caller. + +## 16.2 Dependency direction + +``` +domain/ ← không import NestJS, không import Prisma (chứa rule + entity thuần) +application/ ← use cases, nhận interface (ports) +infrastructure/→ implements ports: Prisma repo, Twilio client (adapters) +interfaces/ → Nest controllers, Next server actions +``` +Hexagonal đáng khi: business rule phức tạp, sống >5 năm (fintech). Quá đáng khi: CRUD nội bộ 3 tháng sống — lúc đó domain không import framework = ceremony rỗng. Nói được cả 2 chiều = điểm senior. + +## 16.3 DDD chiến thuật (đủ dùng) + +```ts +// Value object: equality theo giá trị, tự validate lúc tạo +class Money { constructor(private cents: number) { if (cents < 0) throw ... } } +// Entity: có identity (OrderId), trạng thái đổi, identity giữ nguyên +// Aggregate: Order = root; thêm item PHẢI đi qua Order.addItem() — invariant "tổng = Σ items" +// không ai sửa OrderItem trực tiếp → repo load/save theo aggregate +// Domain event: OrderPaid → listener gửi email, ghi ledger — side effect ra khỏi transaction chính (qua outbox 15.7) +``` +Bounded context: `Orders` (sales) và `Fulfillment` (warehouse) cùng có khái niệm "Order" nhưng shape khác → 2 model riêng + anti-corruption layer dịch giữa. + +## 16.4 Context map (org đang làm sẵn) + +`CONTEXT-MAP.md` → mỗi app có `CONTEXT.md` glossary. Task của bạn: khi implement, dùng đúng từ trong glossary; phát hiện term mơ hồ → challenge (đây là skill bạn vừa học: domain-modeling). + +## 16.5 Consistency giữa services + +- 1 transaction 1 DB: invariant phải nằm trong 1 aggregate/shard +- saga orchestration (1 điều phối, dễ theo dõi, đi kèm single-point) vs choreography (event dây chuyền, không ai sở hữu flow → debug địa ngục khi >3 bước) +- compensation: step 2 fail → chạy step 1 ngược (trừ tiền → hoàn tiền), thiết kế MỖI bước có bước lùi trước khi viết +- eventual consistency chấp nhận ở: search index, analytics, notification. KHÔNG chấp nhận ở: số dư, kho hàng, vé máy bay. + +## 16.6 Code smell senior soi (org audit list) + +```ts +const user = (res as any).data.user as User; // type-bypass cast = hệ thống đang nói dối bạn +if (state === 'x' && foo) {...} // switch scattered: cùng 1 state machine nếu khắp 5 file +``` +Smells: shared mutable module state, abstraction có đúng 1 implementation, config cho giá trị không đổi bao giờ, comment kể lại code ("// set name to name"), `try{}catch(e){}` swallow, deep nesting 4 tầng. + +## 16.7 Refactor chiến thuật + +1. tạo seam để test được (chèn interface DI vào chỗ không test được) +2. viết test khoá hành vi cũ +3. refactor trong test +4. strangler fig: thay hệ thống cũ bằng cách dựng lớp mới SONG SONG, migrate theo traffic % (route by tenant), không bao giờ rewrite-big-bang + +## 16.8 Error design + +Boundary: domain → trả `Result`/throw DomainError; transport (Nest filter) → dịch 1 chỗ sang HTTP. Service KHÔNG import HttpStatus. Lỗi lạ → crash loud + filter chung → 500 + log. Không `catch(e) { return null }` — mất thông tin, bug không tìm được thủ phạm. diff --git a/learn/senior-web-dev/17-leadership-behavioral.md b/learn/senior-web-dev/17-leadership-behavioral.md new file mode 100644 index 0000000..6f03d67 --- /dev/null +++ b/learn/senior-web-dev/17-leadership-behavioral.md @@ -0,0 +1,39 @@ +# L17 — Leadership & behavioral + +## 17.1 Công thức kể chuyện tech (STAR + quyết định + số) + +Cấu trúc mỗi câu ≤ 2 phút: +``` +S: "Checkout p99 4s, tỉ lệ fail 2% giờ cao điểm." +T: "Em owning dịch vụ payment, 3 tuần tới Tết." +A: "Profile flame graph → N+1 query trong auth middleware gọi DB mỗi request. Chọn cache session Redis + guard chỉ đọc cache, đổi lại: revocation chậm 60s — thống nhất với team security vì window 60s chấp nhận được." +R: "p95 4.2s → 380ms, fail rate 0.3%. Runbook + dashboard RED cho oncall." +``` +Chữ A phải có: lựa chọn + từ chối phương án nào + vì sao + số đo. Không có số = không có câu chuyện. + +## 17.2 Xung đột kỹ thuật + +Ví dụ mẫu: "Team muốn microservice cho tính năng mới, em phản đối." +→ không kể "em đúng": kể bạn đưa tiêu chí (team size, deploy độc lập không, data coupling), demo chi phí (2 tuần infra để đổi 1 module), chốt pilot modular monolith với module boundary enforce bằng dependency-cruiser, sau 6 tháng tách được thật khi cần. Kết: disagree & commit, ship both ways an toàn. + +## 17.3 Mentoring + +- code review: comment vào code, không vào người; giải thích NGUYÊN TẮC + dẫn link rule/docs; đánh dấu loại nào là *blocking* vs *suggestion* (junior không đoán được comment nào bắt buộc sửa) +- 1:1 junior: hỏi "chỗ nào blocker" + "muốn học gì", không phải "tiến độ tới đâu" +- doc: viết runbook để người KHÁC vận hành được = bạn không bị oncall mãi + +## 17.4 Incident ownership (câu hỏi gần như chắc chắn có) + +Kể 1 sự cố bạn gây ra hoặc cứu. Điểm cộng: phát hiện trước khách hàng (alert), mitigation nhanh (flag off), sau đó FIX QUY TRÌNH (thêm CI check, đổi template), không đổ người. + +## 17.5 Estimate & say no + +- "Em estimate 5 ngày, sai số ±30%. Rủi ro chính: API đối tác chưa có sandbox. Siết được khi spike 1 ngày trước." +- Say no + alternative: "Làm đủ 100% tính năng này cần 6 tuần. Trong 3 tuần: 80% giá trị, bỏ phần X (chưa ai dùng nhiều). Chọn?" +- "Không biết": "Em chưa chạy Postgres ở scale đó. Nguyên tắc em dùng là X, nhưng cần benchmark thật trước khi chốt." — nói sớm trong interview, không nói ở production. + +## 17.6 Tech debt + +Định nghĩa đo được: velocity giảm, bug cùng 1 module lặp, thời gian onboarding tăng. +Chiến thuật trả: tax 10–20% mỗi sprint, opportunistic (sửa module nào thì dọn module đó), không "sprint dọn dẹp toàn hệ thống" (PM sẽ không duyệt, đúng thôi). +Nghệ thuật negotiate: "Nếu không trả nợ chỗ này, quý sau mỗi feature +40% thời gian. Bằng chứng: commit history module X." diff --git a/learn/senior-web-dev/18-phong-van.md b/learn/senior-web-dev/18-phong-van.md new file mode 100644 index 0000000..7835b08 --- /dev/null +++ b/learn/senior-web-dev/18-phong-van.md @@ -0,0 +1,56 @@ +# L18 — Kỹ năng phỏng vấn (skill riêng, khác năng lực) + +## 18.1 Live coding — quy trình 6 bước + +``` +1. Đọc hết đề + ví dụ. Nói ra input/output bạn hiểu — confirm với interviewer (bắt edge case ở đây) +2. Hỏi: dữ liệu cỡ nào? đã sorted? trùng lặp? unicode? (câu hỏi đúng = điểm, không phải yếu) +3. Nói hướng đi trước khi code, 30 giây. Được gật → code. +4. Code phần chính trước, edge sau. Chạy/test tay 1 case thường + 1 case biên +5. Nếu stuck: nói to suy nghĩ, liệt kê ứng viên — interviewer help người nói ra hướng, không help người im lặng +6. Pass rồi mới hỏi: "em refactor tách hàm được không?" — làm gọn, không làm lại từ đầu +``` +Anti-pattern: code luôn không hỏi (giả định sai → sửa cả bài), hoặc xin hint mỗi 2 phút không tự thử gì. + +## 18.2 Hỏi ngược — senior hỏi nhiều hơn nói + +Đầu system design: "dữ liệu nào đọc nhiều nhất?", "team hiện tại mấy người vận hành cái này?", "có ràng buộc stack nào không?" +Cuối văn hoá: "quyết định kỹ thuật gần nhất nào team đổi vì phản biện của một người?" — câu trả lời lộ maturity team thật, và bạn đang audit họ. + +## 18.3 Đồng hồ system design 45' + +``` +0-5 requirement + chỉ tiêu +5-10 estimation, chốt con số quyết định +10-15 API + data model (viết gọn, đừng vẽ đẹp) +15-25 high-level diagram — XIN interviewer xác nhận trước khi deep dive ("em đi sâu vào cache được không?") +25-35 deep dive — thành phần interviewer chỉ +35-40 bottleneck + kế hoạch scale bậc tiếp +40-45 TÓM TẮT trade-off — bước này hay bị hết giờ, luyện để còn 5' +``` + +## 18.4 Chuỗi "tại sao X" — luyện với từng tool bạn dùng + +Mỗi tool phải trả lời 3 tầng: +``` +"Sao dùng BullMQ mà không Kafka?" → quy mô msg/s + org đã có Redis +"Sao không RabbitMQ?" → chưa cần routing phức tạp +"Sau này thay được không?" → interface ở application layer, producer/consumer tách qua message schema +``` +Tool nào trả lời tầng 3 bí → về đọc lại tầng tương ứng (L14/L15). + +## 18.5 Take-home + +``` +trước khi code: đọc test spec (nếu có), viết README stub: phạm vi, trade-off chọn, cái gì bạn CỐ Ý bỏ +trong khi: commit nhỏ, message "why"; không commit file rác +nộp: 3 phần README = ① cách chạy ② quyết định + đánh đổi ③ "nếu có thêm 1 ngày em sẽ..." (bước này người ta chấm senior rất nặng) +không over-engineer: không Docker+K8s cho 1 CRUD API +``` + +## 18.6 Chuẩn bị 6 câu chuyện STAR + +Mỗi câu ≥ 1 số đo, cover: ① incident ownership ② trade-off technical ③ xung đột & commit ④ mentoring ⑤ deadline negotiation ⑥ học cái mới từ số 0. +Viết ra giấy, tập nói 2 phút/câu, tiếng Việt trước rồi tiếng Anh (đừng học thuộc — nắm khung 17.1). + +**Check cuối cùng:** mock interview 45' system design với đồng hồ thật + ghi âm, tự chấm theo 15.1: đủ 7 bước chưa, bao nhiêu câu "vì… đổi lại…". diff --git a/learn/senior-web-dev/LEARNING-PATH.md b/learn/senior-web-dev/LEARNING-PATH.md new file mode 100644 index 0000000..ededd68 --- /dev/null +++ b/learn/senior-web-dev/LEARNING-PATH.md @@ -0,0 +1,335 @@ +# Senior Web Dev Knowledge Map — NestJS / Next.js / TypeScript + +Mục tiêu: năng lực senior dài hạn. Phỏng vấn là cột mốc kiểm tra. +Cách dùng: đánh dấu `[x]` = giải thích được trade-off cho người khác, không phải "đã từng dùng". +Thứ tự học: từ dưới lên. Tầng dưới hổng thì tầng trên học vẹt. + +--- + +## File chi tiết (ví dụ minh hoạ cho từng mục nhỏ) + +| Tầng | File | +|---|---| +| L0 | 00-nang-luc-senior.md | +| L1 | 01-javascript-runtime.md | +| L2 | 02-typescript.md | +| L3 | 03-http-web-platform.md | +| L4 | 04-browser-frontend.md | +| L5 | 05-react.md | +| L6 | 06-nextjs.md | +| L7 | 07-nodejs-runtime.md | +| L8 | 08-nestjs.md | +| L9 | 09-database.md | +| L10 | 10-api-design.md | +| L11 | 11-auth-security.md | +| L12 | 12-performance.md | +| L13 | 13-testing.md | +| L14 | 14-devops-infra.md | +| L15 | 15-system-design.md | +| L16 | 16-architecture-craft.md | +| L17 | 17-leadership-behavioral.md | +| L18 | 18-phong-van.md | + +--- + +## L0. Mô hình năng lực senior (thang đo) + +- 0.1 Kỹ thuật: chiều sâu 1 stack + rộng đủ để hội thoại với mọi tầng +- 0.2 Phán đoán trade-off: chọn phương án và nói được cái gì đánh đổi +- 0.3 Hệ quả dài hạn: quyết định hôm nay ảnh hưởng 2 năm sau +- 0.4 Communicate risk: estimate, nói "không biết", escalate đúng lúc +- 0.5 Nâng người khác: review, mentor, viết doc người khác dùng được +- 0.6 Ownership: từ yêu cầu mơ hồ → hệ thống chạy được + vận hành được + +--- + +## L1. JavaScript runtime (nền móng, hay bị hỏi ngược) + +- 1.1 Ngôn ngữ: primitives vs object, tham chiếu vs giá trị, `==` vs `===`, coercion +- 1.2 Function & this: call sites, arrow vs method, `bind/call/apply`, closure +- 1.3 Prototype chain: `class` chỉ là syntactic sugar, `Object.create`, inheritance thật +- 1.4 Scope & hoisting: TDZ, IIFE hết thời, module scope +- 1.5 Event loop: call stack → microtask queue (Promise, queueMicrotask) → macrotask (setTimeout, I/O). thứ tự in của `setTimeout vs Promise.resolve vs console.log` +- 1.6 Blocking vs non-blocking: vì sao 1 thread vẫn chịu tải lớn; `process.nextTick` vs microtask +- 1.7 Libuv: thread pool (4 by default) cho fs/DNS/crypto, io_uring/epoll cho network +- 1.8 Async: callback → promise → async/await; error propagation; `Promise.all/allSettled/race/any`; unhandled rejection +- 1.9 Garbage collection: young/old generation, memory leak kinh điển (closure giữ reference, listener quên remove, global cache không eviction) +- 1.10 Structured data: JSON giới hạn (không Date, Map, cycle), structuredClone, bigint +- 1.11 Errors: Error subclass, stack trace, error vs exception flow, `try/finally` semantics + +**Check**: giải thích tại sao `await` trong loop for là tuần tự, và khi nào điều đó đúng ý. + +--- + +## L2. TypeScript (senior TS ≠ biết annotation) + +- 2.1 Type system: structural vs nominal, assignability, `unknown` vs `any` vs `never` +- 2.2 Narrowing: discriminated union, type predicate (`x is T`), `asserts`, control-flow analysis +- 2.3 Generics: constraints, `extends infer`, conditional types, distributive conditional, `satisfies` +- 2.4 Utility types: `Partial/Pick/Omit/Record/Awaited/ReturnType`; tự viết lại được +- 2.5 Mapped & template literal types: `keyof`, indexed access, `[K in keyof T]` +- 2.6 `strict` đầy đủ: `strictNullChecks`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes` — hiểu vì sao từng flag tồn tại +- 2.7 Declaration: interface vs type (khi nào bắt buộc interface: declaration merging, class implements) +- 2.8 Module system: ESM vs CJS, `type: module`, `verbatimModuleSyntax`, dual package hazard, `tsconfig moduleResolution: bundler/node16` +- 2.9 tsconfig layers: `target/lib/module/moduleResolution` tách biệt nhau; project references; incremental build +- 2.10 Type-erasure runtime: enum trap, `typeof`, runtime validation vẫn cần (zod/class-validator) +- 2.11 Declaration files: `.d.ts`, `declare module`, augmentation của lib khác +- 2.12工具的: tsc vs transpile-only (SWC/esbuild) — vì sao type-check không发生在 runtime +- 2.13 Pattern thực chiến: branded types cho DomainId, discriminated union cho state, Result type vs throw + +**Check**: viết `DeepPartial<T>`, `Paths<T>` từ đầu không tra. + +--- + +## L3. HTTP & web platform (nơi mọi stack gặp nhau) + +- 3.1 HTTP/1.1: method semantics (safe/idempotent), status classes, header, keep-alive, head-of-line blocking +- 3.2 HTTP/2: multiplexing, stream, server push (chết), header compression; HTTP/3 QUIC vì sao +- 3.3 TLS: handshake 1.3 (1-RTT), certificate chain, SNI; vì sao private key không rời server +- 3.4 Cache: `Cache-Control` (max-age, s-maxage, stale-while-revalidate), ETag vs Last-Modified, `Vary`, CDN tier vs browser tier +- 3.5 Content negotiation: `Accept/Content-Type`, multipart/form-data (mode upload của org) +- 3.6 Cookie: SameSite (Lax/Strict/None), Secure, HttpOnly, Domain/Path; vì sao SameSite phá CORS-with-credentials +- 3.7 CORS: preflight khi nào nổ, `Access-Control-Allow-*`, credentials mode; sửa ở edge hay ở app +- 3.8 URL & encoding: `encodeURIComponent` phạm vi, `URL`/`URLSearchParams` API +- 3.9 Streaming: chunked transfer, SSE vs WebSocket vs long-poll — khi nào dùng gì +- 3.10 Range, compression (`Content-Encoding`), ETag yếu vs mạnh + +**Check**: giải thích một request ảnh qua CDN thất bại vì `Vary: Cookie` — vì sao cache miss. + +--- + +## L4. Browser & frontend foundations + +- 4.1 Critical rendering path: HTML parse → CSSOM → render tree → layout → paint → composite +- 4.2 Reflow vs repaint vs composite; `transform/opacity` tại sao rẻ; `will-change` lạm dụng hại gì +- 4.3 Scripts: `defer` vs `async` vs inline; blocking render; module scripts là defer mặc định +- 4.4 Core Web Vitals: LCP (nguồn gốc: server TTFB, resource load, lazy quá tay), INP (task dài, hydration cost), CLS (thiếu size ảnh, font swap → `font-display`) +- 4.5 Resource loading: `preload/prefetch/preconnect/fetchpriority`, srcset/sizes, `loading=lazy` sai chỗ hại LCP +- 4.6 Storage: localStorage vs IndexedDB vs cookie vs Cache API; giới hạn size; private mode +- 4.7 Accessibility: semantic HTML trước ARIA, focus management, keyboard path, `prefers-reduced-motion` +- 4.8 Security browser: CSP (nonce vs hash vs unsafe-inline), XSS vector, clickjacking (`frame-ancestors`), `target=_blank` noopener +- 4.9 Navigation: history API (React Router dựa vào đây), PRG pattern +- 4.10 Modern APIs đáng biết: View Transitions, container queries, `:has()`, popover, `<dialog>` native, Intl (i18n) + +--- + +## L5. React (trước khi đụng Next.js) + +- 5.1 Render model: UI = f(state); reconciliation; key và vị trí; vì sao index-as-key hỏng list có reorder +- 5.2 Fiber: render phase vs commit phase, interruptible rendering, priority +- 5.3 State: `useState` batching, updater function, state không phải biến — là input của render kế tiếp +- 5.4 Effect: cleanup là phần của effect, không phải "componentWillUnmount"; dependency array là contract; Effect dùng cho đồng bộ với hệ thống ngoài, không dùng để "chạy sau render" +- 5.5 Ref: thoát khỏi render model có kiểm soát; `forwardRef` vs ref prop (React 19) +- 5.6 Memoization: `useMemo/useCallback/react.memo` — chỉ thắng khi prop ổn định + render thật sự đắt; đo trước +- 5.7 Context: re-render cả subtree; tách context theo tần suất thay đổi; selector pattern (zustand/jotai thay vì context cho state nhanh) +- 5.8 Suspense & concurrent: lazy, `useTransition`, `useDeferredValue`, `use()` (React 19), activity +- 5.9 Forms: controlled vs uncontrolled, Action pattern (React 19 `useActionState`, `useOptimistic`) +- 5.10 Composition vs props-drilling: children-as-slot, component API design (senior hay bị hỏi ở đây) +- 5.11 Patterns lỗi: derived state trong effect (nên tính lúc render), `useEffect` để fetch dữ liệu server, state duplication +- 5.12 Server components (đọc ở L6): React không còn chỉ là client library + +--- + +## L6. Next.js (App Router làm trung tâm) + +- 6.1 Rendering spectrum: SSG / ISR / SSR / CSR / PPR — với mỗi loại: dữ liệu nằm ở đâu, revalidate khi nào, khi nào chọn +- 6.2 App Router: layout tree, nested routing, `loading.tsx`, parallel & intercepting routes, route groups +- 6.3 Server vs Client components: boundary rules, serialization, vì sao Server Component mặc định là mặc định đúng +- 6.4 Server Actions: formAction, `revalidatePath/revalidateTag`, progressive enhancement, security (chúng là public endpoint — validate input như API) +- 6.5 Caching layers: Data Cache, Full Route Cache, Router Cache, `unstable_cache` — và Next 15+ đổi default sang `no-store` semantics. Đây là chỗ 90% dev hiểu sai +- 6.6 Data fetching: fetch vs server component vs route handler; dedupe bằng cache; streaming & Suspense boundary cho TTFB +- 6.7 Middleware: edge runtime, chạy mỗi request, đừng nặng; redirect/rewrite/i18n/locale +- 6.8 `next/image`: resize on-demand vs build, `priority`, loader custom; `next/font`: zero layout shift +- 6.9 Streaming SSR + selective hydration: `next/dynamic` đúng/sai, client boundary và bundle size +- 6.10 Rendering errors: error boundary (global-error, error.tsx), not-found +- 6.11 i18n: `next-intl` pattern, locale routing, message catalog +- 6.12 Deploy: standalone output, edge vs node runtime, ISR trên serverless (cache per-instance → vấn đề gì) +- 6.13 Migration: Pages Router → App Router; `getServerSideProps` tương đương cái gì; vì sao `useSearchParams` cần Suspense + +**Check**: vẽ lại đường đi dữ liệu của 1 trang: request → middleware → RSC payload → cache layers → browser. + +--- + +## L7. Node.js runtime (backend) + +- 7.1 Process model: event loop 6 phase, thread pool, `worker_threads` (CPU-bound task duy trì 1 loop không được), cluster (process-per-core, IPC qua IPC channel) +- 7.2 Streams: backpressure là lý do duy nhất tồn tại; `pipe` tự xử lý; `pipeline` + error; objectMode; Readable/Web ReadableStream hai hệ +- 7.3 Buffer & typed arrays: encoding, `Buffer.alloc` vs `Buffer.allocUnsafe` (security) +- 7.4 fs: sync vs async vs `fs.promises`; `graceful-fs` hết thời; file watching +- 7.5 Child process: spawn vs exec vs fork, khi nào thoát Node là trả lời đúng +- 7.6 Memory: heap snapshot, `--max-old-space-size`, vì sao OOM trong container khi memory limit của Node > limit cgroup +- 7.7 Worker offload pattern: offload crypto/parse lớn sang worker thread hoặc separate service +- 7.8 Observability runtime: `process.env` anti-pattern đọc mỗi request, `perf_hooks`, async_hooks (basis của Nest request context) +- 7.9 Security runtime: prototype pollution, `Object.freeze`, `process.binding`, dependency exec + +--- + +## L8. NestJS (DI + kiến trúc ứng dụng) + +- 8.1 DI: provider lifetime (singleton/request/transient), `forwardRef` là code smell, circular DI thật = module design sai, `@Inject` token, custom provider factory, scope explosion (request-scoped chain toàn bộ upstream thành request-scoped) +- 8.2 Module design: feature vs domain vs infrastructure module; module boundary = public API của 1 bounded context; `exports` là interface; barrel file leak +- 8.3 Layering: controller (transport) / service (use case) / repository (data access) — tách để test, không phải vì book nói vậy; controller không import repository +- 8.4 Request lifecycle: middleware → guards → interceptors → pipes → controller → interceptors(tail) → exception filters — thứ tự đúng và mỗi tầng nên/cấm làm gì +- 8.5 Pipes & validation: class-validator (decorator = runtime metadata) vs zod (schema = source of truth, validate trước DTO); org dùng interceptor upload (mediaUpload inject URL vào body trước validation — nắm để không phá flow) +- 8.6 Guards: auth guard vs permission guard tách nhau, `canActivate` chạy mỗi request (đắt = cache) +- 8.7 Interceptors: response shaping, timeout, logging, cache interceptor; RxJS ở đây có đáng giữ không (Next 15 ecosystem nghiêng về promise) +- 8.8 Exception filter: HttpException vs domain error (Result pattern); error mapping nhất quán, không leak stack +- 8.9 Config: `@nestjs/config` + zod/env validation at boot — fail fast ở startup chứ không ở request đầu tiên +- 8.10 Background work: `@nestjs/schedule`, BullMQ cho job queue, event emitter vs message queue (in-process vs durable) +- 8.11 Database integration: Prisma 7 (org rule), transaction scope, repository vs raw Prisma lộ ra service — trade-off +- 8.12 Auth module: Passport strategy, JWT access+refresh rotation, session vs JWT vs org đã chọn gì +- 8.13 Testing Nest: unit test với mocked provider, e2e với supertest + Test.createTestingModule override provider — boundary giả lập đúng chỗ nào +- 8.14 Performance: keep-alive agent khi gọi service khác, connection pool (DB/redis) vs connection per request, `fastify` adapter trade-off (schema validation, logger) +- 8.15 Monorepo & library boundaries: `packages/` là deep module — export tối thiểu, import qua entrypoint (org rule, hỏi đến là phải trả lời được) + +**Check**: thiết kế module "Order" cho 1 team khác dùng lại; giải thích interface của nó nằm ở file nào. + +--- + +## L9. Database (Prisma-first nhưng không chỉ Prisma) + +- 9.1 Relational model: 1:N vs M:N (join table), cascade, ON DELETE vs app-level delete; soft delete phá index/unique constraint thế nào +- 9.2 Index: B-tree hoạt động sao, composite index thứ tự cột quan trọng hơn có index, covering index, index write cost, `EXPLAIN` đọc được: Seq Scan vs Index Scan vs Bitmap +- 9.3 Transactions & isolation: 4 level, read committed mặc định, lost update & race condition thường gặp (check-then-insert → unique constraint là fix đúng, không phải app lock) +- 9.4 N+1: phát hiện qua log, fix bằng join/`include`/batch loader; `select` để giảm payload +- 9.5 Connection pool: pool size bao nhiêu là đúng (không phải mỗi request 1 conn), PgBouncer khi serverless +- 9.6 Prisma-specific: PrismaClient singleton (cấm new trong lambda), `transaction` interactive vs atomic, migration workflow, raw query escape hatch +- 9.7 Modeling senior: enum trong DB vs app, money = integer minor unit, decimal vs float, timezone = store UTC + `timestamptz` +- 9.8 Cache-DB consistency: cache invalidation strategy, write-through vs cache-aside, stale accepted khi nào +- 9.9 Search: LIKE không scale → trigram index / tsvector / Meilisearch khi nào +- 9.10 Partitioning & scale-out: khi nào thật sự cần (volume threshold nào), read replica + replication lag +- 9.11 Migrations production: expand-contract pattern (zero-downtime), không bao giờ drop cột trong migration đầu tiên + +--- + +## L10. API design + +- 10.1 REST maturity: resource naming, status code semantics thật (201 + Location, 409 conflict, 422 vs 400), pagination (offset vs cursor — vì sao cursor thắng khi có insert), filtering, sparse fieldset +- 10.2 Idempotency: Idempotency-Key header cho payment, retry-safe design +- 10.3 Versioning: URL vs header vs none (additive-first: thêm field không phá client cũ) +- 10.4 Error contract: RFC 7807 (problem+json), error code taxonomy ổn định cho client switch +- 10.5 OpenAPI/type-first: tRPC hoặc `@nestjs/swagger` từ DTO; contract là nguồn sinh docs + client type +- 10.6 GraphQL: khi nào đáng (N client, N frontend team), khi nào không (auth phức tạp, cache, N+1 tự gây); persisted query, DataLoader +- 10.7 File upload: inline multipart qua endpoint business + interceptor (org standard) — vì sao presign-then-PUT là anti-pattern ở đây +- 10.8 Rate limiting: token bucket vs sliding window, theo user vs IP, 429 + `Retry-After` +- 10.9 Webhook (backend gọi lại): HMAC sign, retry + backoff, idempotency phía receiver +- 10.10 Realtime: SSE (one-way) vs WebSocket (bidirectional) — chọn theo chiều dữ liệu, không theo thói quen; Redis pub/sub khi multi-instance +- 10.11 Public API thinking: deprecation policy, changelog, backward compat 2 version + +--- + +## L11. Auth & security + +- 11.1 Session vs JWT: vì sao JWT cho web app thường sai (logout, rotation); JWT phù hợp service-to-service, refresh token +- 11.2 OAuth2/OIDC: authorization code + PKCE (public client không có secret), access token ngắn hạn, refresh rotation & reuse detection; scope vs permission +- 11.3 Password: bcrypt/scrypt/argon2 (hash ≠ encrypt), pepper, reset token one-time + expiry +- 11.4 Web top attacks: XSS → CSP, CSRF (SameSite + token), SQL injection (parameterized, ORM không tự miễn nếu raw string), SSRF (validate outbound URL), path traversal (upload filename), prototype pollution +- 11.5 Headers baseline: CSP, HSTS, X-Frame-Options/frame-ancestors, Referrer-Policy, COOP/COEP (chỉ khi cần) +- 11.6 Secrets: env at runtime vs build time (NEXT_PUBLIC_* = public, không phải secret), secret manager, không commit — org audit rule soi cái này +- 11.7 Authz mô hình: RBAC vs ABAC, policy ở guard vs ở service, "confused deputy" khi cho user truyền ID rồi query theo ID (phải filter theo owner) +- 11.8 Supply chain: lockfile, audit, pin version, postinstall script là attack vector + +--- + +## L12. Performance engineering (senior hỏi cách tìm, không phải cách sửa) + +- 12.1 Đo trước sửa: profiler, flame graph, A/B measure; không có số = không có claim +- 12.2 Backend: event loop lag metric, pool saturation, GC pause, CPU profile qua `--prof`/clinic +- 12.3 N+1 API-level: một trang gọi 40 endpoint tự gây — fix bằng composition endpoint, không phải client cache +- 12.4 Frontend bundle: code splitting tự nhiên của App Router, `next build --experimental-metrics` / bundle analyzer, tree-shaking fail vì side-effect +- 12.5 TTFB chain: DNS → TLS → TTFB server → streaming; p95 vs median — senior nói p95 +- 12.6 Image & media: format, dimension, CDN, lazy đúng chỗ +- 12.7 Caching pyramid: in-memory (per-instance, mất khi scale) → Redis → CDN → browser; mỗi tầng invalidate khác nhau +- 12.8 Queue offload: request phải trả lời trong bao lâu, việc gì cho queue (email, resize, report) +- 12.9 DB: long query log, lock contention, covering index fix được gì và không fix được gì + +--- + +## L13. Testing (kiểm chứng, không phải diễn kịch) + +- 13.1 Test pyramid thực dụng: unit nhiều (logic thuần), integration qua boundary thật (DB testcontainers), e2e ít mà sắc (Playwright) +- 13.2 Testable design: dependency injection để thay boundary; code khó test = thiết kế sai, không phải "viết test khó" +- 13.3 Mock đúng chỗ: mock hệ thống ngoài (payment, email), không mock DB (org e2e rule: chạy với API thật); mock-echo test = slop +- 13.4 TDD: dùng cho business rule, state machine, parser — không dùng cho CRUD cơ học +- 13.5 Fix flaky: await condition không await time, test isolation (shared state là nguồn số 1) +- 13.6 Coverage: đo để tìm vùng tối, không phải chạy theo %; value-free test (assert tautology) là vi phạm audit rule +- 13.7 E2E tổ chức: fixtures, auth reuse, chạy song song, screenshot/video khi fail trên CI + +--- + +## L14. DevOps / infra (senior vận hành thứ mình viết) + +- 14.1 Container: Dockerfile multi-stage, layer order cho cache, non-root user, image size vì sao quan trọng, cgroup limit → Node memory +- 14.2 CI/CD: pipeline cache deps, build once promote many, preview env per PR (Next Vercel pattern) +- 14.3 Environment: config qua env (12-factor), secret per-env, feature flag tách deploy khỏi release +- 14.4 Observability 3 trụ: log (structured JSON + correlation/request id), metric (RED: rate/errors/duration, USE resource), trace (OpenTelemetry, context propagation qua queue) +- 14.5 Alerting: alert trên symptom (user impact) không phải cause; SLO/error budget +- 14.6 Incident: severity, mitigation trước root cause (rollback là fix), blameless postmortem — interview hỏi bằng behavioral câu hỏi +- 14.7 Deploy safety: zero-downtime (readiness probe, drain), migration + deploy order, canary/blue-green, rollback plan bắt buộc +- 14.8 IaC: Terraform/module/state, K8s khái niệm tối thiểu (deployment, service, ingress, HPA) đủ để tranh luận với DevOps +- 14.9 Cost: khi nào serverless đắt hơn server, DB storage growth, egress +- 14.10 GitHub Actions: reusable workflow, changesets release automation (org standard) + +--- + +## L15. System design (vòng quyết định senior) + +- 15.1 Framework: requirement (functional + non-functional: QPS, p99, data size, team size) → ước lượng con số → API → data model → high-level → deep dive theo yêu cầu → bottleneck → trade-off +- 15.2 Estimation: 1 req/s vs 1k vs 100k; storage/ngày; bandwith; 2N+1 replicas +- 15.3 Load balancing: L4 vs L7, algorithm, session affinity vs stateless +- 15.4 Horizontal scaling: stateless app (auth ở token/redis), sticky problem, cache invalidation đa instance +- 15.5 Caching chiến lược: cache-aside, write-through, write-behind, invalidation by tag; thundering herd (single-flight) +- 15.6 Database scale: read replica + lag, sharding (key range/hash/lookup), CQRS tách write model, vì sao 95% bài toán dừng ở "1 Postgres to" là đúng +- 15.7 Async: queue (BullMQ/Kafka/SQS), at-least-once vs exactly-once (exactly-once = idempotency ở consumer), DLQ, outbox pattern (DB write + event atomic) +- 15.8 Rate limit, circuit breaker, bulkhead, backpressure, timeout ở mọi tầng +- 15.9 Real-time hệ: fan-out on write vs on read, presence, pub/sub scale +- 15.10 Bài kinh điển phải làm được: URL shortener, news feed, chat, notification system, design YouTube-lite (upload + transcode pipeline), rate limiter, web crawler, distributed lock (và vì sao lock phân tán nguy hiểm) +- 15.11 Trade-off vocabulary: consistency vs availability, latency vs throughput, build vs buy, monolith vs microservice (và recovery path khi microservice sai) + +--- + +## L16. Architecture & craft (code quality) + +- 16.1 Deep module (org rule — packages/): interface nhỏ, implementation ẩn sau entrypoint; depth = value/complexity +- 16.2 Dependency direction: domain không import framework, ports & adapters / hexagonal — khi nào đáng, khi nào over-engineering +- 16.3 DDD chiến thuật: entity vs value object, aggregate boundary, invariant nằm ở aggregate, domain event, anti-corruption layer khi integration legacy +- 16.4 Context map: bounded context cho NestJS module / Next.js feature / package; CONTEXT.md glossary (org đang làm cái này — dùng nó) +- 16.5 Consistency model: transactional (1 DB), saga (multi-service: orchestration vs choreography + compensation), eventual consistency chấp nhận ở đâu +- 16.6 Code smell senior: type-bypass cast (`as`), switch phát tán khắp nơi, shared mutable module state, config cho value không đổi, abstraction 1 implementation (org audit rule = soi đúng cái này) +- 16.7 Refactor chiến thuật: seam để test trước khi refactor, strangler fig khi thay hệ thống +- 16.8 Error design: Result/Either vs exception — boundary: domain return Result, transport throw; không swallow + +--- + +## L17. Leadership & behavioral (vòng senior hay trượt ở đây) + +- 17.1 Kể chuyện tech: STAR nhưng có quyết định + số đo ("chọn X thay vì Y vì Z, kết quả giảm 40% p99") +- 17.2 Xung đột kỹ thuật: disagree & commit, cách bạn defend design bằng số +- 17.3 Mentoring: code review mẫu (comment dạy, không chỉ sửa), 1:1 với junior +- 17.4 Ownership sự cố: kể 1 incident bạn cause, bạn phát hiện, bạn fix quy trình +- 17.5 Ước lượng & say no: scope negotiation với PM, nói "không biết" + cách tìm ra +- 17.6 Tech debt: trả khi nào, negotiate với feature, đo bằng interest rate (velocity giảm) + +--- + +## L18. Phỏng vấn cụ thể (skill phỏng vấn, khác năng lực) + +- 18.1 Live coding: đọc hết ví dụ → hỏi input edge case → nói hướng trước khi code → chạy test nhỏ → refactor sau khi pass +- 18.2 Hỏi ngược: hỏi traffic, team size, constraint — senior hỏi nhiều hơn trả lời ở giai đoạn đầu +- 18.3 System design whiteboard: quản thời gian 45' (5 req, 5 estimation, 10 API+DB, 15 high-level, 10 deep dive, 5 trade-off) +- 18.4 "Tại sao X": mỗi tool dùng trong dự án phải trả lời được thay thế bằng gì và mất gì +- 18.5 Take-home: đọc test spec trước, commit nhỏ có message, README trade-off, không over-engineer +- 18.6 Behavioural prep: 6 câu chuyện STAR đã có số, mỗi câu cover 1 năng lực L0 + +--- + +## Thứ tự học đề xuất (đường chính, đã tối ưu cho gap mid→senior) + +1. **L15 System design** — đòn bẩy cao nhất, senior fail ở đây nhiều nhất, và nó ép bạn đi ngược lên mọi tầng khác để giải thích +2. **L8 NestJS depth + L6 Next.js caching/rendering** — đúng stack phỏng vấn, hỏi sâu là lộ ngay học vẹt +3. **L9 Database depth** — index/transaction/race condition là nơi "đã từng dùng" và "hiểu" tách nhau +4. **L15 bài kinh điển ×6** — mỗi bài 1 lần viết ra giấy, tự thuyết trình 30' +5. **L17 behavioral + L18** — 2 buổi tối, không cần học dài +6. Các tầng còn lại: học bị động theo chiều ngược — khi system design/L8/L9 chạm tới unit nào, về unit đó đọc sâu (bottom-up reinforcement) + +Học song song: 1 unit "build" (code thật có trade-off) + 1 unit "explain" (viết blog/nói cho ai đó). Đây là câu trả lời Q7/Q8/Q10 của bạn nếu bạn build theo cách đó.