0

Vì sao nhiều dev code 5 năm vẫn không lên senior?


Năm 2020, mình phỏng vấn một bạn apply vào vị trí Senior Backend. CV đẹp, 5 năm kinh nghiệm, stack Java Spring Boot + Kafka + Redis + Docker. Lý lịch sạch, không gap, công ty trước cũng ổn.

Mình hỏi câu đầu tiên: "Bạn đang dùng Kafka — consumer group của bạn có bao nhiêu partition, và bạn xử lý duplicate message như thế nào?"

Bạn ấy im. Khoảng 8 giây. Rồi nói: "Thường thì em chỉ consume rồi xử lý thôi anh, chưa gặp duplicate."

Mình không hỏi thêm về Kafka nữa. Chuyển sang câu khác: "Bạn kể cho mình nghe một production incident bạn từng xử lý — từ lúc phát hiện đến lúc root cause?"

"Dạ, hồi đó service bị chậm, em restart lại thì hết ạ."

"Sau đó có tìm nguyên nhân không?"

"Dạ... không, vì nó hết rồi ạ."


Mình không kể chuyện này để chê bạn ấy. Mình kể vì mình đã nghe câu này, hoặc câu tương tự, không dưới 50 lần trong đời. Và mỗi lần nghe, mình đều cảm thấy một sự tiếc nuối kỳ lạ — không phải vì bạn ấy "dở", mà vì rõ ràng đây là người chăm chỉ, đi làm đều, không có vấn đề gì nghiêm trọng — nhưng đã lãng phí 5 năm theo một cách rất cụ thể mà họ không nhận ra.

Thực ra feedback của công ty Singapore từ chối mình hồi năm thứ 4 cũng tương tự: "Strong in execution, lacks ownership and system thinking." Lúc đó mình đọc, nghĩ "ý họ là gì?", rồi để đó. Mất thêm 2 năm nữa mình mới thực sự hiểu họ đang nói cái gì.

Bài này mình viết cho người đang ở năm thứ 3, 4, 5 và cảm thấy mình không đi đến đâu. Không phải để động viên sáo rỗng, mà để nói thẳng những thứ mà trong môi trường công ty ít ai nói với bạn — vì manager bận, vì senior không muốn đụng chạm, vì văn hoá "khen nhiều hơn góp ý".

Đọc hết. Một số chỗ sẽ khó nghe. Nhưng khó nghe mà đúng thì vẫn tốt hơn dễ nghe mà vô nghĩa.


Phần 1: 5 năm không phải là số đo của bất cứ thứ gì

Ngành này có một thói quen kỳ lạ: dùng số năm kinh nghiệm như một proxy cho năng lực. Job description viết "5+ years experience", offer letter ghi "Senior Engineer với 5 năm trở lên"... như thể có một cái phép màu nào đó sẽ xảy ra sau đúng 1825 ngày đi làm.

Thực tế là không có.

Mình đã làm việc với những người 7 năm kinh nghiệm mà mình phải giải thích cho họ tại sao không nên để business logic trong stored procedure. Và mình cũng đã làm việc với một bạn 2.5 năm kinh nghiệm mà có thể ngồi design một caching strategy hợp lý cho hệ thống high-read từ đầu mà không cần ai dắt tay.

Cái tạo ra sự khác biệt không phải là thời gian. Mà là chất lượng của sự chú ý bạn đặt vào công việc trong thời gian đó.

Có một cái framework mình dùng để phân loại — không hoàn hảo nhưng khá gần với thực tế:

Junior Mid-level Senior
Nghĩ ở tầm Task Feature Hệ thống
Khi gặp vấn đề Hỏi Tự tìm Đã phòng trước
Với production bug Sợ Xử lý Học và ngăn tái diễn
Ownership Không có Một phần Toàn bộ flow
Giao tiếp Code-centric Mixed Business + Technical

Cái bẫy của mid-level là: họ perform tốt trong context được giao, nhưng không bao giờ tự đặt câu hỏi về context đó. Họ làm task rất tốt — nhưng không hỏi tại sao task này tồn tại, nó giải quyết vấn đề gì, và nếu làm khác đi thì sao.

Và cái bẫy đó, cộng với môi trường không có ai push họ ra ngoài nó, có thể giữ một người ở mid-level indefinitely.


Phần 2: 7 lý do cụ thể — và mình sẽ không viết nhẹ tay

Lý do #1: Chỉ code, không nghĩ hệ thống

Câu chuyện này xảy ra ở một startup mình tham gia hồi 2019. Team 6 người, hệ thống e-commerce nhỏ, traffic vài nghìn user mỗi ngày. Không lớn.

PM yêu cầu thêm tính năng gửi email xác nhận đơn hàng. Bạn Tuấn — mid-level 4 năm, rất chăm, không có vấn đề gì về attitude — nhận task. Implement trong 2 ngày, test với vài case, PR được merge.

Một tuần sau order API bắt đầu lag. Thời gian response từ 150ms lên 2-3 giây. QA report. Mọi người nhìn nhau. Không ai nghĩ ngay đến email.

Sau khoảng 1.5 tiếng trace, mình tìm ra: Tuấn gọi email service synchronously trong main transaction của order flow. Email service của họ dùng một SMTP provider free, timeout mặc định 30 giây, và lúc load tăng thì provider đó bắt đầu chậm. Kết quả là mỗi order request block cả thread trong khi chờ email.

Tuấn không sai về mặt "implement đúng yêu cầu". Email được gửi. Feature hoạt động. Nhưng anh ấy không nghĩ đến cái email đó sẽ làm gì với order flow khi nó chậm.

Senior engineer nhìn vào task gửi email sẽ hỏi ngay:

  • Cái này có cần synchronous không? Nếu email gửi trễ 30 giây thì user care không?
  • Nếu email service down thì order có nên fail không? Hay order vẫn success và email retry sau?
  • SMTP timeout mặc định là bao nhiêu? Có set timeout explicit không?
  • Volume email tăng theo order thì có rate limit gì không?

Không phải senior nghĩ phức tạp hơn. Mà là senior đã từng thấy cái gì đó nhỏ kéo cả hệ thống xuống đủ nhiều lần, nên phản xạ của họ khác.

Triệu chứng bạn đang trong bẫy này:

Bạn có thể implement bất kỳ feature nào được giao — nhưng nếu ai hỏi "service này depend vào những gì? và nếu những cái đó chết thì service của mình behave thế nào?", bạn mơ hồ. Bạn chưa bao giờ vẽ dependency graph của hệ thống mình đang làm. Bạn không biết SLA của third-party services mình đang tích hợp. Bạn chưa bao giờ tự hỏi "cái gì xảy ra nếu message queue đầy?" hoặc "nếu Redis down thì fallback là gì?"

Cái khó của lý do này: Nó bị ẩn sau sự chăm chỉ. Bạn đang làm việc. Bạn không lười. Bạn deliver đúng hạn. Mọi thứ nhìn vào đều ổn — cho đến khi production bị ảnh hưởng và bạn không biết bắt đầu nhìn từ đâu.

Cách tập system thinking không cần học thêm khóa gì:

Mỗi khi nhận một task, trước khi mở IDE, hãy vẽ — bằng tay, trên giấy, không cần đẹp — luồng dữ liệu đi qua những gì. Service nào gọi service nào. Database nào liên quan. External API nào. Và với mỗi arrow trong diagram đó, hỏi: nếu cái này fail hoặc chậm, điều gì xảy ra với toàn bộ flow?

Làm vậy mỗi ngày trong 3 tháng. Bạn sẽ ngạc nhiên.

Ngoài ra, có một bài tập mình hay yêu cầu các bạn mid-level làm: vẽ lại architecture diagram của hệ thống mình đang làm từ trí nhớ, không nhìn code hay docs. Hầu hết đều bỏ sót 30-40% các thành phần. Không phải vì họ không thông minh. Mà vì họ chưa bao giờ bắt bản thân nhìn toàn bộ bức tranh cùng một lúc.


Lý do #2: Tránh trách nhiệm và thiếu ownership

Mình phân biệt rõ giữa hai loại developer:

Loại một: Khi hệ thống có vấn đề ngoài scope của mình, họ notify đúng người rồi... tiếp tục làm việc của mình.

Loại hai: Khi hệ thống có vấn đề ngoài scope của mình, họ vẫn nhảy vào — không phải để "hero", mà vì họ cảm thấy có trách nhiệm với sản phẩm chung.

Cả hai đều không "sai" về mặt process. Nhưng loại hai mới là người được trust với những thứ quan trọng hơn.

Câu chuyện: Năm 2021, một startup mình biết có production incident lúc 11 giờ đêm. Payment flow bị lỗi — transaction fail nhưng không có error message rõ ràng, user nhận thông báo thất bại nhưng tiền đã bị trừ. Classic nightmare scenario.

CTO nhắn vào group. Có 3 developer online.

Bạn Hùng — phụ trách user service, không liên quan đến payment — đọc tin, nhận ra không phải module mình, tiếp tục làm việc.

Bạn Phong — người phụ trách payment — đang nhậu, điện thoại để trong túi áo khoác.

Bạn Long — phụ trách reporting, cũng không liên quan payment — nhảy vào ngay. Anh ấy không biết payment code, nhưng biết cách đọc log. Anh lọc log trong 15 phút, tìm ra pattern: error bắt đầu xuất hiện sau một deployment 3 tiếng trước, và error message từ payment gateway là DUPLICATE_TRANSACTION_ID. Anh post đầy đủ context lên group, tag CTO và Phong, đề xuất rollback deployment trước.

30 phút sau vấn đề được giải quyết.

Long không fix được vấn đề một mình. Nhưng Long đã reduce mean time to resolution từ có thể vài tiếng xuống còn 30 phút bằng cách làm một việc đơn giản: cảm thấy có trách nhiệm dù đó không phải module của mình.

Ownership có 4 cấp độ:

Cấp 1 — Task: Làm xong đúng spec đúng hạn. Hầu hết mid-level làm được.

Cấp 2 — Feature: Không chỉ implement, mà còn biết feature đó đang perform thế nào sau khi deploy. Tự track xem user có dùng không, có lỗi gì không. Proactively report nếu có vấn đề.

Cấp 3 — System: Quan tâm đến sức khỏe chung của hệ thống, không chỉ phần mình phụ trách. Biết khi nào cần alert, khi nào cần refactor, khi nào nên raise concern.

Cấp 4 — Business: Hiểu technical decision ảnh hưởng đến business như thế nào. Có thể nói chuyện về technical tradeoff bằng ngôn ngữ mà product manager và CEO hiểu được.

Mid-level hầu hết ở cấp 1, đôi khi cấp 2. Senior phải consistently ở cấp 3, và khi cần thì cấp 4.

Biểu hiện cụ thể của thiếu ownership:

"Cái đó không phải bug của mình, mình implement đúng spec rồi." — Câu này mình ghét nhất. Spec đúng nhưng behavior sai thì vẫn là vấn đề. User không đọc spec của bạn.

"Anh assign gì mình làm nấy." — Attitude này tốt cho outsourcing contract, không tốt cho người muốn grow.

Merge PR xong, không bao giờ nhìn lại feature đó trong production. Không xem log có lỗi gì không. Không check metric có gì bất thường không.

Một cách build ownership rất đơn giản: Sau mỗi lần deploy feature, đặt reminder 3 ngày sau để vào xem log và metric. 15 phút thôi. Không cần dashboard fancy. Chỉ cần habit đó — thói quen "nhìn lại" sau khi làm — sẽ dần thay đổi cách bạn viết code ngay từ đầu, vì bạn biết mình sẽ phải nhìn lại nó.

Và một điều cần nói rõ: ownership không có nghĩa là ôm hết mọi thứ về mình rồi burn out. Người có ownership biết khi nào cần escalate, khi nào cần gọi người khác, khi nào cần nói thẳng "cái này cần thêm resource". Đó cũng là một phần của trách nhiệm.


Lý do #3: Tutorial hell — học nhiều nhưng không đủ sâu để dùng được

Hồi năm thứ 3 đi làm, mình học rất nhiều. Udemy, Coursera, sách, blog. Xong AWS Solutions Architect Associate, làm thêm Docker + Kubernetes trên Udemy, đọc thêm "Clean Code", theo dõi Martin Fowler. Cảm giác đang tiến bộ mỗi ngày.

Rồi có lần junior trong team hỏi mình: "Anh ơi, tại sao ở đây lại dùng message queue thay vì gọi trực tiếp?"

Mình giải thích được — decoupling, resilience, async processing. Textbook.

Bạn ấy hỏi tiếp: "Vậy khi nào thì mình biết nên dùng cái nào? Có tiêu chí gì không?"

Mình ú ớ. Không phải vì mình không biết câu trả lời lý thuyết — mà vì mình chưa bao giờ phải quyết định cái đó trong một context thực tế với đủ ràng buộc: latency requirement, team familiarity, operational complexity, cost. Mình biết Kafka là gì nhưng chưa từng xử lý consumer rebalancing khi thêm partition. Biết Redis là gì nhưng chưa từng phải quyết định eviction policy khi memory đầy và chọn sai có thể làm mất session của user.

Đó là tutorial hell.

Tutorial hell không phải là học nhiều. Là học rộng nhưng không bao giờ đủ sâu để xử lý được vấn đề thực.

Dấu hiệu cụ thể:

Bạn có thể giải thích Kafka là gì — producer, consumer, partition, offset. Nhưng nếu hỏi: "Bạn đang dùng at-least-once delivery, vậy bạn đảm bảo idempotency ở consumer như thế nào?" — bạn blank.

Và đây là cái gap quan trọng: tất cả Kafka consumer trong thực tế đều phải đối mặt với duplicate message. Rebalancing xảy ra — consumer crash giữa chừng, commit offset chưa xong, consumer khác nhận lại message đó và process lại từ đầu. Exactly-once là một tính năng của Kafka mới và phức tạp — hầu hết system vẫn đang dùng at-least-once. Nếu bạn chưa bao giờ nghĩ đến idempotency key, chưa bao giờ dùng database unique constraint hoặc Redis SETNX để deduplicate, chưa bao giờ implement transactional outbox pattern để đảm bảo message chỉ được publish sau khi database transaction commit thành công — thì bạn đang dùng Kafka nhưng chưa hiểu những rủi ro thực tế của nó.

Bạn biết Redis có nhiều data structure. Nhưng chưa bao giờ phải chọn giữa Sorted Set và List cho một leaderboard usecase và giải thích tradeoff.

Bạn đã học Docker. Nhưng khi service bị OOMKilled lúc 3 giờ sáng, bạn không biết cách đọc dmesg hay check container memory limit để xác nhận hypothesis.

Bạn đọc về clean architecture. Nhưng codebase bạn đang làm vẫn có business logic trong controller và database query trong service layer vì "refactor thì sợ break".

Tại sao tutorial hell nguy hiểm:

Nó cho false confidence. Bạn có thể nói về công nghệ trong phỏng vấn. Bạn có thể list chúng trong CV. Nhưng khi gặp vấn đề thực tế — khi consumer lag tăng đột biến, khi cache hit rate drop, khi memory leak chậm trong 3 ngày — bạn không có đủ depth để debug.

Hơn nữa nó lãng phí thời gian kinh khủng. 3 tháng lướt qua 10 tutorial không bằng 3 tháng đi thật sâu vào 1-2 thứ.

Cách học có depth thực sự:

Với bất kỳ technology nào, mình có một checklist đơn giản trước khi coi là "đã biết":

  1. Mình có thể giải thích nó hoạt động thế nào ở mức implementation không — không phải chỉ mức API?
  2. Mình đã từng làm nó fail chưa — cố tình inject lỗi, tạo tải cao, kill dependency?
  3. Mình có thể troubleshoot nó trong production khi không có IDE không?
  4. Mình có thể giải thích khi nào không nên dùng nó không?

Nếu không qua được checklist này, mình coi là mình biết về nó, không phải biết nó.

Ví dụ cụ thể với Redis: Thay vì làm 2 tutorial về Redis trong 2 tuần, hãy dành 3 tuần để:

  • Implement một caching layer thực tế cho một feature trong project, với cache invalidation strategy rõ ràng.
  • Thử set maxmemory thấp và quan sát eviction behavior với các policy khác nhau (allkeys-lru vs volatile-lru vs noeviction).
  • Simulate Redis down và xem service behave thế nào — có fallback không, hay hard fail?
  • Đọc config file của Redis và hiểu từng option liên quan đến persistence (appendonly, save, rdbcompression...).

Sau 3 tuần đó, nếu ai hỏi về Redis trong production bạn sẽ có câu trả lời có chiều sâu thực sự — không phải chỉ định nghĩa từ documentation.


Lý do #4: Kỹ năng mềm kém — và đây không phải về việc nói chuyện hay

Mình cần frame lại cái "kỹ năng mềm" vì nhiều bạn technical nghe cụm từ này là tự động chán. Nó nghe có vẻ corporate, fuzzy, không đo được.

Nhưng mình không nói về chuyện tự tin trước đám đông hay thuyết trình hay. Mình nói về những thứ rất cụ thể và kỹ thuật:

Communicate uncertainty đúng cách:

Mid-level thường làm một trong hai: hoặc commit một con số estimate không chắc chắn ("3 ngày"), hoặc nói "không biết" và để người khác đoán.

Senior communicate uncertainty một cách structured: "Nếu requirement giữ nguyên như hiện tại và không có dependency mới, tôi estimate 3-4 ngày. Có 2 điểm tôi cần clarify với team data trước khi chắc chắn hơn — nếu phức tạp hơn kỳ vọng, có thể lên đến 6 ngày. Tôi sẽ spike 1 ngày đầu để có estimate chính xác hơn."

Nghe khác nhau rất nhiều. Cùng một mức độ không chắc chắn, nhưng cách sau cho người nghe đủ thông tin để plan.

Explain technical things without losing the non-technical person:

Câu chuyện thật: Một bạn mid-level trong team cũ của mình từng có buổi meeting với product manager để explain tại sao cần thêm 2 tuần cho một feature. Anh ấy explain rằng cần refactor database schema từ single-table inheritance sang concrete table inheritance vì query performance. PM nhìn blank. Meeting kết thúc không có outcome.

Tuần sau senior trong team explain cùng vấn đề đó: "Cách database hiện tại được setup, khi catalog product vượt quá 100,000 item — dự kiến Q3 theo roadmap — search sẽ chậm từ 200ms lên khoảng 8-10 giây. Chúng ta cần 2 tuần để fix điều này trước. Nếu không làm, chúng ta sẽ phải fix gấp trong mùa sale và risk cao hơn nhiều."

PM approve ngay.

Cùng một vấn đề kỹ thuật. Nhưng một cách framing kết nối với business concern, một cách thì không.

Code review không phải chỉ để tìm bug:

Mid-level review code thường làm một trong hai: approve mà gần như không comment, hoặc comment kiểu "biến này đặt tên không tốt" mà thiếu context.

Review tốt trông như thế này: "Đoạn này sẽ gây N+1 query khi list có hơn 10 item vì mỗi item trigger thêm một query lấy category. Với dữ liệu hiện tại thì chưa thấy, nhưng nếu bật eager loading ở đây thì tốt hơn — tôi có thể share một example nếu cần."

Comment đó có: vấn đề cụ thể, context tại sao là vấn đề, impact, và hướng giải quyết. Không phải judgment, là information.

Push back mà không làm vỡ relationship:

Đây là cái nhiều bạn dev Việt Nam struggle nhất vì culture. Khi PM yêu cầu một feature không realistic trong deadline, có 3 phản ứng phổ biến:

Một: đồng ý rồi chạy loạn, deadline qua thì xin lỗi.

Hai: nói thẳng "không làm được" mà không có alternative.

Ba (senior approach): "Deadline 5 ngày nhưng estimate là 10 ngày. Tôi có 3 option: giảm scope bằng cách bỏ tính năng A và B — estimate còn 5 ngày. Giữ scope nhưng bỏ qua error handling và test — đúng deadline nhưng technical debt cao, cần refactor sau. Hoặc dời deadline 5 ngày — deliver đầy đủ và stable. Mỗi option có tradeoff khác nhau — bạn muốn quyết định thế nào?"

Option ba vừa honest, vừa cho người ra quyết định đủ thông tin, vừa không đơn phương.

Về việc viết:

Mình nói thêm một chút về viết vì nhiều người bỏ qua. Senior engineer viết rõ ràng — trong Slack, trong PR description, trong technical spec, trong incident report. Không phải vì họ giỏi văn, mà vì họ hiểu rằng bad writing tốn time của người khác.

PR description viết kiểu "Fix bug login" là đang đẩy cost sang reviewer — họ phải đọc code để hiểu context, họ phải guess xem bạn đã test những gì, họ phải hỏi lại nếu cần. PR description viết rõ với: vấn đề là gì, tại sao fix theo cách này, những gì đã test, những gì cần reviewer chú ý — đó là respect time của người khác.


Lý do #5: Không biết debug và xử lý production incident

Production incident là cái phân loại engineer rõ nhất, nhanh nhất, và không thể giả vờ được.

Mình mô tả cùng một incident — CPU spike lên 90%, service chậm — với 4 cách phản ứng:

Cách 1 (Junior): "Em kiểm tra code phần mình thì không có gì sai."

Cách 2 (Mid-level — reactive): Mở Grafana, thấy CPU spike, restart service. Vấn đề tạm hết. Comment "Đã fix, theo dõi tiếp." Không investigate root cause.

Cách 3 (Mid-level — có tìm nhưng chưa đủ): Mở log, thấy nhiều error, paste một số lên Slack, nói "Có lỗi này anh ơi" — không có analysis, không có hypothesis, không có next step.

Cách 4 (Senior): Không restart ngay. Đầu tiên stabilize — nếu service đang ảnh hưởng production, scale horizontal hoặc circuit break trước. Sau đó mới investigate.

Investigation có structure: CPU spike bắt đầu từ lúc nào? Có deployment nào trước đó không? Traffic có gì bất thường không? Đọc thread dump — thread nào đang block, đang chờ gì? Sau đó correlation: CPU spike xảy ra cùng với gì — slow query spike, GC pause, hay incoming request pattern bất thường?

Với Java/JVM cụ thể, CPU spike thường đến từ:

  • GC thrashing: heap đầy, GC chạy liên tục nhưng không giải phóng được nhiều — cần heap dump để xem object retention.
  • Thread contention: nhiều thread cùng chờ lock trên shared resource — thread dump sẽ show BLOCKED threads và cái gì chúng đang wait on.
  • Hot loop trong code: một đoạn code đang chạy không dừng — CPU profiler (async-profiler, JFR) sẽ point thẳng vào.
  • Runaway connection pool: thread pool exhausted, request queue up, context switching tăng.

Mỗi case trên cần approach khác nhau để investigate. Ai chỉ biết "restart" đang bỏ qua toàn bộ bước học hỏi từ incident.

Kỹ năng cụ thể cần build:

Với backend JVM: biết dùng jstack, jmap, jstat. Biết đọc GC log. Biết dùng async-profiler hoặc Java Flight Recorder.

Với database: biết đọc slow query log, biết chạy EXPLAIN/EXPLAIN ANALYZE và interpret kết quả, biết các signal của index miss (Seq Scan trên table lớn, high rows examined/rows returned ratio).

Với Linux: top, htop, iostat, vmstat, netstat, ss, lsof. Biết cái nào dùng cho cái gì.

Với distributed system: biết correlation ID là gì và cách trace một request qua nhiều service. Biết đọc distributed trace trên Jaeger/Zipkin/X-Ray.

Cái kỹ năng không ai dạy: bình tĩnh dưới áp lực.

Khi incident xảy ra, mọi người nhắn tin dồn dập, manager hỏi ETA 5 phút một lần, user complain trên mạng xã hội. Trong cái chaos đó, người panic sẽ làm random things — restart service khi không cần, rollback deployment sai, thay đổi config mà không biết impact. Và thường làm vấn đề phức tạp hơn.

Bình tĩnh trong incident không phải là talent. Nó đến từ đã trải qua đủ nhiều incident, đã build đủ mental model về how systems fail, và đã có đủ experience để biết rằng mọi production issue đều có root cause — bạn chỉ cần systematic và kiên nhẫn.

Cách tập: volunteer cho on-call rotation ngay cả khi không bị bắt buộc. Sau mỗi incident — dù lớn hay nhỏ — viết postmortem cho riêng mình dù không ai yêu cầu. Timeline, root cause, contributing factors, action items. Làm vậy 10 lần, bạn sẽ thấy mình debug khác hẳn.


Lý do #6: Tư duy "làm cho xong" thay vì "làm cho đúng"

Mình cẩn thận với lý do này vì nó dễ bị misread.

"Làm cho đúng" không có nghĩa là over-engineer mọi thứ. Không có nghĩa là refactor infinite loop vì lý thuyết. Senior engineer không phải là người luôn làm perfect solution — họ biết perfect không tồn tại, và làm perfect cho mọi thứ là cách chắc chắn nhất để không ship được gì.

Vấn đề mình nói đến tinh tế hơn: thiếu awareness về quality và consequence của chính code mình đang viết.

Câu chuyện: Dũng nhận task thêm validation cho user input. Làm nhanh trong 1 ngày — check null, trim string, basic regex. Done.

3 tháng sau:

  • Có user submit tên với emoji được lưu vào database với encoding sai, hiển thị thành ký tự lạ.
  • Có account bị SQL injection mặc dù đang dùng ORM — vì Dũng dùng native query ở một chỗ và không parameterize.
  • Có field địa chỉ được lưu chuỗi 5000 ký tự vào column VARCHAR(255), truncation xảy ra silently không có error.
  • Validation rule không nhất quán giữa create và update endpoint — tạo user thì check email format, update thì không.

Dũng không lười. Anh ấy đã làm đúng yêu cầu. Nhưng anh ấy không hỏi: cái gì có thể sai? Edge case nào tôi chưa nghĩ đến?

So sánh nhỏ về code quality:

// Kiểu mid-level: work, không right
public User getUser(int id) {
    return userRepo.findById(id);
}

// Kiểu senior: right
public Optional<User> findUserById(int userId) {
    if (userId <= 0) {
        throw new IllegalArgumentException(
            "User ID phải là số dương, nhận được: " + userId
        );
    }
    return userRepo.findById(userId);
}

Không phải về style. Về tư duy: người viết code thứ hai đang nghĩ đến người gọi hàm này với input sai sẽ gặp gì. Người viết code thứ nhất chỉ đang nghĩ đến happy path.

Technical debt là hậu quả tích lũy của "làm cho xong":

Mình từng join một project 3 năm tuổi. Codebase không có một piece nào được refactor từ ngày viết. Team estimate mọi feature mất gấp 3 so với cùng feature trên greenfield project. 60% thời gian là fighting với legacy code thay vì build feature mới. Mọi người khổ sở. Sản phẩm chậm. Business không hiểu tại sao tech team chậm thế.

Nguyên nhân: 3 năm liên tiếp, mọi người "làm cho xong" — shortcut chồng shortcut, workaround chồng workaround, không ai dừng lại để nói chúng ta cần dọn cái này trước khi tiếp tục.

Nhưng cũng cần nói thẳng:

Có một nhóm developer khác — không phải "làm cho xong" mà là ngược lại — muốn mọi thứ phải perfect, refactor mãi không xong, không ship được gì vì tiêu chuẩn tự đặt ra quá cao. Đó cũng là vấn đề. Senior không phải là người viết code đẹp nhất — họ là người biết khi nào "đủ tốt" nghĩa là đủ tốt cho context này, và khi nào cần invest thêm.

Khi phải làm shortcut vì deadline — okay, nhưng phải acknowledge đó là shortcut, comment trong code, tạo ticket tech debt, và có intent sẽ quay lại. Đó là professional behavior. Làm shortcut mà không acknowledge là đang nói dối chính mình và team.


Lý do #7: Sợ system design và né tránh sự phức tạp

Đây là điểm nhạy cảm nhất vì nó đụng vào một thứ ít ai thừa nhận: sợ không biết đúng-sai.

Code có test. Test fail hay pass rõ ràng. Nhưng system design thì không — bạn propose một approach, người khác có thể argue ngược lại và cả hai đều có lý. Cái ambiguity đó rất uncomfortable với người quen với binary outcome.

Mình thấy nhiều bạn mid-level né tránh system design discussion theo những cách rất khéo:

  • "Để senior quyết định, em không rành về cái đó."
  • "Cái này em chưa làm bao giờ nên em không dám ý kiến."
  • Ngồi im trong architecture meeting, ghi chép, không có ý kiến gì.
  • Khi được hỏi ý kiến về approach: "Cái nào cũng được anh ơi, tùy anh."

Tất cả những điều đó là cách tránh rủi ro bị sai. Nhưng cũng là cách tránh luôn cơ hội phát triển.

Sự thật về system design:

Không có đúng sai tuyệt đối. Chỉ có tradeoff và context. Senior không biết "đáp án đúng" — họ biết cách reason về tradeoff và communicate uncertainty của mình một cách rõ ràng.

Ví dụ: "Nếu dùng approach A thì write throughput cao hơn nhưng read latency tăng. Nếu dùng approach B thì ngược lại. Với use case của chúng ta hiện tại — read-heavy, write có thể chịu delay — tôi nghiêng về A. Nhưng nếu có kế hoạch tăng write volume trong 6 tháng tới thì nên reconsider."

Câu đó không chắc 100%. Nhưng nó có reasoning, có tradeoff, có context. Đó là cách senior contribute vào technical discussion.

Về việc né tránh những thứ "không phải việc của mình":

Mình thường nghe: "Cái đó là việc của DevOps/Data/Frontend, mình không cần biết."

Câu này sẽ ngày càng trở thành liability. Software engineering hiện đại không có ranh giới cứng như vậy. Backend dev mà không hiểu container resource limit là gì, thì khi service bị OOMKilled vào ban đêm, không debug được. Không biết health check trong Kubernetes hoạt động thế nào, thì viết readiness probe sai và mỗi deployment có downtime mà không biết tại sao.

Không cần là expert về mọi thứ. Nhưng cần có đủ breadth để không bị bất ngờ hoàn toàn khi cần phối hợp với các domain khác.

Một thói quen đơn giản: Mỗi tháng, chọn một engineering blog post từ công ty lớn (Netflix, Stripe, Cloudflare, Shopify...) — họ publish rất nhiều bài hay về real engineering problem và cách họ giải quyết. Đọc với mục tiêu: họ đang giải quyết vấn đề gì? Tại sao approach họ chọn? Tradeoff là gì? Mình có thể áp dụng gì không? Không cần implement, chỉ cần hiểu reasoning.


Phần 3: Mid-level vs Senior — cụ thể hơn là chỉ "nghĩ khác nhau"

Mình muốn đi cụ thể hơn với một scenario thực tế. PM yêu cầu implement feature: cho phép user export danh sách đơn hàng ra Excel, tối đa 50,000 records.

Mid-level approach:

Nhận task, estimate 2 ngày. Query 50,000 records, tạo Excel file bằng Apache POI, trả về response. Test với 100 records — OK. Deploy lên production.

Ngày hôm sau: server timeout, memory spike lên 85%, một số request khác bị ảnh hưởng. Với 50,000 records, mỗi row có 20 column, Apache POI giữ toàn bộ trong memory trước khi write — có thể lên đến vài trăm MB per request. Nếu 5 user cùng export — server chết.

Senior approach:

Trước khi estimate, hỏi 3 câu:

  1. "50,000 records là số hiện tại, 6 tháng nữa có thể là bao nhiêu? Growth rate của data như thế nào?"

  2. "User có cần download ngay không, hay có thể nhận link download sau 1-2 phút?"

  3. "Feature này sẽ được dùng thường xuyên không hay chỉ thỉnh thoảng? Cùng lúc có bao nhiêu user có thể export?"

Với câu trả lời từ những câu hỏi đó, propose: async export — user submit request, system xử lý background với streaming write để không load toàn bộ vào memory, gửi email hoặc notification khi file ready để download. File lưu trên S3 với pre-signed URL, expire sau 24 giờ.

Estimate: 4 ngày thay vì 2 ngày. Nhưng explain được tại sao — bao gồm giải thích risk của approach đơn giản hơn.

Không phải senior phức tạp hóa mọi thứ. Họ đặt câu hỏi đúng trước khi code, và câu trả lời của những câu hỏi đó thay đổi cách implement.


Bảng so sánh thực tế:

Khía cạnh Mid-level Senior
Nhận task không rõ Làm theo cách hiểu của mình Clarify assumptions, document rõ
Gặp technical blocker Stuck hoặc workaround nhanh Investigate root cause, evaluate options
Code review Approve hoặc comment style Explain impact của vấn đề, suggest fix
Estimation Một con số Range + assumptions + unknowns
Production incident Reactive, fix symptom Stabilize → root cause → prevent recurrence
Tech debt Tạo ra và chấp nhận Acknowledge, document, có plan
Cross-team Tối thiểu Proactive khi cần
Documentation Thiếu hoặc sơ sài Đủ để người mới đọc hiểu và không cần hỏi

Phần 4: Thay đổi mindset — cụ thể hơn là "cần think different"

Mình ghét những lời khuyên kiểu "thay đổi mindset" mà không nói thay đổi cụ thể là làm gì. Dưới đây là những thứ rất cụ thể:

Thứ nhất: Hỏi "tại sao" trước khi hỏi "làm thế nào"

Khi nhận task, trước khi hỏi implementation detail, hãy hiểu business reason. "Feature này giải quyết vấn đề gì cho user? Nếu không có feature này thì user đang làm gì? Success của feature này được đo bằng gì?"

Không phải để khó chịu hay challenge PM. Mà vì hiểu "tại sao" thường thay đổi hoàn toàn "làm thế nào" — và đôi khi bạn sẽ nhận ra có cách đơn giản hơn nhiều để đạt cùng outcome.

Thứ hai: Sau khi deploy, không phải hết việc

Đặt reminder 3 ngày và 1 tuần sau deploy. Vào check log có error gì không, metric có bất thường không, user có phản hồi gì không. 15-20 phút. Thói quen này thay đổi cách bạn viết code ngay từ đầu — vì bạn biết mình sẽ phải nhìn lại nó.

Thứ ba: Viết nhiều hơn nói

Sau mỗi technical decision quan trọng, viết một đoạn ngắn: vấn đề là gì, các option đã xem xét, option được chọn và tại sao, tradeoff của nó. Không cần fancy — một comment trong code, một note trong Confluence, một đoạn trong PR description. Sau vài tháng, bạn sẽ thấy reasoning của mình rõ hơn, và người khác sẽ hiểu work của bạn hơn.

Thứ tư: Tìm feedback chủ động, không chờ

Sau mỗi project hoặc sprint, chủ động hỏi một người — có thể là manager, có thể là senior colleague — một câu: "Có điều gì tôi làm tốt hơn trong lần này không?" Nghe đơn giản nhưng hầu hết người không làm.

Thứ năm: Đừng giả vờ biết khi không biết

"Tôi không chắc, để tôi check lại và quay lại với bạn" là câu của người có confidence thực sự. Người sợ bị nhìn nhận là không biết mới hay bịa câu trả lời hoặc nói vòng vo.


Phần 5: Lộ trình 6-18 tháng — không có shortcut nhưng có con đường rõ

Giai đoạn 0: Honest assessment (2 tuần đầu)

Trước khi làm gì, phải biết mình đang ở đâu.

Câu hỏi tự đánh giá:

  • Tôi có thể vẽ architecture diagram của hệ thống mình đang làm từ trí nhớ không?
  • Khi có production incident, tôi có thể tự investigate mà không cần ai chỉ không?
  • Tôi có thể explain một technical decision cho product manager và họ hiểu không?
  • PR description của tôi có đủ context để reviewer không cần hỏi thêm không?
  • Trong 6 tháng qua, tôi đã tạo ra gì mà nếu không có tôi thì không có, hoặc tệ hơn nhiều?

Với mỗi câu trả lời "không" — đó là gap. List ra. Đó là roadmap của bạn.

Giai đoạn 1: Foundation (Tháng 1-3)

Technical depth:

Chọn một area để go deep thực sự — không phải học thêm tool mới, mà là hiểu sâu hơn về thứ đang dùng. Nếu bạn làm backend Java, hãy thực sự hiểu JVM — GC mechanisms, memory model, threading, concurrency primitives. Nếu bạn làm database-heavy system, hãy hiểu query optimizer hoạt động thế nào, index internals, transaction isolation levels và implementation.

Mục tiêu: sau 3 tháng, khi có vấn đề performance, bạn biết cụ thể cần nhìn vào đâu và tại sao.

Hệ thống hiện tại:

Vẽ architecture diagram đầy đủ. Identify single points of failure. Identify những chỗ không có monitoring. Pick một trong số đó và fix — dù không ai yêu cầu.

Soft skills:

Bắt đầu viết PR description tốt hơn từ ngay hôm nay. Template: Vấn đề/context → Approach và tại sao → Những gì đã test → Những gì reviewer nên chú ý → Screenshots nếu có UI change.

Giai đoạn 2: Visibility và impact (Tháng 4-9)

Lead một initiative nhỏ:

Đề xuất và lead một technical improvement — setup monitoring dashboard, introduce linting rules, cải thiện CI/CD pipeline, viết runbook cho on-call, refactor một module legacy. Không cần lớn. Cần có measurable impact và cần bạn drive nó từ đầu đến cuối.

System design practice:

Mỗi tuần, tự design một system — không cần người review. Chọn một bài từ "System Design Interview" của Alex Xu hoặc từ engineering blog của các công ty. Tự đặt câu hỏi, tự trả lời, tự identify gap trong reasoning của mình.

Sau đó tìm một senior hoặc mentor để review ít nhất một lần mỗi tháng. Không cần nhiều — một session 1 tiếng với người có experience sẽ cho bạn feedback mà tự học không cho được.

Mentoring:

Bắt đầu giúp junior trong team. Không cần formal — chỉ cần pair với họ 1-2 lần mỗi tuần. Giải thích thứ bạn biết. Cái này vừa giúp team, vừa giúp bạn — vì giải thích cho người khác là cách nhanh nhất để tìm ra gap trong understanding của chính mình.

Giai đoạn 3: Senior-level impact (Tháng 10-18)

Ở đây, mục tiêu không còn là "học thêm" — mà là consistently demonstrate senior behaviors và có measurable impact.

Câu hỏi để self-evaluate:

  • Team có chạy được tốt hơn vì có mình không?
  • Junior trong team có grow nhanh hơn vì có mình không?
  • Có ít nhất một technical decision quan trọng trong 6 tháng qua mà mình có đóng góp rõ ràng không?
  • Tôi có thể point vào ít nhất một metric cụ thể mà tốt hơn vì việc tôi làm không?

Conversation với manager:

Đừng chờ được nhận ra. Chủ động có cuộc nói chuyện: "Tôi đang actively working toward Senior role. Cụ thể tôi đã làm [X, Y, Z]. Theo anh/chị, Senior engineer ở đây cần thể hiện thêm những gì? Và có opportunity nào phù hợp để tôi demonstrate senior-level work không?"

Câu này làm ba việc cùng lúc: show initiative, clarify expectation, và create alignment với manager.

Resources thực sự cần đọc:

Designing Data-Intensive Applications của Martin Kleppmann — không có cuốn nào khác giải thích distributed systems và data engineering ở mức này cho developer thực chiến.

A Philosophy of Software Design của John Ousterhout — không dài, không glamorous, nhưng thay đổi cách mình nghĩ về complexity.

Engineering blogs của Stripe, Cloudflare, Discord, Shopify — họ publish về real problems và real solutions. Không phải tutorial. Là engineering case study thực tế.


Những cái bẫy phổ biến khi đang cố lên Senior

Bẫy 1: Nhảy công ty để lấy title.

Không phải không hợp lý — employer mới đôi khi dễ convince hơn employer hiện tại. Nhưng nếu bạn chưa thực sự có senior behaviors, title đó sẽ lộ ra rất nhanh. Bạn sẽ được expect perform ở level cao hơn mà không có support infrastructure giúp bạn get there. Imposter syndrome sẽ nặng hơn nhiều so với nếu bạn được grow thật sự vào vị trí đó.

Bẫy 2: "Tôi cần học thêm A, B, C thì mới đủ điều kiện."

Không bao giờ đủ điều kiện theo cách này. Senior không phải người biết nhiều nhất — họ là người biết đủ để deliver, biết cách tìm thêm khi cần, và biết khi nào cần hỏi người khác.

Bẫy 3: "Tôi đang làm nhiều hơn mọi người, tại sao không được promote?"

Số lượng task hoàn thành không phải là thứ promote bạn. Impact là. Nếu bạn làm 10 task nhỏ trong khi người khác làm 2 task có leverage cao hơn nhiều — bạn đang busy, không phải impactful. Hỏi bản thân: trong 6 tháng qua, nếu không có mình, team có bị thiệt hại thực sự không? Hay mọi thứ vẫn chạy được gần như bình thường?

Bẫy 4: Môi trường không cho phép grow.

Đây là thứ thật nhưng hay bị dùng như excuse. Đúng là có môi trường toxic, có manager không tốt, có công ty không có culture of learning. Những môi trường đó thực sự kìm hãm sự phát triển.

Nhưng trước khi conclude đó là lý do, hãy honest: "Tôi đã làm hết phần của mình chưa? Tôi đã có những cuộc nói chuyện thẳng thắn về career với manager chưa? Tôi đã chủ động tìm opportunity chưa, hay tôi chờ opportunity được đưa đến?"

Nếu câu trả lời là có, và môi trường vẫn không cho bạn không gian để grow — thì đó là lúc thực sự cân nhắc chuyển. Nhưng hãy đảm bảo bạn đang chuyển để tìm môi trường tốt hơn, không phải chạy trốn khỏi những vấn đề bạn cần đối mặt ở chính mình trước.


Kết: Một vài điều mình muốn nói thẳng

Không phải ai cũng nên lên Senior.

Mình nói thật. Senior không phải là đích đến duy nhất của career. Có người hạnh phúc và productive ở mid-level — họ biết mình muốn gì, không có ambition để own hệ thống lớn hơn, và không bị frustrated vì không lên level. Đó là lựa chọn hợp lý.

Bài này dành cho người đang thực sự muốn lên senior và đang frustrated vì không biết tại sao mình không đến được đó.

Company sẽ không tự nhiên promote bạn.

Đây là sự thật không được nói đủ nhiều. Công ty promote bạn khi họ thấy evidence rõ ràng rằng bạn đang operate ở level tiếp theo — không phải khi bạn đã làm tốt ở level hiện tại đủ lâu. Bạn cần act at the next level trước khi được công nhận ở level đó.

Senior engineer sai nhiều — họ chỉ học tốt hơn từ sai lầm.

Đừng chờ cảm thấy "đủ sẵn sàng". Sẵn sàng không đến từ chuẩn bị — nó đến từ làm và adjust. Nếu bạn đang chờ không còn uncertain về technical decisions, bạn sẽ chờ mãi. Uncertainty là bình thường ở mọi level. Senior không less uncertain — họ better at operating with uncertainty.

Và điều cuối:

Mình đã nói những điều khó nghe trong bài này. Nhưng mình nói vì mình đã ở đó — đã là người 4 năm kinh nghiệm mà bị từ chối senior position, đã không hiểu tại sao, đã mất thêm 2 năm để thực sự hiểu những thứ mình viết trong bài này.

Nếu bạn nhận ra mình trong một hoặc nhiều lý do trên — đó không phải là tin xấu. Đó là tin tốt. Vì bạn biết cái gì cần thay đổi. Cái khó nhất không phải là thay đổi — mà là biết mình cần thay đổi ở đâu.

Bắt đầu từ một thứ. Không cần bắt đầu từ mười thứ cùng lúc.


Bài viết dựa trên 12 năm làm backend engineering qua outsourcing, product company và scale-up. Nếu bạn có câu hỏi hoặc muốn chia sẻ câu chuyện của mình — comment bên dưới.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí