Kinh nghiệm phỏng vấn tại công ty A*

- Published on
- /19 mins read/
Email mời làm bài test tới vào một tối thứ Ba: 5 câu hỏi, làm trong thời gian giới hạn, nộp code Java và tài liệu thiết kế. Đọc lướt đề thấy câu nào cũng quen. Đa luồng, sliding window, thiết kế 3 tầng, SDLC, OSI. Kiểu đề mà ai đi phỏng vấn backend vài năm cũng từng gặp ít nhất một lần.
Chính vì quen nên mới dễ mất điểm. Đề càng phổ biến thì người chấm càng có sẵn trong đầu một bản "đáp án trung bình", và thứ họ tìm là chỗ bạn làm tốt hơn mức đó.
Bài này mình kể lại bài test, đi qua từng câu với cách làm, code đã chạy thử, và những chỗ mình thấy hay bị trừ điểm. Phần sau tổng hợp các vòng phỏng vấn thường gặp tiếp theo ở các công ty fintech kiểu này, dựa trên chia sẻ công khai của ứng viên, kèm cách chuẩn bị.
Tên công ty mình để là A*, vì mục đích là chia sẻ cách làm bài chứ không phải review công ty.
# tổng quan bài test
Năm câu chia làm ba nhóm khá rõ:
| Câu | Nội dung | Đo cái gì |
|---|---|---|
| 1 | n chiếc xe xuất phát cùng lúc bằng đa luồng Java | Hiểu công cụ đồng bộ trong java.util.concurrent |
| 2 | Chuỗi con dài nhất không lặp ký tự | Thuật toán, độ phức tạp |
| 3 | Thiết kế API 3 tầng, mã hoá bất đối xứng, vẽ UML | Tư duy kiến trúc, bảo mật |
| 4 | Các pha SDLC và developer giỏi làm gì ở từng pha | Kinh nghiệm làm việc thật |
| 5 | Mô hình OSI theo cách hiểu của bạn | Nền tảng mạng |
Hai câu code ngắn, ba câu sau là viết. Theo mình thì câu 3 nặng ký nhất, vì nó là câu duy nhất không có đáp án chuẩn để chép.
# câu 1: n chiếc xe xuất phát cùng lúc
Đề: There are n racing cars, let them all be ready on the starting line, start at the same time, use Java multi-threading technology to simulate this kind of scene.
Bản nháp đầu tiên của nhiều người (kể cả mình) trông thế này:
for (int i = 1; i <= numberOfCars; i++) {
final int carId = i;
new Thread(() -> {
System.out.println("Car " + carId + " is ready at the starting line.");
Thread.sleep(1000); // "chờ súng"
System.out.println("Car " + carId + " is VROOMING!");
...
}).start();
}Chạy thì in ra đúng thứ tự trông rất giống, nhưng nó không giải quyết đề bài. Mỗi xe tự ngủ 1 giây tính từ lúc thread của nó được khởi động, mà các thread khởi động lệch nhau. Không có thứ gì đảm bảo "tất cả cùng xuất phát". Khác nào mỗi vận động viên tự đếm 3-2-1 trong đầu rồi chạy.
Đề hỏi đúng hai sự kiện đồng bộ: mọi xe đã vào vạch và súng nổ. java.util.concurrent có sẵn công cụ cho cả hai:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class RaceStart {
public static void main(String[] args) throws InterruptedException {
int cars = 5;
CountDownLatch ready = new CountDownLatch(cars); // mỗi xe báo "đã vào vạch"
CountDownLatch go = new CountDownLatch(1); // tiếng súng xuất phát
CountDownLatch finish = new CountDownLatch(cars); // chờ cả đoàn về đích
try (ExecutorService pool = Executors.newFixedThreadPool(cars)) {
for (int i = 1; i <= cars; i++) {
int id = i;
pool.submit(() -> {
try {
System.out.println("Car " + id + " ready");
ready.countDown();
go.await();
long start = System.nanoTime();
Thread.sleep(ThreadLocalRandom.current().nextInt(500, 2000));
System.out.printf("Car %d finished in %d ms%n", id, (System.nanoTime() - start) / 1_000_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
finish.countDown();
}
});
}
ready.await(); // chắc chắn cả 5 xe đã vào vạch
System.out.println("GO!");
go.countDown();
if (!finish.await(10, TimeUnit.SECONDS)) System.out.println("Timeout");
}
}
}Chạy thử trên Java 21:
Car 1 ready
Car 4 ready
Car 2 ready
Car 3 ready
Car 5 ready
GO!
Car 1 finished in 1032 ms
Car 3 finished in 1170 ms
Car 5 finished in 1376 ms
Car 2 finished in 1439 ms
Car 4 finished in 1834 msMấy chỗ người chấm hay để ý:
Ba latch, ba vai trò. ready đếm ngược từ n, main thread chờ nó về 0 mới bắn súng. Nhiều bài làm chỉ có latch go rồi để main Thread.sleep(1000) "cho các xe kịp vào vạch". Đó lại là một phỏng đoán về thời gian, máy chậm hay pool bận là sai. finish để main biết lúc nào cuộc đua kết thúc, có timeout để không treo vĩnh viễn nếu một xe "chết máy".
countDown() nằm trong finally. Xe có văng exception thì vạch đích vẫn được đếm, không thì main chờ mãi.
Xử lý InterruptedException cho đúng. Không nuốt im, gọi lại Thread.currentThread().interrupt() để giữ trạng thái ngắt cho tầng trên.
Dùng ExecutorService thay vì new Thread(). Từ Java 19, ExecutorService là AutoCloseable, try-with-resources sẽ tự shutdown và chờ task xong. Nếu muốn ghi điểm thêm thì nhắc tới virtual thread: Executors.newVirtualThreadPerTaskExecutor() với 10.000 chiếc xe vẫn nhẹ nhàng, còn 10.000 platform thread thì không.
Biết các lựa chọn khác và vì sao không chọn. CyclicBarrier(n) cũng làm được: mỗi xe gọi await(), xe thứ n tới là cả đoàn cùng đi. Điểm khác là barrier dùng lại được cho nhiều vòng đua, còn latch chỉ dùng một lần. Phaser linh hoạt hơn nữa khi số xe thay đổi giữa chừng. Đề chỉ có một lần xuất phát nên latch là gọn nhất, nói được điều này là thấy bạn chọn có lý do.
Thêm một ý nếu còn thời gian: "cùng lúc" trong đa luồng chỉ là tương đối. go.countDown() đánh thức các thread chờ, nhưng thread nào chạy trước vẫn do scheduler của hệ điều hành quyết định. Nói được giới hạn này cho thấy bạn hiểu chứ không chỉ thuộc API.
# câu 2: chuỗi con dài nhất không lặp ký tự
Đề: tìm chuỗi con liên tiếp dài nhất không có ký tự lặp. Ví dụ java2novice cho ra a2novice, java_language_is_sweet cho ra uage_is.
Đây là bài LeetCode 3 quen thuộc, có một điểm khác nhỏ mà dễ bỏ qua: đề hỏi chính chuỗi con, không phải độ dài. Nên ngoài độ dài lớn nhất còn phải nhớ vị trí bắt đầu.
Ý tưởng là cửa sổ trượt. Hai con trỏ start, end giữ một cửa sổ không có ký tự lặp. Mỗi bước mở rộng end; nếu ký tự mới đã xuất hiện bên trong cửa sổ thì nhảy start tới ngay sau lần xuất hiện trước đó.
public static String findLongestSubstring(String s) {
if (s == null || s.isEmpty()) {
return "";
}
// Map to store the character and its most recent index
HashMap<Character, Integer> map = new HashMap<>();
int maxLength = 0;
int maxStart = 0;
int start = 0;
for (int end = 0; end < s.length(); end++) {
char currentChar = s.charAt(end);
// If the character is already in the map, move the start pointer
if (map.containsKey(currentChar)) {
// start only moves forward; it never moves backward
start = Math.max(start, map.get(currentChar) + 1);
}
// Update/insert the character's newest index
map.put(currentChar, end);
// Check if the current window is the longest we've seen
if (end - start + 1 > maxLength) {
maxLength = end - start + 1;
maxStart = start;
}
}
// Extract and return the longest substring using the tracked indices
return s.substring(maxStart, maxStart + maxLength);
}Input: java2novice -> Output: a2novice
Input: java_language_is_sweet -> Output: uage_isCái bẫy chính là Math.max. Map lưu vị trí gần nhất của mọi ký tự từng gặp, kể cả những ký tự đã nằm ngoài cửa sổ. Thử abba: tới chữ a cuối, map nói a ở vị trí 0, nếu gán thẳng start = 0 + 1 = 1 thì start lùi từ 2 về 1, cửa sổ thành bba có lặp. Math.max đảm bảo start chỉ tiến, không bao giờ lùi.
Đừng tạo chuỗi trong vòng lặp. Một biến thể hay gặp là res = s.substring(start, end + 1) mỗi lần tìm được cửa sổ dài hơn. Vẫn đúng, nhưng mỗi lần là một lần copy chuỗi. Lưu maxStart và maxLength rồi cắt một lần cuối cùng thì sạch hơn. Tổng thời gian O(n), bộ nhớ O(k) với k là số ký tự khác nhau. Nếu chắc chắn chỉ có ASCII thì thay HashMap bằng int[128] cho nhanh hơn và khỏi boxing.
Nếu có thời gian, viết luôn vài test: chuỗi rỗng, một ký tự, toàn ký tự giống nhau (aaaa ra a), không ký tự nào lặp, và ca abba. Người chấm thấy bạn tự nghĩ ra ca biên thì điểm khác hẳn.
Câu này trên site mô phỏng thuật toán của mình có hoạt ảnh từng bước, bạn sửa input rồi xem cửa sổ trượt thế nào.
# câu 3: API 3 tầng với mã hoá bất đối xứng
Đề yêu cầu thiết kế một ứng dụng web cung cấp API (ví dụ truy vấn thông tin người dùng), với năm điều kiện: mô hình 3 tầng và triển khai trên 3 tầng; có cả thiết kế logic lẫn vật lý (deployment diagram); client gọi qua HTTP và API được bảo vệ bằng mã hoá bất đối xứng; tự chọn framework, database, middleware; mọi sơ đồ vẽ bằng UML.
Đây là câu ăn điểm hay mất điểm nhiều nhất. Mình chia câu trả lời thành bốn phần.
Kiến trúc logic. Ba tầng rõ ràng: Presentation (reverse proxy / API gateway nhận request, rate limit, định tuyến), Application (Spring Boot theo Controller, Service, Repository, thêm một thành phần Crypto riêng), Data (PostgreSQL, thêm Redis làm cache). Điểm cần nói rõ là mỗi tầng chỉ nói chuyện với tầng kề nó, presentation không bao giờ chạm thẳng database.
Kiến trúc vật lý. Đề nhấn mạnh "deploy trên 3 tầng", nghĩa là tách ở mức mạng chứ không chỉ tách package trong code:
Firewall hay security group chỉ cho tầng 1 gọi tầng 2 ở đúng port ứng dụng, tầng 2 gọi tầng 3 ở đúng port database. Database không có đường ra Internet. Vẽ phần này bằng UML deployment diagram (node, artifact, communication path) chứ không phải hình khối tự do, vì đề ghi rõ là UML.
Mã hoá bất đối xứng: chỗ phân loại ứng viên. Câu trả lời phổ thông là: client mã hoá payload bằng public key của server, server giải mã bằng private key; chiều về thì ngược lại với cặp khoá của client. Ý đúng, nhưng thiếu một chi tiết kỹ thuật quan trọng: RSA không mã hoá được payload dài. Với khoá RSA 2048 bit và padding OAEP/SHA-256, mỗi lần mã hoá tối đa 256 - 2×32 - 2 = 190 byte. Một JSON thông tin người dùng là vượt rồi. Thêm nữa, RSA chậm hơn mã hoá đối xứng cả trăm lần.
Cách làm đúng là mã hoá lai (hybrid), giống hệt cách TLS và chuẩn JWE làm: sinh một khoá AES ngẫu nhiên cho mỗi request, mã hoá payload bằng AES-GCM, rồi dùng RSA chỉ để bọc cái khoá AES đó.
// Client: sinh khoá AES ngẫu nhiên, mã hoá payload bằng AES-GCM, bọc khoá AES bằng RSA public key của server
byte[] payload = "{\"userId\":42,\"fields\":[\"name\",\"email\",\"phone\"]}".repeat(20).getBytes(StandardCharsets.UTF_8);
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256);
SecretKey aes = kg.generateKey();
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
Cipher gcm = Cipher.getInstance("AES/GCM/NoPadding");
gcm.init(Cipher.ENCRYPT_MODE, aes, new GCMParameterSpec(128, iv));
byte[] body = gcm.doFinal(payload);
rsa.init(Cipher.ENCRYPT_MODE, server.getPublic(), OAEP);
byte[] wrappedKey = rsa.doFinal(aes.getEncoded());
// Server: mở khoá AES bằng private key, giải mã payload
rsa.init(Cipher.DECRYPT_MODE, server.getPrivate(), OAEP);
SecretKey opened = new SecretKeySpec(rsa.doFinal(wrappedKey), "AES");
gcm.init(Cipher.DECRYPT_MODE, opened, new GCMParameterSpec(128, iv));
byte[] plain = gcm.doFinal(body);Chạy thử, kèm một lần cố mã hoá thẳng 191 byte bằng RSA để thấy giới hạn:
191 bytes with RSA-OAEP: IllegalBlockSizeException
payload 940 bytes, wrapped key 256 bytes, round trip ok = trueMấy ý nên nói thêm trong tài liệu thiết kế:
- Mã hoá không thay được xác thực. Ai cũng có public key của server, nên ai cũng gửi được payload mã hoá hợp lệ. Muốn biết request đến từ client nào thì client phải ký request bằng private key của nó (JWS), server kiểm tra bằng public key của client đã đăng ký. Mã hoá giữ bí mật, chữ ký chứng minh danh tính và toàn vẹn. Hai việc khác nhau.
- Chống replay. Kẻ nghe lén không đọc được payload nhưng vẫn gửi lại nguyên gói được. Thêm timestamp và nonce vào phần được ký, server từ chối request quá cũ hoặc nonce đã dùng.
- HTTPS vẫn bắt buộc. Đề ghi "over HTTP", nhưng theo mình nên nói rõ là vẫn chạy TLS bên dưới, mã hoá payload là lớp bảo vệ thêm (end-to-end tới tận tầng ứng dụng, kể cả khi TLS bị kết thúc ở load balancer). Nếu hai bên là hệ thống với hệ thống thì mTLS là lựa chọn gọn hơn nhiều.
- Quản lý khoá. Private key của server nằm trong HSM hoặc KMS, không nằm trong file
application.yml. Có kế hoạch xoay khoá, và giữ nhiều khoá cùng lúc (định danh bằngkid) để xoay không làm gián đoạn client.
UML. Tối thiểu nên có: component diagram (kiến trúc logic), deployment diagram (vật lý), sequence diagram cho một request hoàn chỉnh từ client ký và mã hoá, qua gateway, tới service giải mã, query, mã hoá kết quả trả về. Sequence diagram là chỗ người chấm nhìn ra bạn có hiểu thứ tự các bước hay không.
# câu 4: SDLC và developer giỏi làm gì ở từng pha
Câu này không có đúng sai về lý thuyết. Ai cũng liệt kê được các pha: lập kế hoạch, phân tích yêu cầu, thiết kế, phát triển, kiểm thử, triển khai, vận hành và bảo trì. Thứ người chấm muốn thấy là phần sau của câu hỏi: một developer giỏi làm gì ở từng pha, và câu trả lời có mùi của người đã đi làm thật hay không.
Theo mình nên trả lời theo kiểu hành động cụ thể:
- Phân tích yêu cầu: hỏi cho ra các ca biên và yêu cầu phi chức năng ("bao nhiêu người dùng đồng thời?", "dữ liệu giữ bao lâu?", "lỗi thì sao?"). Developer giỏi phát hiện mâu thuẫn trong yêu cầu trước khi viết dòng code nào.
- Thiết kế: viết tài liệu thiết kế ngắn có trade-off và phương án bị loại, ước lượng tải, xác định điểm có thể hỏng. Review thiết kế với người khác.
- Phát triển: code đọc được, test đi kèm code, commit nhỏ, review code của người khác nghiêm túc như code của mình.
- Kiểm thử: không coi test là việc của QA. Viết unit test và integration test, tham gia viết kịch bản test cho ca biên.
- Triển khai: có kế hoạch rollback, feature flag cho thay đổi rủi ro, theo dõi metric ngay sau khi deploy.
- Vận hành: trực sự cố, viết postmortem không đổ lỗi, biến sự cố thành việc cần làm (thêm alert, thêm test).
Nhắc thêm rằng SDLC ngoài đời ít khi đi tuần tự kiểu waterfall. Với Agile/Scrum, các pha lặp lại trong mỗi sprint vài tuần, và DevOps kéo vận hành sát lại với phát triển. Một hai câu về chuyện này cho thấy bạn không chỉ học thuộc giáo trình.
# câu 5: mô hình OSI theo cách hiểu của bạn
Lại một câu dễ chép. Chép bảng 7 tầng thì ai cũng làm được, nên mình thấy nên trả lời theo kiểu: mỗi tầng giải quyết vấn đề gì, và một backend developer gặp tầng đó ở đâu trong công việc.
| Tầng | Giải quyết vấn đề gì | Backend gặp ở đâu |
|---|---|---|
| 7 Application | Ứng dụng nói chuyện với nhau theo giao thức nào | HTTP, gRPC, DNS; load balancer L7 định tuyến theo path, header |
| 6 Presentation | Biểu diễn dữ liệu: mã hoá ký tự, nén, mã hoá | JSON/Protobuf, gzip; TLS thường được xếp quanh tầng này |
| 5 Session | Duy trì phiên giữa hai bên | Phiên TLS, resume session; trong thực tế hay bị gộp vào tầng 7 |
| 4 Transport | Truyền tin cậy (hay không) giữa hai tiến trình | TCP và UDP, port, connection pool, timeout, load balancer L4 |
| 3 Network | Định tuyến gói tin giữa các mạng | IP, subnet, route table, security group, VPN |
| 2 Data Link | Truyền frame trong cùng một mạng | MAC, VLAN, switch |
| 1 Physical | Truyền bit trên môi trường vật lý | Cáp quang, Wi-Fi; với backend thì gần như là "của đội hạ tầng" |
Ý quan trọng nên nói: OSI là mô hình để tư duy, còn mạng thật chạy theo TCP/IP với 4 (hoặc 5) tầng. Giá trị của OSI là khi debug: "không kết nối được" là tầng nào? DNS sai (tầng 7)? Port bị chặn (tầng 4)? Route sai (tầng 3)? Đi từ dưới lên hay từ trên xuống một cách có hệ thống thì tìm lỗi nhanh hơn rất nhiều so với đoán mò. Kể một ví dụ debug cụ thể kiểu "telnet host 5432 được nhưng app vẫn không kết nối, vậy tầng 4 ổn, lỗi ở tầng trên, kiểm tra TLS và credential" là câu trả lời được điểm cao hơn hẳn bảng chép.
# những vòng phía sau: tổng hợp từ chia sẻ công khai
Phần này không phải trải nghiệm của mình, mà là tổng hợp từ các chia sẻ công khai của ứng viên (Glassdoor và các trang tổng hợp đề phỏng vấn) về quy trình ở công ty này và các công ty fintech cùng khu vực. Quy trình mỗi nơi, mỗi vị trí, mỗi năm một khác, nên coi đây là bức tranh tham khảo chứ không phải lịch trình cố định.
Theo những chia sẻ đó, sau vòng test (hoặc thay cho vòng test) thường có ba tới bốn vòng, kéo dài từ hai tới năm tuần:
Vòng kỹ thuật 1, khoảng một tiếng, với một kỹ sư cùng cấp. Thường gồm live coding một bài cỡ LeetCode dễ tới trung bình, và hỏi sâu về dự án gần nhất. Live coding khác làm bài test ở chỗ người phỏng vấn nhìn bạn nghĩ. Nói to suy nghĩ của mình, hỏi lại đề trước khi code, chạy tay một ví dụ, nêu độ phức tạp, và tự đề xuất ca biên. Nếu đã làm bài test, rất có thể họ hỏi lại chính bài test: "vì sao chọn CountDownLatch?", "nếu số xe là một triệu thì sao?", "RSA giới hạn payload bao nhiêu?". Hiểu bài mình nộp là điều tối thiểu.
Vòng kỹ thuật 2 với team lead hoặc kỹ sư senior. Ít code hơn, nhiều khái niệm hơn. Các chủ đề hay được nhắc tới cho vị trí Java backend: JVM (bộ nhớ heap, GC, cách đọc một bản heap dump), concurrency (synchronized khác ReentrantLock thế nào, volatile đảm bảo gì và không đảm bảo gì, ConcurrentHashMap hoạt động ra sao), Spring (vòng đời bean, @Transactional hoạt động qua proxy nên gọi nội bộ trong cùng class sẽ không có transaction), database (index, isolation level, deadlock), và một bài thiết kế hệ thống cỡ vừa. Với fintech, câu hỏi về tính nhất quán dữ liệu (idempotency, tránh trừ tiền hai lần, xử lý khi gọi hệ thống bên ngoài bị timeout) xuất hiện rất thường xuyên.
Vòng hành vi và HR. Kể về một xung đột trong team, một quyết định kỹ thuật sai và bạn xử lý thế nào, vì sao muốn chuyển việc. Chuẩn bị sẵn 4–5 câu chuyện thật theo cấu trúc STAR (tình huống, nhiệm vụ, hành động, kết quả). Câu chuyện thật có chi tiết, có số liệu thì thuyết phục hơn câu chuyện chung chung rất nhiều.
Một số công ty có thêm vòng với quản lý cấp cao hơn hoặc với người ở văn phòng nước ngoài, thường bằng tiếng Anh. Nếu công ty có văn phòng chính ở nước ngoài thì khả năng giao tiếp tiếng Anh kỹ thuật gần như chắc chắn được đánh giá trong mọi vòng.
# chuẩn bị thế nào
Nếu bạn sắp làm một bài test kiểu này, đây là những việc theo mình đáng làm nhất:
- Làm bài như thể code sẽ lên production. Có xử lý lỗi, có test, có README ngắn giải thích cách chạy và các quyết định. Bài test là sản phẩm đầu tiên công ty thấy từ bạn.
- Viết ra trade-off. Chọn
CountDownLatchthay vìCyclicBarriervì sao, chọn hybrid encryption thay vì RSA thuần vì sao. Người chấm chấm cách bạn nghĩ nhiều hơn chấm việc bạn ra đáp án. - Ôn lại nền tảng đúng chỗ. Với Java backend:
java.util.concurrent, JVM memory và GC, Spring transaction, SQL và index. Mấy chủ đề đó xuất hiện ở gần như mọi vòng kỹ thuật. - Luyện live coding có người nhìn. Giải 50 bài một mình không giống giải 1 bài trước mặt người lạ. Nhờ bạn bè đóng vai người phỏng vấn vài buổi.
- Hiểu sâu những gì mình đã nộp. Vòng sau gần như chắc chắn hỏi lại.
Bộ đề và lời giải đầy đủ của 384 bài LeetCode mình luyện, kèm mô phỏng từng bước cho các bài kinh điển, có ở algorithm.tungdadev.com. Chúc bạn phỏng vấn thuận lợi.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # tổng quan bài test
- # câu 1: n chiếc xe xuất phát cùng lúc
- # câu 2: chuỗi con dài nhất không lặp ký tự
- # câu 3: API 3 tầng với mã hoá bất đối xứng
- # câu 4: SDLC và developer giỏi làm gì ở từng pha
- # câu 5: mô hình OSI theo cách hiểu của bạn
- # những vòng phía sau: tổng hợp từ chia sẻ công khai
- # chuẩn bị thế nào