Mở đầu — vì sao bài này quan trọng
Có một câu chuyện tôi hay kể cho các bạn tech lead mới: quyết định chọn tech stack giống như quyết định mua nhà, không phải mua áo. Áo mặc chán thì bỏ, mất vài trăm nghìn. Nhà thì bạn sẽ sống trong đó nhiều năm, mọi ngóc ngách của nó — dột chỗ nào, ổ điện đặt đâu, hàng xóm ra sao — sẽ ảnh hưởng đến cuộc sống hàng ngày của bạn. Tech stack cũng vậy. Bạn chọn ngôn ngữ, framework, database, hạ tầng hôm nay, và ba năm sau cả đội vẫn phải sống chung với lựa chọn đó — kể cả khi nó đã lỗi thời hoặc không còn phù hợp.
Điều khiến việc chọn stack trở nên khó là nó hiếm khi có "câu trả lời đúng". Không có ngôn ngữ nào tốt nhất tuyệt đối, không có database nào thắng mọi kịch bản. Cùng một bài toán, một startup 5 người và một tập đoàn 500 kỹ sư sẽ chọn khác nhau, và cả hai đều đúng trong bối cảnh của mình. Vậy nên nhiệm vụ của tech lead không phải là tìm ra "stack tốt nhất" mà là xây dựng một khung quyết định (decision framework) minh bạch, cân nhắc đúng các yếu tố, và quan trọng nhất là giải thích được vì sao với đội và với cấp trên.
Bài này tập trung hẹp vào đúng điều đó: khi bạn phải chọn công nghệ nền tảng cho một dự án hoặc một sản phẩm mới, bạn dựa vào những yếu tố nào, cân đo chúng ra sao, và làm sao để ra quyết định mà không bị cảm tính hay theo trend dẫn dắt. Đây không phải bài về việc quản lý danh mục công nghệ toàn công ty (Tech Radar), cũng không phải bài về việc nên tự build hay đi mua (Build vs Buy) — đó là những bài riêng. Ở đây, ta nói về khoảnh khắc bạn ngồi xuống và phải trả lời: "Chúng ta sẽ viết cái này bằng gì?"
Khái niệm cốt lõi
Một quyết định chọn stack tốt là quyết định cân bằng được nhiều yếu tố mâu thuẫn nhau. Tôi thường nhóm chúng thành bảy nhóm chính. Không phải nhóm nào cũng có trọng số bằng nhau — điều đó phụ thuộc vào bối cảnh của bạn — nhưng bỏ sót bất kỳ nhóm nào cũng dễ dẫn đến hối tiếc.
1. Team expertise — đội bạn đang thạo gì?
Đây gần như luôn là yếu tố nặng ký nhất, đặc biệt với các đội nhỏ hoặc dự án có deadline gấp. Một đội thành thạo Node.js sẽ ship một sản phẩm bằng Node.js nhanh gấp nhiều lần so với việc học Go từ đầu, dù Go có thể "phù hợp về mặt kỹ thuật" hơn. Chi phí học một stack mới không chỉ là thời gian đọc tài liệu — nó là hàng tháng trời mắc những lỗi ngớ ngẩn mà một người có kinh nghiệm sẽ không bao giờ mắc, là những quyết định kiến trúc sai vì chưa hiểu rõ "cách làm đúng" của hệ sinh thái đó.
Nhưng cẩn thận: "đội đang thạo gì" không có nghĩa là "mãi mãi chỉ dùng thứ đội đang thạo". Nếu vậy thì công ty nào cũng còn viết PHP thuần từ 2005. Câu hỏi tinh tế hơn là: sự chênh lệch về năng suất giữa cái đội đã biết và cái mới có đáng để đánh đổi lợi ích kỹ thuật lâu dài không?
2. Hiring market — tuyển người ở thị trường của bạn có dễ không?
Đây là yếu tố mà kỹ sư hay quên nhưng tech lead giỏi luôn nhớ. Bạn không chọn stack cho đội hiện tại — bạn chọn cho cả đội tương lai mà bạn chưa tuyển. Ở thị trường Việt Nam, thực tế phũ phàng là: Java, JavaScript/Node.js, PHP, Python và gần đây là Golang có nguồn ứng viên dồi dào, mức lương hợp lý, tuyển trong vài tuần là có người. Ngược lại, Rust, Elixir, Scala, Clojure — bạn có thể đăng tin ba tháng vẫn không tìm được người senior ở TP.HCM hay Hà Nội, và nếu tìm được thì mức lương họ đòi cũng cao vượt trội vì họ biết mình hiếm.
Điều này tạo ra một nghịch lý thú vị: một stack "kém hấp dẫn về mặt kỹ thuật" nhưng dễ tuyển đôi khi là lựa chọn kinh doanh khôn ngoan hơn. Ngược lại, một stack "cool" nhưng khó tuyển có thể biến bạn thành con tin của vài kỹ sư — họ nghỉ là dự án đứng.
3. Sự trưởng thành của hệ sinh thái (ecosystem maturity)
Một công nghệ trưởng thành có nghĩa là: khi bạn gặp lỗi, gõ vào Google là ra câu trả lời trên Stack Overflow; thư viện cho mọi thứ bạn cần (thanh toán, gửi mail, xử lý ảnh) đã có sẵn và được bảo trì; công cụ debug, monitoring, CI/CD đều hỗ trợ. Một công nghệ non trẻ thì ngược lại — bạn thường xuyên là người đầu tiên gặp một bug nào đó, và phải tự viết thứ mà hệ sinh thái khác đã có sẵn.
4. Sự phù hợp với bài toán (fitness for purpose)
Đây là yếu tố "kỹ thuật thuần" mà mọi người hay nghĩ tới đầu tiên. Bài toán của bạn có đặc thù gì? Xử lý real-time hàng triệu kết nối đồng thời (WebSocket, game, chat) thì Elixir/Go/Node có lợi thế. Tính toán số học nặng, machine learning thì Python. Ứng dụng doanh nghiệp phức tạp, cần độ tin cậy và kiểu dữ liệu chặt chẽ thì Java/C#. Đừng bắt một con ngựa đi bơi.
5. Chi phí vận hành và hiệu năng (operational cost)
Một số stack ngốn RAM/CPU hơn, cần nhiều server hơn để chịu cùng tải. Với startup đang đốt tiền nhà đầu tư thì có thể không quan trọng, nhưng với đội đã có traffic lớn, chênh lệch hiệu năng có thể là hàng chục nghìn đô tiền cloud mỗi tháng.
6. Tốc độ phát triển vs khả năng bảo trì dài hạn
Có những stack cho phép bạn ship cực nhanh ở giai đoạn đầu (Ruby on Rails, Django, Laravel) nhưng có thể trở nên khó kiểm soát khi codebase phình to. Có những stack chậm hơn lúc đầu nhưng "an toàn" hơn khi quy mô lớn (các ngôn ngữ có kiểu tĩnh mạnh như TypeScript, Java, Go). Trade-off này gắn chặt với giai đoạn của công ty bạn.
7. Chiến lược và tính nhất quán của tổ chức
Nếu công ty bạn đã có 20 dịch vụ viết bằng Go, thì việc dịch vụ thứ 21 dùng Python cần một lý do rất thuyết phục. Sự đồng nhất giúp kỹ sư di chuyển giữa các đội dễ dàng, chia sẻ thư viện nội bộ, giảm chi phí vận hành. "Best tool for the job" nghe rất hay nhưng nếu áp dụng máy móc, bạn sẽ có một vườn thú công nghệ mà không ai bảo trì nổi.
Tình huống thực tế
Ví dụ 1 — Startup fintech tại TP.HCM chọn Node.js thay vì "stack lý tưởng"
Một startup fintech giai đoạn seed ở Việt Nam, đội 6 kỹ sư, cần ra mắt sản phẩm ví điện tử trong 4 tháng để kịp demo cho vòng gọi vốn Series A. CTO ban đầu muốn dùng Go cho backend vì "fintech cần hiệu năng và độ tin cậy cao". Nghe rất hợp lý về mặt kỹ thuật.
Nhưng khi ngồi lại phân tích, họ nhận ra: cả 6 người đều mạnh về JavaScript/TypeScript, chỉ 1 người từng viết Go ở mức sở thích. Thị trường tuyển dụng ở TP.HCM lúc đó có hàng trăm CV Node.js so với chỉ vài chục CV Go biết. Với deadline 4 tháng, việc học Go sẽ ngốn mất 1-2 tháng đầu chỉ để "làm quen". Họ quyết định chọn Node.js + TypeScript + PostgreSQL, và dành phần công sức tiết kiệm được để làm kỹ khâu testing và audit bảo mật — thứ thực sự quan trọng với fintech.
Kết quả: họ ship đúng hạn, gọi vốn thành công. Hai năm sau, khi một vài dịch vụ lõi (xử lý giao dịch) cần hiệu năng cao hơn, họ mới viết lại riêng những phần đó bằng Go — lúc này đội đã lớn hơn, có tiền tuyển người Go, và quan trọng là họ đã hiểu rõ bài toán.
Bài học: ở giai đoạn sống-còn, "đủ tốt và ship được" thắng "hoàn hảo về kỹ thuật nhưng trễ hạn". Và bạn luôn có thể tối ưu cục bộ sau, khi rủi ro đã giảm.
Ví dụ 2 — Cái giá của việc chọn stack "cool" nhưng khó tuyển
Một công ty công nghệ tầm trung ở Đông Nam Á (khoảng 40 kỹ sư) từng có một tech lead rất giỏi mê Elixir. Anh dẫn dắt đội xây dựng hệ thống thông báo real-time bằng Elixir/Phoenix — và về mặt kỹ thuật, nó thực sự xuất sắc, xử lý hàng trăm nghìn kết nối đồng thời mượt mà. Không ai phủ nhận Elixir là công cụ đúng cho bài toán đó.
Vấn đề nảy sinh 18 tháng sau, khi anh tech lead này rời công ty. Cả đội còn lại chỉ 2 người biết Elixir, và cả hai đều không muốn "gánh" mảng này lâu dài. Công ty đăng tin tuyển kỹ sư Elixir suốt 5 tháng — ở thị trường Đông Nam Á gần như không có ứng viên, và những người biết thì đòi lương cao gấp rưỡi mặt bằng. Cuối cùng họ buộc phải viết lại toàn bộ hệ thống bằng Node.js, tốn thêm gần 4 tháng công sức của cả đội — một khoản chi phí khổng lồ chỉ để đổi lấy khả năng tuyển dụng và bảo trì bền vững.
Bài học: một quyết định stack không chỉ được đánh giá bằng việc nó giải bài toán kỹ thuật tốt đến đâu, mà còn bằng việc tổ chức có thể duy trì nó không, kể cả khi những cá nhân giỏi nhất ra đi. "Bus factor" (số người nghỉ là dự án chết) phải là một tiêu chí thực sự.
Ví dụ 3 — Tập đoàn lớn ưu tiên tính nhất quán hơn "best tool"
Một ngân hàng số ở Việt Nam đã chuẩn hóa toàn bộ backend microservices trên Java + Spring Boot. Khi một đội mới đề xuất viết một dịch vụ phân tích dữ liệu bằng Python (vì Python mạnh về data), lãnh đạo engineering đã cân nhắc rất kỹ. Về mặt "best tool for the job", Python đúng là hợp hơn cho phần machine learning.
Nhưng họ quyết định vẫn giữ Java cho phần dịch vụ (service layer, API), và chỉ cho phép Python trong một phạm vi cô lập rõ ràng: các job xử lý dữ liệu chạy offline, đóng gói riêng. Lý do: toàn bộ hạ tầng CI/CD, monitoring, thư viện bảo mật nội bộ, chuẩn logging của họ đều được xây quanh Java. Thêm một ngôn ngữ vào tầng dịch vụ nghĩa là nhân đôi công sức duy trì tất cả những thứ đó, trong một ngành mà yêu cầu tuân thủ (compliance) cực kỳ khắt khe.
Bài học: ở quy mô lớn, chi phí của sự đa dạng công nghệ (cognitive load, hạ tầng, bảo mật, tuyển dụng, luân chuyển nhân sự giữa các đội) thường lớn hơn lợi ích kỹ thuật cục bộ. Nhất quán là một tính năng, không phải sự lười biếng. Cách xử lý khôn ngoan là cho phép ngoại lệ trong phạm vi được kiểm soát chặt, chứ không phải cấm tiệt hay thả cửa.
Hướng dẫn từng bước
Đây là quy trình tôi khuyên bạn dùng mỗi khi phải chọn stack cho một dự án đáng kể. Đừng làm trong đầu — hãy viết ra.
Bước 1 — Xác định bối cảnh và ràng buộc trước tiên. Trước khi nói về công nghệ, trả lời: Công ty đang ở giai đoạn nào (tìm product-market fit hay scale)? Deadline ra sao? Đội bao nhiêu người, thạo gì? Ngân sách hạ tầng? Yêu cầu đặc thù (real-time, compliance, offline...)? Những ràng buộc này quyết định trọng số của từng yếu tố. Một startup 4 tháng deadline sẽ đánh trọng số "team expertise" rất cao; một hệ thống ngân hàng sẽ đánh trọng số "độ tin cậy và compliance" rất cao.
Bước 2 — Liệt kê 2-3 phương án khả dĩ, không nhiều hơn. Nếu bạn có 8 phương án, bạn đang phân tích quá rộng. Thu hẹp bằng cảm quan chuyên môn xuống 2-3 ứng viên thực sự nghiêm túc.
Bước 3 — Chấm điểm theo bảng trọng số. Lập một bảng đơn giản: hàng là các yếu tố (team expertise, hiring market, ecosystem, fitness, chi phí vận hành, bảo trì dài hạn, nhất quán tổ chức), cột là các phương án. Gán trọng số cho mỗi yếu tố dựa trên Bước 1, rồi chấm điểm từng phương án 1-5. Bảng này không phải để "máy móc chọn điểm cao nhất" — nó để buộc bạn suy nghĩ tường minh và để giải thích với người khác.
Bước 4 — Kiểm tra "bus factor" và kịch bản xấu. Với phương án dẫn đầu, hỏi: Nếu người giỏi nhất về nó nghỉ hôm nay thì sao? Ta tuyển thay được trong bao lâu? Nếu công nghệ này ngừng được cộng đồng hỗ trợ trong 3 năm thì sao? Nếu câu trả lời khiến bạn rùng mình, hãy cân nhắc lại.
Bước 5 — Làm một spike/prototype nhỏ nếu còn phân vân. Với những quyết định lớn và không chắc chắn, dành 1-2 tuần dựng thử phần rủi ro nhất của bài toán bằng phương án dẫn đầu. Thực tế luôn dạy bạn nhiều hơn lý thuyết.
Bước 6 — Ghi lại quyết định dưới dạng văn bản (ADR). Viết một tài liệu ngắn: bối cảnh, các phương án đã cân nhắc, quyết định cuối, và lý do. Tài liệu này (Architecture Decision Record) vô giá cho người sau — 18 tháng nữa khi có người hỏi "sao hồi đó lại chọn cái này?", bạn có câu trả lời rõ ràng thay vì "tôi cũng không nhớ nữa".
Bước 7 — Định nghĩa điểm review lại. Không quyết định nào là vĩnh viễn. Ghi rõ: "Ta sẽ đánh giá lại lựa chọn này khi đạt X người dùng / khi đội vượt Y người / sau 12 tháng." Điều này biến quyết định từ "vĩnh cửu và đáng sợ" thành "có thể điều chỉnh".
Lỗi thường gặp & mẹo
Lỗi 1 — Chọn theo trend (resume-driven development). Kỹ sư đôi khi vô thức muốn dùng công nghệ mới nhất để CV của họ đẹp hơn, chứ không phải vì nó tốt cho công ty. Là tech lead, bạn phải nhận ra động lực này — kể cả ở chính mình. Câu hỏi kiểm tra: "Nếu công nghệ này không hot trên Twitter/HackerNews, tôi có còn chọn nó không?"
Lỗi 2 — Bỏ qua hiring market vì nó "không phải vấn đề kỹ thuật". Rất nhiều tech lead giỏi kỹ thuật thất bại ở đây. Khả năng tuyển và giữ người cho một stack là yếu tố sống còn, không phải chi tiết phụ.
Lỗi 3 — "Best tool for the job" áp dụng máy móc. Mỗi dịch vụ chọn một ngôn ngữ riêng vì "hợp nhất" nghe rất kỹ thuật, nhưng kết quả là một tổ chức không thể bảo trì. Chi phí đa dạng là có thật và thường bị đánh giá thấp.
Lỗi 4 — Quyết định một mình rồi áp đặt. Đội sẽ phải sống với stack này, nên họ cần được tham gia. Một quyết định được thảo luận và đồng thuận sẽ được thực thi với tinh thần trách nhiệm cao hơn nhiều so với quyết định áp từ trên xuống.
Lỗi 5 — Nhầm sở thích cá nhân với dữ liệu. "Tôi ghét PHP" không phải là một lý luận kỹ thuật. Tách bạch cảm xúc cá nhân khỏi phân tích khách quan.
Mẹo: Với các thành phần lõi và khó thay đổi (ngôn ngữ chính, database chính), hãy bảo thủ — chọn thứ đã được kiểm chứng, dễ tuyển, hệ sinh thái trưởng thành. Với các thành phần ngoại vi và dễ thay (một thư viện, một job phụ trợ), bạn có thể mạnh dạn thử nghiệm hơn. Đây là cách để vừa an toàn vừa không ngừng học hỏi.
Mẹo: Đừng quên chi phí ẩn của "chuyện thần thoại về sự đồng nhất một ngôn ngữ" như dùng Node.js cho cả frontend và backend. Nghe hấp dẫn ("một ngôn ngữ, chia sẻ code!"), nhưng frontend và backend là những bài toán rất khác nhau — đừng để lời hứa "cùng ngôn ngữ" che mờ việc đánh giá xem nó có thực sự là công cụ tốt cho từng phía không.
Bài tập thực hành
Bài tập 1 — Lập bảng quyết định. Hãy tưởng tượng bạn là tech lead của một startup thương mại điện tử tại Việt Nam, đội 8 kỹ sư (mạnh Python và JavaScript), cần xây lại backend trong 6 tháng, kỳ vọng scale lên 100.000 người dùng trong năm đầu. Lập bảng trọng số với ít nhất 6 yếu tố, chấm điểm cho 3 phương án: (a) Python/Django, (b) Node.js/NestJS, (c) Go. Viết ra kết luận và lý do, không chỉ điểm số.
Bài tập 2 — Phản biện chính mình. Chọn stack mà dự án hiện tại của bạn (hoặc dự án bạn biết) đang dùng. Viết một đoạn 200 từ lập luận rằng lựa chọn đó sai, và một đoạn 200 từ lập luận nó đúng. Mục tiêu là rèn khả năng nhìn cả hai phía thay vì bảo vệ định kiến.
Bài tập 3 — Viết một ADR thật. Chọn một quyết định công nghệ bạn từng tham gia và viết lại nó dưới dạng Architecture Decision Record: bối cảnh, các phương án, quyết định, hệ quả. Nếu bạn không viết nổi lý do rõ ràng, đó là dấu hiệu quyết định ban đầu thiếu cơ sở — một bài học quý.
Tóm tắt
Chọn tech stack không phải là đi tìm "công nghệ tốt nhất" — thứ đó không tồn tại — mà là ra một quyết định cân bằng, minh bạch và phù hợp với bối cảnh cụ thể của bạn. Bảy yếu tố cần cân nhắc gồm: năng lực đội (team expertise), thị trường tuyển dụng (đặc biệt quan trọng ở Việt Nam, nơi Java/Node/Python dễ tuyển còn Rust/Elixir rất khó), sự trưởng thành của hệ sinh thái, sự phù hợp với bài toán, chi phí vận hành, cân bằng giữa tốc độ và khả năng bảo trì dài hạn, và tính nhất quán của tổ chức.
Trọng số của từng yếu tố thay đổi theo giai đoạn công ty: startup ưu tiên tốc độ và năng lực đội sẵn có; tổ chức lớn ưu tiên nhất quán, độ tin cậy và khả năng tuyển dụng bền vững. Ba câu chuyện thực tế đã cho thấy: chọn stack dễ tuyển đôi khi thắng stack "hoàn hảo kỹ thuật"; stack "cool" nhưng khó tuyển có thể trở thành gánh nợ khi người giỏi ra đi; và ở quy mô lớn, tính nhất quán thường đáng giá hơn "best tool for the job" áp dụng máy móc.
Cuối cùng, hãy biến quyết định thành một quy trình có thể giải thích được: xác định ràng buộc, thu hẹp phương án, chấm điểm theo trọng số, kiểm tra bus factor, prototype nếu cần, ghi lại bằng ADR, và định trước thời điểm review lại. Một quyết định stack tốt không phải là quyết định không bao giờ sai — mà là quyết định bạn hiểu rõ vì sao mình chọn, và sẵn sàng điều chỉnh khi bối cảnh đổi thay.