Câu hỏi phỏng vấn

Luyện trả lời theo đúng trình độ bạn đang ứng tuyển. Mỗi câu có đáp án ngắn gọn và điều người phỏng vấn thật sự muốn nghe.

  • Junior 0–2 năm: nền tảng, công cụ hằng ngày, làm việc an toàn trong team.
  • Middle 2–5 năm: tự chịu trách nhiệm một tính năng, hiểu đánh đổi.
  • Senior 5+ năm: thiết kế hệ thống, dẫn dắt kỹ thuật, quyết định dài hạn.
  1. JuniorAI AgentAI agent khác chatbot và workflow ở điểm nào? Khi nào không nên dùng agent?

    Câu trả lời tốt phân biệt ba thứ theo một tiêu chí: ai quyết định bước tiếp theo.

    • Chatbot theo luật: luật viết tay quyết định (ví dụ dò từ khóa). Rẻ, nhưng không hiểu câu; "đơn của em giao chưa" có thể bị hiểu thành hỏi phí ship vì có chữ "giao".
    • Workflow: lập trình viên viết sẵn đường đi trong code; LLM chỉ làm từng bước nhỏ như phân loại hay viết câu trả lời. Dễ đoán, dễ kiểm thử, rẻ, nhưng chỉ đi được những đường đã viết.
    • Agent: LLM tự chọn bước tiếp theo trong một vòng lặp: gọi công cụ, đọc kết quả, quyết định gọi tiếp hay trả lời. Xử lý được yêu cầu nhiều bước, có điều kiện, như "nếu đơn chưa giao thì thêm một món".

    Không nên dùng agent khi:

    • các loại yêu cầu biết trước và mỗi loại có cách xử lý rõ ràng (workflow đủ, nhanh và rẻ hơn);
    • một lần gọi LLM kèm thông tin phù hợp đã giải quyết được;
    • hành động có rủi ro cao (tiền, xóa dữ liệu) mà chưa có người duyệt.

    Điểm cộng: nhắc tới giới hạn số bước của vòng lặp, chi phí mỗi bước là một lần gọi mô hình, và thiết kế kết hợp, tức workflow ở cửa vào, agent chỉ cho yêu cầu phức tạp.

    Mở trang riêng của câu hỏi
  2. JuniorAI AgentLLM hallucination là gì? Khi xây chatbot chăm sóc khách hàng, bạn giảm nó bằng cách nào?

    Hallucination là khi mô hình ngôn ngữ sinh ra thông tin nghe hợp lý nhưng sai, hoặc không có trong dữ liệu được cung cấp. Ví dụ, khách hỏi "đơn 1041 giao chưa" mà request gửi cho mô hình không hề có dữ liệu đơn hàng, mô hình vẫn có thể trả lời "đơn của bạn đã giao rồi ạ".

    Ý quan trọng cần nói được là mô hình chỉ biết những gì nằm trong request cộng với kiến thức chung nó đã học. Nó không tự kết nối với database của bạn. Vì vậy, với dữ liệu thay đổi liên tục như trạng thái đơn, viết prompt khéo hơn không phải là cách sửa.

    Các cách giảm, từ dễ đến chắc chắn:

    • Đưa thông tin cần thiết vào prompt (giờ mở cửa, bảng giá) và dặn "chỉ dùng thông tin này".
    • Cho phép mô hình nói "chưa biết", kèm câu thay thế cụ thể. Tài liệu của Anthropic coi đây là kỹ thuật cơ bản đầu tiên.
    • Với dữ liệu động, để code tra dữ liệu thật rồi đưa kết quả cho mô hình, tức là dùng tool calling hoặc RAG.
    • Thêm lớp kiểm tra bằng code cho những câu trả lời rủi ro, và có test cho lớp đó.

    Điểm cộng: nói rõ các kỹ thuật này giảm chứ không loại bỏ hoàn toàn hallucination, nên việc có rủi ro cao (tiền, hứa hẹn với khách) vẫn cần người duyệt.

    Mở trang riêng của câu hỏi
  3. JuniorAI cho developerBạn chọn công cụ AI hỗ trợ lập trình (Copilot, Cursor, Claude Code…) dựa trên tiêu chí nào?

    Câu trả lời tốt không xếp hạng công cụ, mà cho thấy bạn hiểu chúng khác nhau ở đâu và bạn kiểm soát kết quả thế nào.

    Tiêu chí đầu tiên là mức độ giao việc. Gợi ý khi gõ (Copilot, Tab của Cursor) giữ bạn là người viết từng dòng. Agent trong editor sửa nhiều file và để bạn duyệt từng thay đổi. Agent tự chạy lệnh như Claude Code tự chạy test, đọc lỗi, sửa tiếp. Giao càng nhiều thì càng phải review kỹ.

    Tiêu chí thứ hai là quy định của công ty: công cụ nào được phép, code có được gửi ra ngoài không, có chính sách dữ liệu nào không. Ở công ty, đây thường là tiêu chí quyết định.

    Tiêu chí thứ ba là chi phí và gói miễn phí, vì giá thay đổi liên tục, nên luôn xem trang giá chính thức.

    Phần quan trọng nhất là cách bạn kiểm tra kết quả, dù dùng công cụ nào:

    • viết spec ngắn và test trước khi giao việc;
    • dặn rõ "đừng sửa test";
    • tự chạy test với điều kiện giống môi trường thật (ví dụ múi giờ UTC);
    • thử thêm vài trường hợp ngoài test, và giải thích được từng đoạn code trước khi merge.

    Điểm cộng: kể một lỗi cụ thể mà test hoặc việc tự thử thêm đã bắt được.

    Mở trang riêng của câu hỏi
  4. JuniorAI cho developerBạn dùng công cụ AI khi lập trình như thế nào? Làm sao biết code AI viết là đúng?

    Người phỏng vấn không hỏi bạn có dùng AI không; họ muốn biết bạn có kiểm soát được code mình nộp hay không. Một câu trả lời tốt mô tả một quy trình cụ thể:

    1. Định nghĩa "đúng" trước khi nhờ AI. Viết yêu cầu ngắn kèm vài ví dụ đầu vào và kết quả mong muốn, gồm cả trường hợp lỗi. Ví dụ: tin nhắn "cà phê 35k" phải ra 35.000đ, tin nhắn không có số tiền phải bị từ chối.
    2. Biến ví dụ thành test, để AI viết code cho tới khi test qua, và chính mình chạy lại test.
    3. Đọc và giải thích được từng đoạn code trước khi commit. Đoạn nào không giải thích được thì hỏi lại hoặc viết lại.
    4. Thử thêm các trường hợp ngoài ví dụ. Test chỉ bảo vệ những gì mình đã nghĩ tới; ví dụ "35 nghìn" có thể bị đọc thành 35 đồng mà test vẫn xanh.
    5. Kiểm chứng API và thư viện với tài liệu chính thống, vì AI có thể gọi hàm không tồn tại hoặc dùng cú pháp cũ.
    6. Không dán bí mật (API key, dữ liệu khách hàng, code công ty khi chưa được phép) vào công cụ AI.

    Điểm cộng: kể một lần cụ thể bạn phát hiện code AI sai và cách bạn phát hiện ra.

    Mở trang riêng của câu hỏi
  5. JuniorDatabaseVì sao thường không lưu ảnh trực tiếp trong database? Bạn lưu ảnh ở đâu?

    Ảnh là dữ liệu lớn và hầu như không bao giờ cần truy vấn theo nội dung. Lưu thẳng vào database (dạng nhị phân hay base64) gây nhiều vấn đề:

    • Tốn dung lượng đắt nhất. Dung lượng database thường giới hạn và đắt hơn kho file. Ví dụ gói miễn phí của Supabase có 500 MB database: một bài viết dài chỉ vài chục KB chữ, trong khi một ảnh chụp điện thoại đã vài MB. Vài chục bài có ảnh là đầy.
    • Chậm và nặng. Mỗi truy vấn kéo theo dữ liệu lớn, sao lưu và khôi phục lâu hơn, bộ nhớ đệm của database bị chiếm chỗ.
    • Không tận dụng được CDN. Ảnh trong kho file (object storage) được phục vụ qua CDN, lưu đệm gần người dùng; ảnh trong database phải đi qua máy chủ ứng dụng.

    Cách làm phổ biến: lưu file ảnh ở object storage (Supabase Storage, Amazon S3, Cloudflare R2…), còn database chỉ lưu đường dẫn và thông tin mô tả (kích thước, mô tả alt, người tải lên). Thêm vào đó, nén ảnh trước khi tải lên (resize, đổi sang WebP/AVIF) để giảm cả dung lượng lưu trữ lẫn băng thông.

    Điểm cộng: nhắc tới việc dọn ảnh không còn được bài nào dùng, và phân quyền cho kho file, tức ai được tải lên, ai được xem.

    Mở trang riêng của câu hỏi
  6. JuniorDự án & kinh nghiệmHãy kể về một dự án trong CV mà bạn tự hào nhất. Phần nào do chính bạn làm?

    Người phỏng vấn hỏi câu này để kiểm tra ba điều: dự án có thật sự do bạn làm không, bạn hiểu nó sâu đến đâu, và bạn có kể lại một việc kỹ thuật cho người khác hiểu được không.

    Một câu trả lời tốt dài khoảng 2 phút, theo thứ tự:

    1. Vấn đề: dự án giải quyết việc gì, cho ai. Một câu là đủ.
    2. Vai trò của bạn: với dự án nhóm, nói rõ phần nào bạn làm, ví dụ "mình làm toàn bộ phần đăng nhập và phân quyền". Đừng nói "chúng em làm" cho mọi thứ.
    3. Một quyết định kỹ thuật: bạn chọn cách làm nào, vì sao, và đã cân nhắc phương án nào khác. Ví dụ: "mình lưu ảnh trên storage và chỉ lưu đường dẫn trong database để không vượt giới hạn dung lượng".
    4. Một khó khăn và cách vượt qua: một lỗi cụ thể, bạn tìm ra nguyên nhân thế nào.
    5. Nếu làm lại: một điều bạn sẽ làm khác. Câu này cho thấy bạn biết tự nhìn lại.

    Nên tránh:

    • Kể lại danh sách tính năng như đọc README.
    • Nhận làm phần mà bạn không trả lời được câu hỏi tiếp theo về nó. Người phỏng vấn thường hỏi sâu thêm hai, ba tầng vào đúng phần bạn nhận.

    Chuẩn bị trước: với mỗi dự án trong CV, viết sẵn 5 ý trên và tập nói thành lời ít nhất một lần.

    Mở trang riêng của câu hỏi
  7. JuniorDự án & kinh nghiệmNếu được làm lại dự án cá nhân của mình từ đầu, bạn sẽ thay đổi điều gì?

    Câu này kiểm tra xem bạn có tự nhìn lại công việc của mình không. Trả lời "em thấy ổn rồi, không đổi gì" là câu yếu nhất, vì nó cho thấy bạn chưa học được gì từ dự án.

    Một câu trả lời tốt thường có ba phần:

    • Một quyết định cụ thể bạn sẽ đổi, kèm lý do. Ví dụ: "Em sẽ viết bảng ví dụ và test trước khi viết hàm đọc số tiền. Lần đầu em viết code trước, và một tin nhắn 'trà sữa 32K' bị đọc thành 32 tỷ."
    • Điều bạn sẽ giữ nguyên, để cho thấy bạn phân biệt được quyết định tốt và quyết định tệ. Ví dụ: "Em vẫn giữ việc từ chối khi tin nhắn có hai số tiền, vì ghi sai tiền tệ hơn bắt người dùng nhắn lại."
    • Bài học áp dụng được cho việc khác, như cắt phạm vi sớm, nghĩ tới múi giờ của máy chủ, hay hỏi người dùng thật sớm hơn.

    Tránh hai cái bẫy: chê công nghệ đã chọn mà không nêu được vì sao, và kể một thay đổi lớn đến mức nghe như đập đi làm lại toàn bộ.

    Điểm cộng: chuẩn bị trước câu này cho dự án đầu tiên trong CV, vì người phỏng vấn thường hỏi tiếp ngay sau câu "kể về dự án của bạn".

    Mở trang riêng của câu hỏi
  8. JuniorGitGit merge và git rebase khác nhau thế nào? Khi nào nên dùng cái nào?

    Cả hai đều đưa thay đổi từ nhánh này sang nhánh khác, nhưng ghi lại lịch sử theo hai cách khác nhau.

    • git merge tạo một merge commit nối hai nhánh. Lịch sử giữ nguyên, thấy rõ nhánh đã tách và nhập lại khi nào.
    • git rebase viết lại các commit của nhánh bạn lên trên đầu nhánh đích. Lịch sử thành một đường thẳng, dễ đọc hơn, nhưng mã hash của các commit thay đổi.
    Cập nhật nhánh feature từ main
    git switch feat/login
    git fetch origin
    git rebase origin/main   # lịch sử thẳng, chỉ làm trên nhánh của riêng bạn

    Trong phỏng vấn, nói thêm được đánh đổi (lịch sử thật so với lịch sử dễ đọc) và quy tắc của team bạn đang dùng sẽ ghi điểm hơn là chỉ định nghĩa.

    Mở trang riêng của câu hỏi
  9. JuniorHTTP & WebGET và POST khác nhau ra sao? Vì sao không nên dùng GET để xóa dữ liệu?

    GET dùng để đọc dữ liệu: tham số nằm trên URL, có thể được cache, lưu lịch sử và được trình duyệt hay công cụ quét tự gọi lại (prefetch, xem trước liên kết). POST dùng để gửi dữ liệu làm thay đổi trạng thái, dữ liệu nằm trong body.

    GET phải an toàn (không thay đổi gì) và idempotent (gọi nhiều lần cũng như một lần). Nếu một link GET xóa dữ liệu, chỉ cần một bot quét link hay trình duyệt prefetch là dữ liệu mất.

    Mở trang riêng của câu hỏi
  10. JuniorJavaScriptBreakpoint khác console.log thế nào khi debug? Khi nào bạn vẫn dùng console.log?

    Breakpoint dừng chương trình ở một dòng, cho bạn xem mọi biến đang có giá trị gì và kiểu gì, xem ai đã gọi hàm (call stack), và gõ thêm biểu thức để tính ngay trong ngữ cảnh đó. console.log chỉ in ra thứ bạn đoán trước là cần in.

    Một ví dụ cho thấy khác biệt: giá món ăn được lưu nhầm thành chuỗi "35.000". console.log(item.price) in ra 35.000, nhìn y như ba mươi lăm nghìn. Trong khung Variables của debugger, giá trị hiện kèm dấu nháy, và typeof trả về 'string'. Lỗi lộ ra ngay: "35.000" * 2 bằng 70 vì JavaScript đọc dấu chấm là dấu thập phân.

    Các công cụ nên kể được: breakpoint có điều kiện (chỉ dừng khi order.id === 1044), Watch, Debug Console, Call Stack, và logpoint (in log mà không dừng, không sửa code).

    console.log vẫn hợp lý khi:

    • viết script nhỏ chạy một lần;
    • lỗi phụ thuộc thời gian, việc dừng chương trình làm đổi thứ tự chạy;
    • trên production, nơi không gắn debugger được, và lúc đó nên ghi log có cấu trúc.

    Điểm cộng: biết log bằng JSON.stringify(value) hoặc { value } để thấy rõ chuỗi hay số.

    Mở trang riêng của câu hỏi
  11. JuniorJavaScriptKhác nhau giữa var, let và const trong JavaScript?
    • var có phạm vi hàm, bị hoisting (khai báo được đưa lên đầu hàm với giá trị undefined) và cho khai báo lại.
    • let có phạm vi khối { }, không cho khai báo lại trong cùng khối, và không dùng được trước dòng khai báo.
    • const giống let nhưng không gán lại được. Lưu ý: object hay mảng khai báo bằng const vẫn sửa được bên trong.
    const không có nghĩa là bất biến
    const user = { name: "An" };
    user.name = "Bình"; // được
    user = {};          // TypeError

    Thói quen tốt: mặc định dùng const, chỉ dùng let khi thật sự cần gán lại, và tránh var trong code mới.

    Mở trang riêng của câu hỏi
  12. JuniorJavaScriptStack trace là gì? Khi gặp "Cannot read properties of undefined", bạn tìm nguyên nhân thế nào?

    Stack trace là danh sách các hàm đang được gọi dở tại thời điểm lỗi xảy ra, kèm tên file, số dòng và số cột của từng lời gọi. Trong JavaScript, khung trên cùng là nơi lỗi xảy ra; các khung bên dưới là đường đi dẫn tới đó.

    Với lỗi TypeError: Cannot read properties of undefined (reading 'name'), một câu trả lời tốt đi theo các bước:

    1. Đọc thông điệp: thuộc tính đang đọc là name, nên thứ đứng ngay trước .name đang là undefined.
    2. Tìm khung đầu tiên thuộc code của mình: bỏ qua các khung trong node_modules hay node:internal, mở đúng file, dòng và cột.
    3. Lần ngược nguồn dữ liệu: dòng báo lỗi là nơi lỗi lộ ra, chưa chắc là nơi bắt đầu. Đi xuống các khung để xem giá trị undefined được truyền từ đâu: API chưa trả về, state ban đầu rỗng, hay dữ liệu thiếu trường.
    4. Tái hiện bằng ví dụ nhỏ nhất, rồi mới sửa đúng chỗ: sửa dữ liệu ở nguồn, hoặc xử lý trường hợp hợp lệ bằng giá trị mặc định hay ?..

    Điểm cộng nếu bạn nhắc thêm: khi bắt lỗi rồi ném lại, dùng new Error("…", { cause: err }) để không mất lỗi gốc; còn với code production đã nén, cần source map mới đọc được stack trace.

    Mở trang riêng của câu hỏi
  13. JuniorNext.jsTrong Next.js App Router, nhóm route (folder) và thư mục riêng tư _folder khác nhau thế nào? Khi nào dùng cái nào?

    Cả hai đều là quy ước đặt tên thư mục trong app, nhưng giải quyết hai vấn đề khác nhau.

    Nhóm route là thư mục có tên trong ngoặc tròn, như (site) hay (shop). Next.js dùng nó để gom các trang lại nhưng không đưa tên nhóm vào URL: app/(site)/bai-viet/page.tsx vẫn là /bai-viet. Công dụng chính là cho một nhóm trang dùng chung một layout riêng. Ví dụ, trang công khai của blog cần header, footer và được tạo sẵn, còn trang quản trị cần khung riêng và tạo mới ở mọi lượt truy cập. Đặt trang công khai vào (site) có layout riêng thì hai bên tách bạch mà URL không đổi.

    Thư mục riêng tư có tên bắt đầu bằng gạch dưới, như _components. Next.js bỏ qua thư mục này và mọi thư mục con khi tạo đường dẫn, nên nó dùng để đặt code không phải trang (component, hàm phụ) ngay cạnh trang.

    Một điểm hay bị hỏi thêm: ngay cả không có gạch dưới, file trong app cũng không thành trang nếu không phải page hoặc route. Vì vậy _folder chủ yếu để nói rõ ý đồ và tránh trùng tên với các file đặc biệt của Next.js.

    Điểm cộng: nhắc tới việc hai layout có thể cùng ứng với / (layout gốc và layout của nhóm) nhưng bọc phạm vi khác nhau.

    Mở trang riêng của câu hỏi
  14. MiddleDatabaseIndex trong database là gì? Có nên đánh index cho mọi cột không?

    Index là một cấu trúc dữ liệu phụ (thường là B-tree) giúp tìm hàng theo giá trị một cột mà không phải quét toàn bảng, giống mục lục của một cuốn sách.

    Không nên đánh index mọi cột vì:

    • Mỗi lần INSERT, UPDATE, DELETE đều phải cập nhật thêm index, ghi chậm hơn.
    • Index tốn dung lượng.
    • Cột ít giá trị khác nhau (như status chỉ có 2 giá trị) thường không được lợi nhiều.
    Đánh index cho truy vấn thật sự hay chạy
    create index posts_published_at_idx on posts (published_at desc)
      where status = 'published';

    Câu trả lời tốt sẽ nhắc tới việc đo trước bằng EXPLAIN ANALYZE rồi mới thêm index cho đúng truy vấn chậm.

    Mở trang riêng của câu hỏi
  15. MiddleLàm việc nhómBạn xử lý thế nào khi không đồng ý với góp ý trong code review?

    Người phỏng vấn muốn biết bạn có tách được cái tôi khỏi code hay không.

    1. Hỏi lại để hiểu đúng lý do của reviewer trước khi phản hồi.
    2. Trả lời bằng dữ kiện: đo đạc, tài liệu, quy ước của team, không bằng cảm giác.
    3. Nếu vẫn khác ý và vấn đề nhỏ, chọn cách của reviewer hoặc ghi lại để bàn sau, đừng chặn PR.
    4. Nếu vấn đề lớn, chuyển sang trao đổi trực tiếp thay vì tranh luận dài trong comment.

    Kể một ví dụ thật, ngắn gọn, có kết quả cụ thể sẽ thuyết phục hơn liệt kê nguyên tắc.

    Mở trang riêng của câu hỏi
  16. MiddleReactuseEffect trong React chạy khi nào? Mảng phụ thuộc dùng để làm gì?

    useEffect chạy sau khi React đã cập nhật giao diện. Nó dùng để đồng bộ component với thứ bên ngoài React: gọi API, đăng ký sự kiện, hẹn giờ.

    • Không có mảng phụ thuộc: chạy sau mỗi lần render.
    • Mảng rỗng []: chạy một lần sau lần render đầu tiên.
    • Có giá trị [id]: chạy lại khi id thay đổi.

    Hàm trả về từ effect là hàm dọn dẹp, chạy trước lần effect tiếp theo và khi component bị gỡ, ví dụ để hủy hẹn giờ hay gỡ sự kiện.

    Mở trang riêng của câu hỏi
  17. SeniorKiến trúcBạn sẽ thiết kế giới hạn tốc độ (rate limiting) cho một API công khai thế nào?

    Bắt đầu bằng câu hỏi làm rõ: giới hạn theo ai (IP, API key, người dùng), mức nào, chặn cứng hay giảm tốc, hệ thống chạy một hay nhiều máy chủ.

    Các thuật toán thường gặp:

    • Fixed window: đếm theo khung phút, đơn giản nhưng dồn request ở mép khung.
    • Sliding window: mượt hơn, tốn bộ nhớ hơn.
    • Token bucket: cho phép bùng nổ ngắn, phổ biến nhất cho API.

    Với nhiều máy chủ, bộ đếm cần nằm ở chỗ dùng chung như Redis với thao tác nguyên tử. Trả về mã 429 Too Many Requests kèm header Retry-After, và ghi log để phát hiện lạm dụng.

    Ở mức senior, nói thêm về đánh đổi: độ chính xác so với chi phí, xử lý khi Redis lỗi (cho qua hay chặn hết), và giới hạn khác nhau cho từng gói khách hàng.

    Mở trang riêng của câu hỏi
  18. SeniorKiến trúcKhi bắt đầu một dự án mới, bạn chọn công nghệ dựa trên những tiêu chí nào?

    Bắt đầu từ bài toán và ràng buộc, không bắt đầu từ công nghệ đang hot.

    • Ai sẽ vận hành và bảo trì sau 6 tháng, team đã quen gì.
    • Ràng buộc thật: ngân sách, hạn chót, SEO, yêu cầu offline, quy mô người dùng.
    • Chi phí chuyển đổi nếu chọn sai, và có lối thoát hay không.
    • Hệ sinh thái, tài liệu, khả năng tuyển người.

    So sánh tối đa ba lựa chọn theo đúng các tiêu chí đó và ghi lại quyết định (một tài liệu ADR ngắn) để lần sau xem lại có còn đúng không.

    Mở trang riêng của câu hỏi