Vòng phỏng vấn Backend hiếm khi dừng ở định nghĩa. Người phỏng vấn thường hỏi một câu ngắn rồi đào sâu bằng "nếu dữ liệu lớn gấp 100 lần thì sao", "nếu hai request đến cùng lúc thì sao". Bài này gom 30 câu hỏi hay gặp theo 6 nhóm: database, đồng thời và giao dịch, mở rộng hệ thống, thiết kế hệ thống, xác thực và API, xử lý sự cố. Mỗi câu có gợi ý trả lời ngắn để bạn biết cần nói những ý nào, sau đó tự luyện bằng ví dụ từ dự án của mình.
Một nguyên tắc chung cho mọi câu: nêu bối cảnh, đưa lựa chọn, nói rõ đánh đổi (trade-off) và cách bạn kiểm chứng bằng số đo. Câu trả lời "tùy trường hợp" chỉ được điểm khi bạn nói được tùy vào điều gì.
Nhóm 1: Database và truy vấn
1. Có những loại index nào và tối ưu query chậm thế nào?
Nêu các loại phổ biến: B-tree (mặc định, hợp với so sánh bằng, khoảng, sắp xếp), hash (chỉ so sánh bằng), index nhiều cột (composite), index bao phủ (covering index chứa đủ cột cần đọc), index toàn văn. Quy trình tối ưu: bật log query chậm, chạy EXPLAIN hoặc EXPLAIN ANALYZE để xem kế hoạch thực thi, tìm chỗ quét toàn bảng, thêm index đúng cột lọc và sắp xếp, đo lại thời gian. Nhấn mạnh index làm chậm thao tác ghi và tốn dung lượng, nên không thêm tràn lan.
2. Vì sao đã có index mà query vẫn chậm?
Các nguyên nhân hay gặp: dùng hàm trên cột (WHERE LOWER(email) = ...) làm index không dùng được; thứ tự cột trong composite index không khớp điều kiện lọc; cột có độ chọn lọc thấp (ví dụ cột trạng thái chỉ vài giá trị); kiểu dữ liệu so sánh bị ép kiểu ngầm; thống kê của database đã cũ; query trả về quá nhiều dòng nên database chọn quét bảng. Cách debug là đọc kế hoạch thực thi chứ không đoán.
3. N+1 query là gì và xử lý ra sao?
Là khi lấy danh sách N bản ghi bằng 1 query, rồi với mỗi bản ghi lại chạy thêm 1 query để lấy dữ liệu liên quan, tổng cộng N+1 lần gọi database. Lỗi này hay xuất hiện khi dùng ORM. Cách xử lý: JOIN hoặc eager loading, gom thành một query WHERE id IN (...), hoặc dùng cơ chế gom lô như DataLoader trong GraphQL. Nên nói thêm cách phát hiện: đếm số query trên mỗi request trong log hoặc công cụ APM.
4. Phân trang OFFSET và cursor khác nhau thế nào?
OFFSET dễ làm, cho phép nhảy tới trang bất kỳ, nhưng càng về trang sau database càng phải bỏ qua nhiều dòng nên chậm dần, và dữ liệu thêm mới giữa chừng làm trang bị lệch. Cursor (keyset) dùng giá trị của bản ghi cuối trang trước (WHERE id < :last_id ORDER BY id DESC LIMIT 20), nhanh ổn định và không lệch, nhưng không nhảy trang tùy ý. Feed, lịch sử giao dịch, API cuộn vô hạn hợp với cursor; trang quản trị cần số trang hợp với OFFSET.
5. Khi nào chọn SQL, khi nào chọn NoSQL?
SQL hợp khi dữ liệu có quan hệ rõ, cần giao dịch ACID, cần truy vấn linh hoạt và báo cáo. NoSQL (document, key-value, wide-column) hợp khi cấu trúc thay đổi thường xuyên, truy cập theo khóa là chính, cần mở rộng ngang dễ. Câu trả lời tốt nên nói rằng nhiều hệ thống dùng cả hai: SQL cho đơn hàng và thanh toán, key-value cho cache và phiên đăng nhập, công cụ tìm kiếm riêng cho full-text.
6. Chuẩn hóa và phi chuẩn hóa đánh đổi điều gì?
Chuẩn hóa tránh trùng lặp, cập nhật một chỗ là đúng mọi nơi, nhưng đọc cần nhiều JOIN. Phi chuẩn hóa sao chép dữ liệu để đọc nhanh, đổi lại phải lo đồng bộ khi dữ liệu gốc thay đổi. Thực tế thường chuẩn hóa ở bảng nghiệp vụ và phi chuẩn hóa có chủ đích ở bảng đọc nhiều (bảng tổng hợp, view vật lý, bản sao cho báo cáo).
Nhóm 2: Đồng thời, khóa và giao dịch
7. Nhiều request cùng cập nhật một tài nguyên (chỉ còn 1 sản phẩm) thì xử lý thế nào?
Đây là bài toán race condition. Các cách: cập nhật có điều kiện trong một câu lệnh (UPDATE stock SET qty = qty - 1 WHERE id = ? AND qty > 0) rồi kiểm tra số dòng bị ảnh hưởng; khóa dòng bằng SELECT ... FOR UPDATE trong giao dịch; optimistic locking bằng cột version; hoặc đưa yêu cầu vào hàng đợi xử lý tuần tự cho các mặt hàng nóng. Nên nêu cách kiểm thử: bắn đồng thời nhiều request và kiểm tra tồn kho không bị âm.
8. Optimistic và pessimistic locking chọn thế nào?
Pessimistic khóa trước khi sửa, an toàn khi xung đột thường xuyên nhưng giảm thông lượng và dễ gây chờ khóa. Optimistic không khóa, khi ghi thì kiểm tra version có đổi không; nếu đổi thì báo xung đột và thử lại. Optimistic hợp khi xung đột hiếm (sửa hồ sơ, sửa bài viết); pessimistic hợp khi tranh chấp cao và sai một lần là đắt (trừ tiền, giữ chỗ).
9. Mức cô lập giao dịch ảnh hưởng gì đến tính nhất quán?
Nêu bốn mức chuẩn: Read Uncommitted, Read Committed, Repeatable Read, Serializable, và ba hiện tượng: đọc dữ liệu chưa commit (dirty read), đọc lại thấy khác (non-repeatable read), xuất hiện dòng ma (phantom). Mức càng cao càng ít hiện tượng nhưng càng nhiều chờ khóa hoặc lỗi phải thử lại. Nên biết mặc định của database bạn dùng, ví dụ PostgreSQL mặc định Read Committed, MySQL InnoDB mặc định Repeatable Read.
10. Deadlock là gì và phòng tránh ra sao?
Deadlock xảy ra khi giao dịch A giữ khóa X chờ khóa Y, trong khi giao dịch B giữ Y chờ X. Database sẽ phát hiện và hủy một bên. Cách giảm: luôn khóa tài nguyên theo cùng một thứ tự, giữ giao dịch ngắn, không gọi API bên ngoài khi đang giữ khóa, có index để khóa đúng phạm vi hẹp, và viết code thử lại khi gặp lỗi deadlock.
11. Idempotency là gì, làm sao chặn trừ tiền hai lần hoặc tạo đơn trùng?
Một thao tác idempotent gọi nhiều lần vẫn cho cùng kết quả. Với thanh toán và tạo đơn, client gửi kèm một khóa idempotency duy nhất; server lưu khóa cùng kết quả xử lý, lần gọi lặp lại với cùng khóa chỉ trả lại kết quả cũ. Kết hợp ràng buộc unique trong database để chặn ở tầng cuối. Webhook cũng cần xử lý idempotent vì bên gửi thường gửi lại khi không nhận được phản hồi.
Nhóm 3: Mở rộng và hiệu năng
12. Mở rộng hệ thống từ nghìn lên triệu người dùng thế nào?
Trả lời theo từng bước có số đo, không nhảy thẳng vào microservices: tách database khỏi máy chủ ứng dụng; cho ứng dụng chạy nhiều bản sau load balancer (ứng dụng phải stateless, phiên lưu ở Redis hoặc token); thêm cache và CDN cho nội dung tĩnh; thêm bản sao đọc (read replica); đưa việc nặng vào hàng đợi chạy nền; cuối cùng mới tính đến chia nhỏ dữ liệu (sharding) hoặc tách dịch vụ theo miền nghiệp vụ. Mỗi bước nên gắn với điểm nghẽn đo được.
13. Làm sao xác định điểm nghẽn của hệ thống backend?
Đo trước khi sửa: theo dõi độ trễ theo phân vị (p50, p95, p99), thông lượng, tỷ lệ lỗi, mức dùng CPU, bộ nhớ, kết nối database. Dùng tracing để xem một request tốn thời gian ở bước nào (query, gọi dịch vụ ngoài, xử lý trong code). Chạy kiểm thử tải để tái hiện. Điểm nghẽn hay gặp nhất là database và các lời gọi mạng tuần tự.
14. Mở rộng dọc và mở rộng ngang khi nào dùng?
Mở rộng dọc (tăng CPU, RAM cho một máy) đơn giản, không phải sửa code, nhưng có giới hạn phần cứng và vẫn là một điểm lỗi. Mở rộng ngang (thêm máy) gần như không giới hạn và chịu lỗi tốt hơn, nhưng đòi hỏi ứng dụng stateless và xử lý dữ liệu phân tán. Database thường mở rộng dọc trước rồi mới tính chuyện ngang; tầng ứng dụng thường mở rộng ngang ngay từ đầu.
15. Chiến lược cache gồm những gì, đặt cache ở đâu?
Cache có thể đặt ở nhiều tầng: trình duyệt (HTTP cache), CDN, reverse proxy, bộ nhớ trong ứng dụng, cache phân tán như Redis, và bộ đệm của chính database. Các mẫu phổ biến: cache-aside (đọc cache, trượt thì đọc database rồi ghi vào cache), write-through (ghi đồng thời cache và database), write-back (ghi cache trước, đẩy xuống database sau). Cần chọn TTL theo mức chấp nhận dữ liệu cũ của từng loại dữ liệu.
16. Cache invalidation là gì và vì sao khó?
Là việc làm mất hiệu lực dữ liệu trong cache khi dữ liệu gốc thay đổi. Khó vì cùng một dữ liệu có thể nằm trong nhiều khóa cache, nhiều máy chủ, và luôn có khoảng thời gian giữa lúc ghi database và lúc xóa cache để request khác đọc dữ liệu cũ rồi ghi ngược vào cache. Các cách xử lý: TTL ngắn, xóa cache theo sự kiện thay đổi, đánh version cho khóa cache, chấp nhận nhất quán cuối cùng ở nơi cho phép.
17. Khi nào cache gây hại?
Khi dữ liệu thay đổi liên tục nên tỷ lệ trúng cache thấp; khi cache trả dữ liệu sai cho người dùng khác do khóa cache thiếu thông tin người dùng; khi nhiều request cùng trượt cache một lúc và dồn vào database (cache stampede); khi cache sập kéo database sập theo vì hệ thống đã quen với tải thấp. Các biện pháp: khóa khi nạp lại cache, thêm độ lệch ngẫu nhiên cho TTL, giới hạn tải vào database khi cache lỗi.
Nhóm 4: Thiết kế hệ thống
18. Thiết kế dịch vụ rút gọn URL chịu tải lớn
Làm rõ yêu cầu: số link tạo mỗi ngày, tỷ lệ đọc so với ghi (thường đọc nhiều hơn rất nhiều), có cần thống kê lượt nhấp không. Thiết kế: sinh mã ngắn bằng mã hóa base62 từ ID tăng dần hoặc sinh ngẫu nhiên kèm kiểm tra trùng; lưu bảng ánh xạ mã sang URL gốc; cache mạnh ở tầng đọc vì link ít thay đổi; trả về chuyển hướng 301 hoặc 302 (302 nếu cần đếm lượt nhấp chính xác); ghi lượt nhấp bất đồng bộ qua hàng đợi. Nói thêm về chống lạm dụng: kiểm tra URL độc hại, giới hạn tốc độ tạo link.
19. Thiết kế hệ thống gửi email hoặc thông báo hàng loạt
Tách phần tạo yêu cầu gửi khỏi phần gửi thật: API nhận yêu cầu, ghi vào hàng đợi, các worker lấy ra gửi qua nhà cung cấp email, SMS hoặc push. Cần có giới hạn tốc độ theo nhà cung cấp, thử lại có độ trễ tăng dần, hàng đợi lỗi (dead letter queue) cho thư gửi thất bại nhiều lần, chống gửi trùng bằng khóa idempotency, lưu trạng thái từng thư để tra cứu, và tôn trọng lựa chọn hủy đăng ký của người nhận.
20. Thiết kế rate limiter chống lạm dụng
Nêu các thuật toán: fixed window (đơn giản nhưng bị dồn tải ở ranh giới cửa sổ), sliding window, token bucket (cho phép dồn một lượng nhất định rồi giới hạn đều), leaky bucket. Trong hệ thống nhiều máy, bộ đếm phải nằm ở nơi dùng chung như Redis, thao tác tăng và kiểm tra phải nguyên tử. Giới hạn theo người dùng, theo khóa API hoặc theo IP, trả mã 429 kèm thời gian nên thử lại. Có thể thử tính ngưỡng với công cụ tính rate limit.
21. Làm sao đảm bảo tính sẵn sàng cao và chịu lỗi?
Loại bỏ điểm lỗi đơn: nhiều bản ứng dụng ở nhiều vùng sẵn sàng, database có bản sao và cơ chế chuyển đổi dự phòng, sao lưu định kỳ và đã thử khôi phục. Kiểm tra sức khỏe để load balancer bỏ máy hỏng ra. Thiết kế để hệ thống xuống cấp từ từ: tính năng phụ lỗi thì tắt tính năng phụ, không kéo sập cả trang. Nêu mục tiêu bằng con số (SLO) và thời gian khôi phục mong muốn.
22. Circuit breaker là gì, khi nào cần?
Là cơ chế ngắt tạm lời gọi tới một dịch vụ đang lỗi. Khi tỷ lệ lỗi hoặc timeout vượt ngưỡng, breaker chuyển sang trạng thái mở và trả lỗi ngay (hoặc dữ liệu dự phòng) thay vì tiếp tục chờ; sau một thời gian, nó cho vài request thử đi qua để kiểm tra dịch vụ đã hồi phục chưa. Cần khi gọi dịch vụ ngoài hoặc dịch vụ nội bộ có thể chậm, để tránh luồng xử lý bị kẹt hàng loạt và lỗi lan dây chuyền.
23. Xuất hoặc xử lý hàng triệu bản ghi mà không làm sập server thế nào?
Không nạp toàn bộ vào bộ nhớ. Đọc theo lô hoặc theo cursor của database, ghi kết quả theo luồng (stream) ra file hoặc ra phản hồi HTTP. Chạy việc nặng dưới dạng tác vụ nền, trả cho người dùng mã tác vụ để theo dõi tiến độ và link tải khi xong. Giới hạn số tác vụ chạy cùng lúc và đọc từ bản sao database để không ảnh hưởng luồng giao dịch chính.
24. Streaming và buffering khác nhau thế nào?
Buffering nạp đủ dữ liệu vào bộ nhớ rồi mới xử lý hoặc gửi đi, đơn giản nhưng tốn RAM và tăng thời gian chờ byte đầu tiên. Streaming xử lý từng phần ngay khi có, bộ nhớ ổn định với dữ liệu lớn, nhưng code phức tạp hơn và cần xử lý backpressure (bên nhận chậm hơn bên gửi). Dùng streaming cho file lớn, xuất báo cáo, video, phản hồi dài từ mô hình AI; dùng buffering cho dữ liệu nhỏ cần xử lý trọn vẹn.
25. Message queue giải quyết vấn đề gì, khi nào dùng kiến trúc hướng sự kiện?
Hàng đợi tách bên gửi khỏi bên xử lý: bên gửi không phải chờ, tải đột biến được hấp thụ, bên xử lý lỗi thì tin nhắn vẫn còn để xử lý lại. Kiến trúc hướng sự kiện hợp khi một sự việc (đơn hàng đã tạo) cần kích hoạt nhiều việc độc lập (gửi email, trừ kho, ghi điểm thưởng). Phải nói được cái giá: thứ tự tin nhắn, xử lý trùng (giao ít nhất một lần nên consumer cần idempotent), khó theo dõi luồng, và dữ liệu chỉ nhất quán sau một khoảng trễ.
Nhóm 5: Xác thực và thiết kế API
26. Thiết kế xác thực và phân quyền an toàn: JWT hoạt động thế nào, khi nào không nên dùng, mật khẩu băm ra sao?
Phân biệt xác thực (bạn là ai) và phân quyền (bạn được làm gì). JWT gồm header, payload và chữ ký; server kiểm tra chữ ký nên không cần tra database mỗi request. Nhược điểm là khó thu hồi trước hạn, nên dùng access token ngắn hạn kết hợp refresh token có thể thu hồi. Không nên dùng JWT làm phiên cho web truyền thống khi cần đăng xuất tức thì, hoặc khi nhét quá nhiều dữ liệu vào token. Mật khẩu phải băm bằng thuật toán chậm có salt như bcrypt, scrypt hoặc Argon2, không dùng MD5 hay SHA-256 trần. Có thể xem cấu trúc một token với công cụ giải mã JWT (không dán token thật của hệ thống production).
27. REST và GraphQL, ưu nhược điểm và khi nào chọn?
REST đơn giản, tận dụng tốt HTTP cache, mỗi tài nguyên một endpoint. GraphQL cho client tự chọn trường cần lấy, giảm việc gọi nhiều endpoint, hợp khi có nhiều loại client với nhu cầu dữ liệu khác nhau. Đổi lại GraphQL khó cache ở tầng HTTP, dễ bị query quá nặng nếu không giới hạn độ sâu và độ phức tạp, và dễ dính N+1 ở resolver. Không có lựa chọn đúng tuyệt đối, chọn theo client và đội ngũ.
28. Thiết kế versioning cho API sao cho không phá client cũ?
Các cách: version trong đường dẫn (/v1/), trong header hoặc trong tham số. Quan trọng hơn cách đặt tên là nguyên tắc: chỉ thêm, không bớt hay đổi nghĩa trường cũ trong cùng một version; thay đổi phá vỡ thì ra version mới; thông báo lịch ngừng hỗ trợ version cũ và theo dõi xem client nào còn gọi. Tài liệu API (như OpenAPI) phải đi cùng từng version.
29. PUT và PATCH khác nhau ra sao, retry và timeout nên thiết kế thế nào?
PUT thay toàn bộ tài nguyên và theo chuẩn là idempotent; PATCH sửa một phần, có thể idempotent hoặc không tùy cách định nghĩa. Với retry: chỉ tự động thử lại các thao tác idempotent hoặc có khóa idempotency; dùng độ trễ tăng dần kèm độ lệch ngẫu nhiên để không dồn tải; giới hạn số lần thử. Timeout phải đặt cho mọi lời gọi mạng, và timeout của tầng gọi ngoài nên lớn hơn tổng timeout của các tầng bên trong.
Nhóm 6: Xử lý sự cố
30. Một endpoint đột nhiên tăng độ trễ hoặc CPU lên 100%, bạn điều tra theo thứ tự nào?
Trình tự gợi ý: (1) xác định phạm vi, một endpoint hay toàn hệ thống, từ lúc nào; (2) kiểm tra thay đổi gần nhất: bản deploy mới, thay đổi cấu hình, lượng truy cập tăng bất thường; (3) xem dashboard: độ trễ database, số kết nối, tỷ lệ lỗi dịch vụ phụ thuộc; (4) xem trace của request chậm và log query chậm; (5) nếu CPU cao, lấy profile hoặc thread dump để biết đoạn code nào đang chạy; (6) giảm thiệt hại trước, như rollback, bật giới hạn tốc độ, tăng số bản chạy, rồi mới sửa gốc; (7) viết báo cáo sau sự cố. Người phỏng vấn đánh giá cao việc bạn ưu tiên khôi phục dịch vụ trước khi tìm nguyên nhân.
Chuẩn bị thế nào cho vòng phỏng vấn Backend
Chuẩn bị 2–3 câu chuyện thật từ dự án: một lần tối ưu query, một sự cố production, một quyết định thiết kế có đánh đổi. Câu chuyện thật thuyết phục hơn mọi định nghĩa.
Luyện vẽ sơ đồ cho câu thiết kế hệ thống: client, load balancer, ứng dụng, cache, database, hàng đợi. Luôn hỏi lại yêu cầu và ước lượng tải trước khi vẽ.
Ôn theo công nghệ của vị trí: bộ câu hỏi theo từng công nghệ như Node.js, SQL và System Design giúp bạn đi sâu hơn từng mảng.
Xem lộ trình tổng thể nếu bạn đang chuyển sang Backend: lộ trình Backend Developer.
Nếu vị trí bạn ứng tuyển là fullstack, hãy ôn thêm câu hỏi phỏng vấn JavaScript và TypeScript và câu hỏi phỏng vấn Next.js nâng cao. Vị trí có phần vận hành thì xem tiếp câu hỏi phỏng vấn DevOps; còn nếu muốn hiểu cách đội QA sẽ kiểm thử API bạn viết, đọc câu hỏi phỏng vấn Tester.

















