Câu hỏi phỏng vấn
Câu hỏi phỏng vấn kỹ sư phần mềm
Luyện tập các câu hỏi phỏng vấn kỹ sư phần mềm về các dạng bài lập trình, cấu trúc dữ liệu, thuật toán, system design, gỡ lỗi, kiểm thử, tư duy vận hành production, cùng câu hỏi hành vi và cộng tác. Hãy dùng danh sách câu hỏi trọng tâm này cùng với nội dung đầy đủ trong Cẩm nang phỏng vấn kỹ sư phần mềm.
19 câu hỏi
6 danh mục
Kỹ sư phần mềm
Cập nhật tháng 5 năm 2026
Mảng, chuỗi và hash map
Mảng, chuỗi và hash map là nền tảng của rất nhiều vòng sàng lọc kỹ thuật. Người phỏng vấn dùng chúng để kiểm tra sự cẩn thận khi xử lý chỉ số, kỹ thuật đếm tần suất, hai con trỏ, cửa sổ trượt và độ chính xác khi cài đặt.
Phương pháp: Tra cứu phần bù
Cách vét cạn là kiểm tra mọi cặp, mất O(n^2). Cách tốt hơn là dùng một hash map ánh xạ từ giá trị sang chỉ số. Khi duyệt mảng, với mỗi số x, giá trị cần tìm là target - x. Nếu phần bù đó đã có trong map, ta trả về chỉ số hiện tại và chỉ số đã lưu. Nếu chưa, lưu x cùng chỉ số của nó rồi duyệt tiếp. Ý then chốt về tính đúng đắn là khi xử lý chỉ số i, map chứa đúng các giá trị ở những chỉ số đứng trước. Nghĩa là nếu có một cặp hợp lệ kết thúc tại i, ta sẽ tìm thấy nó ngay. Cách này cũng tránh dùng một phần tử hai lần, vì ta chỉ ghép với các phần tử đứng trước. Độ phức tạp thời gian là O(n) vì mỗi giá trị chỉ được thêm vào và tra cứu một lần. Độ phức tạp bộ nhớ là O(n) cho map. Các trường hợp biên gồm giá trị trùng lặp, số âm, số 0 và trường hợp không có cặp hợp lệ nếu đề bài cho phép.
Câu hỏi nối tiếp thường gặp
Nếu mảng đã được sắp xếp thì cách làm thay đổi thế nào?
Nếu cần tìm tất cả các cặp thay vì một cặp thì sao?
Làm sao để không trả về cùng một phần tử hai lần?
Phương pháp: Cửa sổ trượt kèm chỉ số xuất hiện gần nhất
Dùng cửa sổ trượt sao cho cửa sổ luôn chỉ chứa các ký tự không trùng nhau. Duy trì con trỏ left và một map từ ký tự sang chỉ số xuất hiện gần nhất của nó. Cho right duyệt qua toàn bộ chuỗi. Nếu ký tự hiện tại đã xuất hiện bên trong cửa sổ hiện tại, dời left đến vị trí ngay sau chỉ số trước đó của ký tự này. Sau đó cập nhật chỉ số của ký tự và ghi nhận độ dài cửa sổ lớn nhất. Chi tiết quan trọng là left chỉ được tiến về phía trước. Nếu một ký tự từng xuất hiện trước cửa sổ hiện tại, nó không được làm cửa sổ co lại. Đó là lý do phép cập nhật là left = max(left, lastSeen[char] + 1). Độ phức tạp thời gian là O(n) vì mỗi ký tự được xử lý một lần và con trỏ left chỉ tiến về phía trước. Độ phức tạp bộ nhớ là O(k), với k là kích thước bộ ký tự. Các trường hợp biên gồm chuỗi rỗng, chuỗi toàn ký tự khác nhau, chuỗi toàn ký tự lặp lại, và Unicode nếu ngôn ngữ lập trình xử lý ký tự khác với byte.
Câu hỏi nối tiếp thường gặp
Nếu đầu vào chứa ký tự Unicode thì sao?
Bạn sẽ trả về chính chuỗi con đó như thế nào?
Nếu mỗi ký tự được phép xuất hiện tối đa hai lần thì sao?
Phương pháp: Khóa chuẩn hóa
Các từ đảo chữ có cùng thành phần ký tự. Tạo một khóa chuẩn hóa cho mỗi từ và nhóm các từ theo khóa đó trong hash map. Khóa trực tiếp nhất là các ký tự của từ sau khi sắp xếp. Ví dụ, “eat”, “tea” và “ate” đều ánh xạ về “aet”. Nếu chuỗi ngắn, sắp xếp từng từ là cách đơn giản và đáng tin cậy. Nếu bảng chữ cái cố định là chữ thường tiếng Anh và chuỗi dài, một bộ đếm tần suất 26 phần tử có thể nhanh hơn vì tránh được chi phí sắp xếp O(k log k) cho mỗi từ. Độ phức tạp thời gian khi sắp xếp là O(n * k log k), với n là số chuỗi và k là độ dài trung bình của chuỗi. Với cách đếm trên bảng chữ cái cố định, có thể đạt O(n * k). Độ phức tạp bộ nhớ là O(n * k) cho kết quả đầu ra cộng các khóa. Các trường hợp biên gồm chuỗi rỗng, phần tử trùng lặp, quy tắc chữ hoa/chữ thường và ký tự ngoài bảng chữ cái tiếng Anh.
Câu hỏi nối tiếp thường gặp
Khi nào bạn dùng khóa tần suất thay vì sắp xếp?
Bạn sẽ nhóm không phân biệt chữ hoa, chữ thường như thế nào?
Nếu bộ ký tự không cố định thì sao?
Danh sách liên kết, cây và đồ thị
Các bài về con trỏ và phép duyệt kiểm tra xem bạn có quản lý trạng thái cẩn thận hay không. Phần khó thường không nằm ở cú pháp mà ở việc xác định mỗi con trỏ, hàng đợi, ngăn xếp hay tập đã thăm đại diện cho điều gì.
Phương pháp: Ba con trỏ: previous, current, next
Dùng ba con trỏ. previous khởi tạo là null, current bắt đầu tại head. Với mỗi nút, lưu next = current.next, trỏ current.next về previous, rồi dời previous sang current và current sang next. Khi current trở thành null, previous chính là head mới. Bất biến ở đây là previous trỏ tới phần đã được đảo ngược, còn current trỏ tới nút đầu tiên chưa được đảo. Việc lưu next trước khi thay đổi current.next là bắt buộc; nếu không, phần còn lại của danh sách sẽ bị mất. Độ phức tạp thời gian là O(n). Độ phức tạp bộ nhớ là O(1). Các trường hợp biên gồm danh sách rỗng, danh sách chỉ có một nút, và bảo đảm head ban đầu trở thành tail với next được gán null.
Câu hỏi nối tiếp thường gặp
Bạn sẽ chỉ đảo ngược đoạn con từ vị trí m đến n như thế nào?
Bạn sẽ đảo ngược danh sách bằng đệ quy như thế nào?
Làm sao phát hiện chu trình trước khi đảo ngược?
Phương pháp: Ràng buộc khoảng giá trị
Một lỗi phổ biến là chỉ kiểm tra mỗi nút có lớn hơn con trái và nhỏ hơn con phải hay không. Như vậy là chưa đủ, vì toàn bộ cây con phải thỏa mãn ràng buộc từ các nút tổ tiên. Dùng DFS với cận dưới và cận trên. Với mỗi nút, kiểm tra lower < node.value < upper. Con trái giữ nguyên cận dưới và lấy giá trị hiện tại làm cận trên. Con phải lấy giá trị hiện tại làm cận dưới và giữ nguyên cận trên. Nếu có nút nào vi phạm khoảng của nó, trả về false. Độ phức tạp thời gian là O(n) vì mỗi nút được thăm một lần. Độ phức tạp bộ nhớ là O(h) cho độ sâu đệ quy, với h là chiều cao cây. Các trường hợp biên gồm giá trị trùng lặp, biên min/max của số nguyên, cây rỗng và cây lệch. Cần hỏi rõ có cho phép giá trị trùng lặp không, và nếu có thì chúng nằm ở phía nào.
Câu hỏi nối tiếp thường gặp
Bạn sẽ giải bài này bằng phép duyệt trung thứ tự (inorder) như thế nào?
Nếu cho phép giá trị trùng lặp thì sao?
Độ sâu đệ quy trong trường hợp xấu nhất là bao nhiêu?
Phương pháp: Tìm kiếm theo chiều rộng theo từng lớp khoảng cách
Mô hình hóa mỗi ô trống thành một nút của đồ thị và mỗi bước đi lên, xuống, trái, phải thành một cạnh có trọng số như nhau. Vì mọi bước đi đều có cùng chi phí, BFS cho ra đường đi ngắn nhất. Bắt đầu từ ô nguồn, đưa nó vào hàng đợi với khoảng cách 0 và đánh dấu đã thăm. Với mỗi ô lấy ra khỏi hàng đợi, xét các ô kề hợp lệ, chưa thăm, nằm trong lưới và không phải chướng ngại vật. Lần đầu tiên chạm tới đích, khoảng cách đó là nhỏ nhất. Tính đúng đắn đến từ việc BFS xử lý các nút theo thứ tự khoảng cách tăng dần. Khi một ô đã được thăm, việc đến được nó muộn hơn không thể tạo ra đường đi ngắn hơn trong đồ thị không trọng số. Độ phức tạp thời gian là O(rows * cols) vì mỗi ô được thăm tối đa một lần. Độ phức tạp bộ nhớ là O(rows * cols) cho hàng đợi và tập đã thăm. Các trường hợp biên gồm điểm đầu hoặc điểm cuối bị chặn, nguồn trùng đích, không có đường đi, và có cho phép đi chéo hay không.
Câu hỏi nối tiếp thường gặp
Nếu cho phép đi chéo thì cách làm thay đổi thế nào?
Nếu đi qua chướng ngại vật chỉ tốn thêm chi phí thay vì bị cấm thì sao?
Bạn sẽ dựng lại đường đi cụ thể như thế nào?
Phương pháp: Map đã thăm từ nút gốc sang nút bản sao
Dùng một hash map ánh xạ mỗi nút gốc sang nút bản sao của nó. Trong quá trình DFS hoặc BFS, khi thăm một nút gốc, tạo bản sao nếu chưa có. Sau đó sao chép từng nút kề bằng đệ quy hoặc vòng lặp, rồi thêm nút kề đã sao chép vào danh sách kề của nút bản sao. Map đã thăm giải quyết hai vấn đề: ngăn vòng lặp vô hạn trong đồ thị có chu trình, và giữ nguyên các tham chiếu dùng chung để đồ thị bản sao có cùng cấu trúc liên kết với đồ thị gốc. Độ phức tạp thời gian là O(V + E), với V là số nút và E là số cạnh. Độ phức tạp bộ nhớ là O(V) cho map và ngăn xếp hoặc hàng đợi khi duyệt, chưa tính đồ thị đầu ra. Các trường hợp biên gồm đầu vào null, cạnh tự nối (self-loop), đồ thị không liên thông nếu đề bài chỉ yêu cầu sao chép từ một nút xuất phát, và nút kề bị lặp.
Câu hỏi nối tiếp thường gặp
Bạn sẽ xử lý đồ thị không liên thông như thế nào?
Với đồ thị có hướng thì cách làm thay đổi thế nào?
Làm sao tránh đệ quy vô hạn?
Quy hoạch động và quay lui
Các câu hỏi quy hoạch động và quay lui kiểm tra khả năng định nghĩa trạng thái. Câu trả lời tốt nhất giải thích được quyết định tại mỗi trạng thái, công thức truy hồi, và vì sao bài toán có các bài toán con gối nhau hoặc áp dụng được cắt tỉa.
Phương pháp: DP từ dưới lên (bottom-up) theo số tiền
Định nghĩa dp[a] là số xu ít nhất cần để tạo thành số tiền a. Khởi tạo dp[0] = 0 và mọi giá trị khác là vô cùng. Với mỗi số tiền từ 1 đến target, thử từng đồng xu. Nếu a - coin không âm, dp[a] = min(dp[a], dp[a - coin] + 1). Cuối cùng, nếu dp[target] vẫn là vô cùng, trả về -1. Công thức truy hồi đúng vì đồng xu cuối cùng trong một lời giải tối ưu phải là một trong các mệnh giá. Nếu đồng xu cuối có giá trị c, số tiền còn lại là target - c, và phần này cũng phải được giải tối ưu. Độ phức tạp thời gian là O(số tiền * số mệnh giá). Độ phức tạp bộ nhớ là O(số tiền). Các trường hợp biên gồm số tiền bằng 0, số tiền không thể tạo thành, có đồng xu mệnh giá 1, mệnh giá trùng lặp và số tiền mục tiêu lớn.
Câu hỏi nối tiếp thường gặp
Bạn sẽ trả về chính các đồng xu đã dùng như thế nào?
Nếu mỗi đồng xu chỉ được dùng một lần thì sao?
Nếu cần đếm số tổ hợp thì bạn làm thế nào?
Phương pháp: DP theo vị trí kết thúc, sau đó tối ưu bằng tìm kiếm nhị phân
Một lời giải DP rõ ràng định nghĩa dp[i] là độ dài dãy con tăng dài nhất kết thúc tại chỉ số i. Với mỗi i, xét mọi j đứng trước. Nếu nums[j] < nums[i] thì nums[i] có thể nối tiếp dãy con kết thúc tại j, nên dp[i] = max(dp[i], dp[j] + 1). Đáp án là max(dp). Cách này mất thời gian O(n^2) và bộ nhớ O(n). Cách tối ưu O(n log n) duy trì một mảng tails, trong đó tails[length - 1] là giá trị kết thúc nhỏ nhất có thể của một dãy con tăng có độ dài đó. Với mỗi số, tìm kiếm nhị phân phần tử đầu tiên trong tails lớn hơn hoặc bằng nó rồi thay thế phần tử đó. Độ dài của tails chính là độ dài dãy con tăng dài nhất (LIS). Trong buổi phỏng vấn, nếu thời gian có hạn, trình bày DP O(n^2) trước rồi mới bàn đến phiên bản tối ưu là hợp lý. Các trường hợp biên gồm phần tử trùng lặp, mảng giảm dần, đầu vào rỗng, và dãy con có bắt buộc tăng ngặt hay không.
Câu hỏi nối tiếp thường gặp
Bạn sẽ dựng lại dãy con như thế nào?
Với dãy con không giảm thì cần thay đổi gì?
Vì sao thay thế một phần tử trong tails không làm mất đáp án?
Phương pháp: Chỉ xây dựng các tiền tố hợp lệ
Dùng quay lui với hai biến đếm: số ngoặc mở đã dùng (open) và số ngoặc đóng đã dùng (close). Ở mỗi bước, ta có thể thêm ngoặc mở nếu open < n, và thêm ngoặc đóng nếu close < open. Điều kiện thứ hai bảo đảm mọi tiền tố luôn hợp lệ, nên ta không bao giờ sinh ra chuỗi sai rồi phải lọc bỏ về sau. Khi độ dài chuỗi hiện tại đạt 2n, thêm nó vào kết quả. Cách này chỉ khám phá các tiền tố hợp lệ và cắt tỉa sớm những trạng thái không thể dẫn tới lời giải. Số kết quả hợp lệ là số Catalan thứ n, nên kích thước đầu ra chi phối thời gian chạy. Độ phức tạp bộ nhớ gồm độ sâu đệ quy O(n) cộng với đầu ra. Các trường hợp biên gồm n = 0 hoặc n = 1, tùy cách đề bài định nghĩa.
Câu hỏi nối tiếp thường gặp
Vì sao chỉ được đóng ngoặc khi close < open?
Bạn sẽ xử lý nhiều loại dấu ngoặc như thế nào?
Bạn có thể sinh kết quả theo thứ tự từ điển không?
Phỏng vấn system design
Phỏng vấn system design đánh giá độ chín chắn trong nghề kỹ sư. Với vị trí trung cấp, mục tiêu là nắm được kiến trúc cơ bản và luồng dữ liệu. Với vị trí cấp cao, yêu cầu là phân tích được đánh đổi, điểm nghẽn, độ tin cậy, khả năng quan sát (observability) và trách nhiệm vận hành.
Phương pháp: Yêu cầu -> API -> sinh ID -> lưu trữ -> luồng chuyển hướng -> mở rộng
Yêu cầu: người dùng gửi một URL dài và nhận lại một URL ngắn. Khi ai đó truy cập URL ngắn, họ phải được chuyển hướng nhanh chóng. Các tính năng tùy chọn gồm alias tùy chỉnh, thời hạn hết hiệu lực, thống kê truy cập (analytics), phát hiện spam và tài khoản người dùng. API cốt lõi: createShortUrl(longUrl, optionalAlias, expiration) và redirect(shortCode). Mô hình dữ liệu: shortCode, longUrl, userId, createdAt, expiresAt, status và metadata thống kê nếu cần. Để sinh ID, có thể dùng ID tự tăng mã hóa base62, mã base62 ngẫu nhiên kèm kiểm tra trùng lặp, hoặc sinh ID phân tán. ID tự tăng đơn giản nhưng để lộ số lượng URL đã tạo nếu không được làm rối. ID ngẫu nhiên dễ phân tán nhưng cần xử lý trùng lặp. Ở quy mô vừa phải, mã base62 ngẫu nhiên kết hợp ràng buộc duy nhất trong cơ sở dữ liệu là chấp nhận được. Luồng đọc là quan trọng nhất. Chuyển hướng phải nhanh: tải shortCode từ cache, nếu không có thì đọc từ cơ sở dữ liệu, kiểm tra thời hạn/trạng thái, rồi trả về HTTP 301 hoặc 302. Dùng 302 nếu cần thống kê hoặc URL đích có thể thay đổi; dùng 301 cho chuyển hướng vĩnh viễn khi muốn trình duyệt cache mạnh hơn. Mở rộng: cache các mã ngắn được truy cập nhiều trong Redis hoặc edge cache, phân mảnh (shard) theo shortCode nếu cần, ghi dữ liệu thống kê bất đồng bộ qua hàng đợi, giới hạn tần suất tạo link, và chạy kiểm tra spam/mã độc. Giám sát độ trễ chuyển hướng, tỷ lệ cache hit, tỷ lệ lỗi, báo cáo lạm dụng và mức độ bão hòa của cơ sở dữ liệu.
Câu hỏi nối tiếp thường gặp
Bạn sẽ sinh mã ngắn duy nhất như thế nào?
Bạn sẽ dùng chuyển hướng 301 hay 302?
Bạn sẽ hỗ trợ thống kê truy cập mà không làm chậm chuyển hướng như thế nào?
Phương pháp: Người dùng -> bài đăng -> fanout -> xếp hạng -> đọc -> tính nhất quán
Trước hết, làm rõ loại bảng tin: bảng tin theo dõi trên mạng xã hội, bảng tin đề xuất hay kết hợp cả hai. Giả sử đây là bảng tin theo dõi, nơi người dùng tạo bài đăng và người theo dõi thấy các bài đó được sắp xếp theo mức độ liên quan hoặc độ mới. Các đối tượng cốt lõi: người dùng, quan hệ theo dõi, bài đăng, media, mục bảng tin, lượt thích/bình luận và các đặc trưng xếp hạng. API gồm createPost, getFeed, followUser, unfollowUser và interactWithPost. Có hai mô hình tạo bảng tin phổ biến. Fanout-on-write đẩy bài mới vào kho bảng tin của từng người theo dõi ngay khi bài được tạo. Cách này giúp đọc nhanh nhưng ghi tốn kém với những người dùng có nhiều người theo dõi. Fanout-on-read tính toán bảng tin khi có yêu cầu bằng cách lấy bài đăng từ những người đang được theo dõi. Cách này giúp ghi rẻ nhưng đọc chậm hơn. Cách kết hợp khá phổ biến: fanout-on-write cho người dùng thông thường và fanout-on-read hoặc xử lý riêng cho tài khoản người nổi tiếng. Kiến trúc: dịch vụ bài đăng ghi bài, dịch vụ đồ thị quan hệ lưu quan hệ theo dõi, dịch vụ bảng tin duy trì dòng thời gian, dịch vụ xếp hạng chấm điểm các bài ứng viên, cache lưu các bảng tin được truy cập nhiều, còn media được đưa lên lưu trữ đối tượng/CDN. Dùng hàng đợi cho fanout bất đồng bộ và cập nhật xếp hạng. Đánh đổi: độ mới của bảng tin so với chất lượng xếp hạng, tính nhất quán khi quan hệ theo dõi thay đổi, xử lý người dùng nổi tiếng, vô hiệu hóa cache, spam, chặn vì lý do quyền riêng tư và bù dữ liệu (backfill) sau khi hàng đợi gặp lỗi. Giám sát độ trễ tải bảng tin, độ mới, độ trễ fanout, mức độ tương tác, tỷ lệ cache hit, lỗi xếp hạng và báo cáo lạm dụng.
Câu hỏi nối tiếp thường gặp
Bạn sẽ xử lý người dùng có hàng triệu người theo dõi như thế nào?
Bạn sẽ xếp hạng các mục trong bảng tin ra sao?
Điều gì xảy ra nếu hàng đợi fanout bị chậm?
Phương pháp: Kết nối -> luồng tin nhắn -> lưu trữ -> gửi nhận -> độ tin cậy
Yêu cầu: tin nhắn một-một và nhóm, gửi tới người dùng đang trực tuyến lẫn ngoại tuyến, lịch sử tin nhắn, trạng thái đã xem, chỉ báo đang nhập và thông báo đẩy. Cần làm rõ có yêu cầu mã hóa đầu cuối, tệp đính kèm và đồng bộ đa thiết bị hay không. Client duy trì kết nối WebSocket tới một dịch vụ gateway. Khi người dùng gửi tin nhắn, gateway xác thực, gán hoặc nhận khóa idempotency (chống xử lý trùng), ghi tin nhắn vào bộ lưu trữ bền vững, phát một sự kiện, rồi gửi tới những người nhận đang trực tuyến qua các kết nối đang mở của họ. Người dùng ngoại tuyến nhận thông báo đẩy và có thể tải lịch sử sau. Mô hình dữ liệu: cuộc trò chuyện, người tham gia, tin nhắn, trạng thái gửi, trạng thái đã đọc và thiết bị/phiên. Bộ lưu trữ cần đọc hiệu quả theo cuộc trò chuyện và thời gian. Cơ sở dữ liệu quan hệ có thể đáp ứng ở quy mô vừa phải; ở quy mô lớn hơn thường dùng kho lưu trữ phân tán. Tệp đính kèm nên đưa lên lưu trữ đối tượng, không lưu trong cơ sở dữ liệu tin nhắn. Các vấn đề về độ tin cậy: gửi trùng, tin đến sai thứ tự, kết nối lại, sót tin, fanout cho nhóm lớn và độ chính xác của trạng thái trực tuyến. Dùng khóa idempotency, số thứ tự tăng đơn điệu cho mỗi cuộc trò chuyện, cơ chế xác nhận (acknowledgement), logic thử lại và API đồng bộ từ tin nhắn cuối cùng đã nhận. Giám sát độ trễ gửi tin, tỷ lệ gửi thành công, số kết nối WebSocket, tỷ lệ kết nối lại, độ trễ hàng đợi, lỗi thông báo đẩy và lỗi ghi vào bộ lưu trữ.
Câu hỏi nối tiếp thường gặp
Bạn sẽ bảo đảm thứ tự tin nhắn như thế nào?
Bạn sẽ hỗ trợ nhiều thiết bị cho mỗi người dùng ra sao?
Bạn sẽ lưu những gì cho trạng thái đã xem?
Gỡ lỗi, kiểm thử và tư duy vận hành production
Nhiều buổi phỏng vấn kỹ sư phần mềm hiện nay có thêm các tình huống thực tế về gỡ lỗi, kiểm thử hoặc vận hành production. Những câu hỏi này phân biệt ứng viên chỉ biết viết code với ứng viên có thể vận hành phần mềm một cách có trách nhiệm.
Phương pháp: Xác thực -> khoanh vùng -> thay đổi gần đây -> phân tích theo phụ thuộc -> giảm thiểu
Trước tiên, xác thực tín hiệu. Kiểm tra xem mức tăng độ trễ có xuất hiện trên nhiều hệ thống giám sát không, và nó ảnh hưởng đến p50, p95, p99 hay chỉ một endpoint cụ thể. Xác nhận người dùng có cảm nhận được hay không. Sau đó khoanh vùng vấn đề theo endpoint, khu vực, vùng khả dụng (availability zone), phân khúc khách hàng, phiên bản, host, dịch vụ phụ thuộc và loại request. Nếu độ trễ tăng trên toàn hệ thống, nguyên nhân nhiều khả năng là một phụ thuộc dùng chung hoặc một lần triển khai. Nếu chỉ tăng trong phạm vi hẹp, vấn đề nằm ở một route, truy vấn, khách hàng hoặc vùng hạ tầng cụ thể. Tiếp theo, rà soát các thay đổi gần đây: các lần triển khai, thay đổi cấu hình, migration cơ sở dữ liệu, lưu lượng tăng đột biến, feature flag, thay đổi cache hoặc sự cố ở dịch vụ phía sau. Dùng trace để phân tích độ trễ theo từng span của dịch vụ. Kiểm tra thời gian truy vấn cơ sở dữ liệu, tỷ lệ cache hit, độ trễ hàng đợi, CPU, bộ nhớ, thu gom rác (garbage collection), thread pool, connection pool và các API bên ngoài. Nếu người dùng đang bị ảnh hưởng, giảm thiểu tác động phải đi trước việc tìm ra nguyên nhân gốc rễ hoàn hảo. Rollback, tắt feature flag, tăng tài nguyên, đi vòng qua dịch vụ phụ thuộc đang chậm hoặc tăng TTL của cache nếu an toàn. Sau khi khôi phục, ghi lại nguyên nhân gốc rễ, lỗ hổng trong khâu phát hiện và kế hoạch phòng ngừa.
Câu hỏi nối tiếp thường gặp
Nếu chỉ có độ trễ p99 tăng thì sao?
Bạn quyết định có rollback hay không như thế nào?
Bạn muốn có những dashboard nào?
Phương pháp: Unit test -> giá trị biên -> đầu vào không hợp lệ -> bất biến
Tôi sẽ bắt đầu bằng việc làm rõ các quy tắc: loại giảm giá, cách cộng dồn nhiều mã, thời hạn, mức chi tiêu tối thiểu, điều kiện áp dụng cho người dùng, cách làm tròn tiền tệ, và giá đã bao gồm thuế hoặc phí vận chuyển hay chưa. Unit test cần bao quát trường hợp thông thường, trường hợp biên, đầu vào không hợp lệ và tương tác giữa các quy tắc. Ví dụ: không có giảm giá, giảm theo phần trăm, giảm số tiền cố định, mức giảm lớn hơn tạm tính, vừa đúng mức chi tiêu tối thiểu, thấp hơn ngưỡng tối thiểu một chút, mã giảm giá đã hết hạn, người dùng không đủ điều kiện, nhiều mã giảm giá nếu được phép, và làm tròn đến từng cent. Tôi cũng sẽ kiểm thử các bất biến: giá cuối cùng không bao giờ âm, mức giảm không vượt quá phần tạm tính đủ điều kiện, khuyến mãi hết hạn không được áp dụng, và tính lại nhiều lần phải cho cùng một kết quả. Nếu phần này liên quan đến thanh toán, tôi sẽ bổ sung integration test với dịch vụ định giá và snapshot test cho đầu ra hóa đơn. Để an toàn khi chạy production, tôi sẽ ghi log các quyết định giảm giá kèm mã lý do, để đội hỗ trợ và đội kỹ thuật có thể chẩn đoán khiếu nại về giá mà không phải đoán mò.
Câu hỏi nối tiếp thường gặp
Bạn sẽ kiểm thử việc làm tròn tiền tệ như thế nào?
Hệ thống nên xử lý ra sao nếu dịch vụ khuyến mãi ngừng hoạt động?
Những kiểm thử nào thuộc unit test, những kiểm thử nào thuộc integration test?
Phương pháp: Tính đúng đắn -> khả năng bảo trì -> rủi ro -> sự rõ ràng
Tôi review code theo từng lớp. Thứ nhất, code có giải quyết đúng vấn đề và giữ được tính đúng đắn không? Thứ hai, code có dễ bảo trì không: tên gọi rõ ràng, ranh giới hợp lý, không phức tạp thừa và phong cách nhất quán? Thứ ba, rủi ro nằm ở đâu: migration, xử lý đồng thời, bảo mật, hiệu năng, khả năng tương thích ngược và khả năng quan sát? Thứ tư, kiểm thử đã đủ cho phần hành vi thay đổi chưa? Review code tốt không phải để thể hiện mình giỏi hơn. Mục tiêu là cải thiện code mà vẫn giữ được tốc độ của cả nhóm. Tôi tách rõ vấn đề bắt buộc phải sửa (blocking) với góp ý. Lỗi về tính đúng đắn, lỗ hổng bảo mật hoặc rủi ro migration là blocking. Sở thích đặt tên hay một chỗ refactor nhỏ có thể là non-blocking. Tôi cũng tìm những chỗ thiếu ngữ cảnh: hành vi sản phẩm chưa rõ, trường hợp biên chưa được kiểm thử, lỗi bị nuốt im lặng, xử lý lỗi sơ sài và thiếu metric cho những thay đổi có rủi ro.
Câu hỏi nối tiếp thường gặp
Bạn xử lý bất đồng trong review code như thế nào?
Điều gì khiến một bình luận trở thành blocking?
Làm sao để không làm chậm cả nhóm?
Câu hỏi hành vi và cộng tác
Vòng hành vi dành cho kỹ sư tập trung vào tinh thần làm chủ (ownership), khả năng cộng tác, óc phán đoán kỹ thuật, cách xử lý tình huống mơ hồ và bài học từ sai lầm. Câu trả lời tốt luôn gắn với những tình huống mà yếu tố kỹ thuật thực sự quan trọng.
Phương pháp: Bối cảnh -> phương án -> đánh đổi -> quyết định -> kết quả
Chọn một quyết định có đánh đổi thực sự: tự xây hay mua giải pháp có sẵn, SQL hay NoSQL, monolith hay tách service, vá nhanh hay refactor sâu, tính nhất quán mạnh hay tính sẵn sàng. Bắt đầu bằng bối cảnh và các ràng buộc, rồi giải thích những phương án bạn đã cân nhắc. Câu trả lời tốt gọi tên sự đánh đổi một cách rõ ràng. Ví dụ: “Phương án A giúp chúng tôi ra mắt trong một tuần nhưng làm tăng gánh nặng vận hành. Phương án B mất ba tuần nhưng loại bỏ được một điểm nghẽn khi mở rộng.” Sau đó giải thích bằng chứng nào dẫn đến quyết định: lưu lượng truy cập, hạn chót của khách hàng, lịch sử sự cố, năng lực của nhóm hay roadmap dài hạn. Kết thúc bằng kết quả và bài học rút ra. Nếu quyết định chưa hoàn hảo, hãy nói bạn sẽ làm khác đi thế nào. Người phỏng vấn đánh giá cao óc phán đoán chín chắn hơn là việc tỏ ra mọi quyết định đều hiển nhiên.
Câu hỏi nối tiếp thường gặp
Ai đã không đồng ý với bạn?
Điều gì có thể khiến bạn thay đổi quyết định?
Bạn đo lường thế nào để biết quyết định đó có hiệu quả?
Phương pháp: Sự cố -> tác động -> ứng phó -> nguyên nhân gốc rễ -> phòng ngừa
Nếu có thể, hãy dùng một sự cố có thật. Bắt đầu bằng tác động đến người dùng: cái gì bị hỏng, ai bị ảnh hưởng và mức độ nghiêm trọng ra sao. Sau đó giải thích vai trò của bạn trong quá trình ứng phó: phát hiện, phân loại mức độ ưu tiên, rollback, giảm thiểu, trao đổi thông tin hay phân tích nguyên nhân gốc rễ. Câu trả lời tốt nhất cho thấy bạn bình tĩnh sắp xếp ưu tiên. Khi đang có sự cố, khôi phục dịch vụ quan trọng hơn chứng minh một giả thuyết. Hãy nêu cách bạn đã dùng log, metric, trace, feature flag, rollback hoặc kiểm tra các dịch vụ phụ thuộc. Sau khi đã giảm thiểu tác động, giải thích nguyên nhân gốc rễ và cách phòng ngừa. Phòng ngừa tốt có thể gồm kiểm thử tốt hơn, quy trình triển khai an toàn hơn, giám sát, tinh chỉnh cảnh báo, runbook, circuit breaker, backpressure hoặc cơ chế bảo vệ khi migration. Tránh đổ lỗi cho người khác. Những đội mạnh cải thiện hệ thống để cùng một sai lầm khó lặp lại hơn.
Câu hỏi nối tiếp thường gặp
Bạn đã trao đổi thông tin như thế nào trong lúc xảy ra sự cố?
Cảnh báo nào lẽ ra đã phát hiện sự cố sớm hơn?
Sau đó nhóm đã thay đổi những gì?
Phương pháp: Mục tiêu chung -> ràng buộc -> phương án -> đánh đổi -> quyết định
Tôi bắt đầu bằng việc thống nhất mục tiêu của người dùng và mục tiêu kinh doanh. Nhiều bất đồng thực ra không phải là kỹ thuật đối đầu với sản phẩm, mà xuất phát từ những giả định khác nhau. Tôi sẽ giải thích rõ ràng buộc kỹ thuật: độ phức tạp, rủi ro về độ tin cậy, tiến độ, khả năng bảo trì, bảo mật hoặc hiệu năng. Sau đó, tôi đưa ra các phương án thay vì chỉ nói không. Ví dụ: ra mắt một MVP nhỏ hơn, tạm thời dùng quy trình thủ công, ẩn tính năng sau feature flag, sắp xếp lại thứ tự các phần việc kỹ thuật, hoặc chọn một thiết kế đơn giản hơn mà vẫn giải quyết được vấn đề cốt lõi của người dùng. Nếu sự đánh đổi là nghiêm trọng, tôi ghi lại các phương án, rủi ro và đề xuất để quyết định được minh bạch. Mục tiêu không phải để phía kỹ thuật thắng. Mục tiêu là cả nhóm đưa ra một quyết định rõ ràng khi đã nắm đủ thông tin.
Câu hỏi nối tiếp thường gặp
Nếu phía sản phẩm vẫn khăng khăng chọn phương án rủi ro thì sao?
Bạn giải thích nợ kỹ thuật cho người không làm kỹ thuật như thế nào?
Khi nào bạn sẽ báo cáo lên cấp trên?
Luyện tập trực tiếp với các câu trả lời này
Interview Pilot gợi ý câu trả lời từ Copilot theo thời gian thực trong các buổi phỏng vấn trực tiếp, giúp bạn trả lời rõ ràng khi gặp những câu hỏi này.