Mời bạn thưởng thức Newsletter #132.
Perfection is not over-engineering
Tác giả mở đầu bằng câu nói mà anh đã nghe không biết bao nhiêu lần trong các cuộc họp: “chúng ta không muốn làm cho hoàn hảo”. Từ “hoàn hảo” bị đối xử như một điều xấu, bởi vì việc thiết kế quá mức (over-engineering) từng khiến nhiều đội nhóm kiệt sức, nên người ta học cách nghi ngờ mọi thứ có mùi cầu toàn. Nhưng theo anh, cả ngành đã âm thầm đồng nhất hai khái niệm khác nhau. Thiết kế quá mức không phải là “quan tâm quá nhiều” hay “làm tốt quá”, mà đơn giản là giải sai bài toán — thường với thiện chí, và gần như luôn kèm theo một đống phức tạp phát sinh. Trong khi đó, một giải pháp hoàn hảo thực sự tồn tại, với điều kiện bạn có một bộ yêu cầu thật rõ ràng và mọi ràng buộc đều được đặt lên bàn. Siết các ràng buộc đủ chặt thì chỉ còn đúng một giải pháp khả thi, và nó hoàn hảo chính vì nó là thứ duy nhất vừa khít. Cùng một không gian bài toán nhưng ràng buộc khác nhau sẽ cho câu trả lời khác nhau: chọn serverless với Python vì không cần biên dịch là hợp lý với người này, nhưng sai với người cần tối ưu hiệu năng hoặc không biết Python.
Điểm thứ hai của bài là mọi hệ thống đều là sản phẩm. Chúng ta hay tự nhủ rằng một thư viện, một API hay một công cụ nội bộ là “thuần kỹ thuật”, nằm ngoài khái niệm sản phẩm — nhưng chúng vẫn có người dùng với nhu cầu thật, và hình dạng của giải pháp chỉ hiện rõ khi bạn xác định yêu cầu một cách trung thực. Dấu hiệu rõ nhất của thiết kế quá mức là khi bạn hỏi “tại sao mọi thứ được xây theo cách này?” mà các câu trả lời đều không đứng vững. Ví dụ kinh điển: một đội ba người duy trì năm microservice có chia sẻ dữ liệu với nhau. Thứ từng là khóa ngoại được cơ sở dữ liệu tự động bảo đảm nay biến thành một chuỗi id rời rạc, tính toàn vẹn dữ liệu mất sạch, một dịch vụ xóa bản ghi mà dịch vụ kia không hề biết. Đổi lại họ được quyền triển khai độc lập — một vấn đề mà với ba người và một miền nghiệp vụ thì họ chưa từng có. Kết luận của tác giả: thiết kế quá mức là thất bại trong việc thu thập yêu cầu, kẻ thù chưa bao giờ là sự hoàn hảo mà là những yêu cầu mơ hồ.
Claude Is Not a Compiler
Đầu năm 2025, Josh Bleecher Snyder từng viết một bài mang tên “Is Claude a Compiler” và khi đó câu trả lời của anh là “tôi không biết”. Giờ anh khá chắc rằng câu trả lời là không — đó là một lỗi phân loại, vì thực ra nó tốt hơn một trình biên dịch. Lập luận của anh xuất phát từ cách phần mềm được xây theo từng lớp, mỗi lớp bổ sung thêm mức đặc tả và che đi những chi tiết “không cần thiết”: tầm nhìn thành chiến lược, kế hoạch sản phẩm thành kế hoạch lập trình, mã nguồn thành tệp nhị phân — với các vai trò khác nhau đảm nhiệm từng bước. Điều quan trọng là mỗi bước đều đòi hỏi rất nhiều quyết định, bởi vì tăng mức đặc tả nghĩa là quyết định. Trình biên dịch làm đúng việc đó ở lớp dưới cùng, và một trình biên dịch tốt giúp lập trình viên khỏi phải tự quyết những chuyện như nội tuyến hàm hay cấp phát thanh ghi. Nhưng cái nhìn phân lớp gọn gàng ấy không đúng với thực tế: các lớp trừu tượng luôn rò rỉ và cọ vào nhau. Tác giả dẫn chuyện xây tòa Empire State Building trong dưới một năm và không vượt ngân sách, một phần nhờ họ họp chung tất cả các lớp — chủ đầu tư, kiến trúc sư, nhà thầu phụ, thợ kim loại, người kiểm định — để quyết định về phần vỏ thép. Claude tốt hơn trình biên dịch chính vì nó làm việc được theo trục dọc: nó bàn được chiến lược, sản phẩm, kiến trúc, mã nguồn và cả mã máy, chưa giỏi bằng một chuyên gia ở từng việc nhưng làm được tất cả mà không cần xếp lịch họp hay xin phép ai.
Phần hay nhất của bài là ví dụ thực tế về hệ thống DNS của exe.dev. Ban đầu họ tự viết máy chủ DNS để tên miền của máy ảo khớp ngay với nguồn sự thật, nhưng khi thêm nhiều vùng thì DNS lại thành điểm nghẽn vì chỉ phục vụ từ Oregon, và mỗi lần triển khai lại gây mất dịch vụ ngắn. Họ cần một máy chủ DNS phân tán theo địa lý nhưng vẫn nhất quán hoàn toàn. Các quyết định chiến lược và kiến trúc lớn được chốt trực tiếp giữa người với người; phần còn lại tác giả giao cho nhiều vòng lặp agent chạy song song dựng trọn hệ thống kèm kiểm thử và rà soát phản biện. Điều gây bất ngờ là số lượng quyết định quan trọng mà các agent không hề hỏi mà tự quyết — và quyết khác nhau. Ví dụ với tình huống cơ sở dữ liệu bị lùi trạng thái, phá vỡ giao kèo chỉ-ghi-thêm: giải pháp anh chọn là thêm trường “timeline” sinh ngẫu nhiên cho mỗi dòng, khi giá trị không khớp thì biết lịch sử đã bị thay đổi và quay về đồng bộ lại từ đầu. Anh lặp lại toàn bộ quy trình phân tích đặc tả so sánh này ba lần, mỗi lần chắt lọc thành một tài liệu hướng dẫn ngắn gọn đủ để dẫn agent qua mọi quyết định quan trọng. Tổng cộng mất khoảng một tuần và anh đọc rất ít mã nguồn thật, nhưng khi trình bày với đồng nghiệp anh trả lời được mọi câu hỏi — và một tháng sau, số sự cố DNS là 0. Đó không phải vibe-coding mà là vibe-engineering, và theo anh trong tương lai gần thì vibe-engineering chỉ đơn giản là… engineering.
In defense of not understanding your codebase
Một kỹ sư phần mềm phải hiểu kho mã nguồn của chính mình tới mức nào? Sean Goedecke cho rằng câu trả lời phụ thuộc vào bạn thuộc nhóm nào. Những người làm việc với kho mã nhỏ và đội ngũ ít thay đổi — kiểu Redis hay game The Witness — sẽ nói rằng hiển nhiên phải hiểu hoàn toàn, nếu không thì không làm việc tốt được. Ngược lại, những người làm ở các kho mã khổng lồ với đội ngũ liên tục biến động — như phần lõi tìm kiếm của Google hay GitHub — sẽ nói rằng hiển nhiên là không thể hiểu hết, bạn chỉ cần làm tốt nhất có thể trong khu vực của mình. Đây là hai nền văn hóa lập trình khác nhau, nhưng nhóm thứ nhất lại chiếm ưu thế trong các cuộc thảo luận trên mạng, nên tác giả muốn bảo vệ nhóm thứ hai: trong nhiều môi trường thực tế, việc hiểu một phần là hoàn toàn bình thường, và với các hệ thống lớn thì đó đã là điều tốt nhất bạn có thể đạt được.
Đối tượng anh tranh luận là bài viết kinh điển “Programming as Theory Building” của Peter Naur, với luận điểm rằng sản phẩm thật của lập trình viên không phải mã nguồn mà là “lý thuyết về chương trình” trong đầu họ. Tác giả đồng ý tới đó, nhưng phản đối kết luận cực đoan của Naur rằng nên bỏ hẳn chương trình cũ và để đội mới làm lại từ đầu. Lý do thứ nhất: bạn đơn giản là không thể dựng lại một hệ thống lớn từ số không, vì nó chứa hàng nghìn tình huống kỳ quặc không thể tái hiện — mọi lần viết lại thành công đều bắt đầu bằng việc chia kho mã cũ thành từng khối nhỏ rồi làm lại từng khối một. Lý do thứ hai: các hệ thống bị bỏ hoang vẫn được hồi sinh liên tục, và bản thân anh đã nhiều lần nhận lấy một kho mã không còn ai hiểu, rồi từ từ dựng lại lý thuyết bằng cách đi hết một luồng xử lý từ đầu đến cuối rồi mở rộng dần. Anh cũng nhắc rằng khi Naur viết bài năm 1985, một “chương trình lớn” nghĩa là 200.000 dòng, còn hệ thống ngày nay lên tới hàng chục triệu dòng. Điểm cuối của bài: giữ được lý thuyết về kho mã chỉ là một giá trị trong nhiều giá trị, và nó vẫn bị đánh đổi mỗi khi bạn cho người khác viết mã trong kho của mình, phải làm tính năng do luật yêu cầu, phải nâng phiên bản để vá lỗ hổng, hay đưa thêm thư viện phụ thuộc vào. Ở chỗ làm, bạn được trả tiền để chấp nhận bộ giá trị kỹ thuật của công ty — chuyện này cũng giống như đôi khi phải viết mã chạy chậm để kịp hạn.
Beyond Happy Path Engineering: Time
Đây là một bài trong loạt bài về việc làm chương trình bền vững hơn khi rời khỏi “đường đi hạnh phúc” — điều kiện lý tưởng trên máy lập trình viên, nơi mạng yên tĩnh, cơ sở dữ liệu ở ngay bên, đồng hồ luôn chạy về phía trước và mọi yêu cầu đều thành công. Lần này chủ đề là thời gian. Thời gian trông như thứ phụ thuộc đơn giản nhất: bạn hỏi thời điểm hiện tại, so sánh với một thời điểm khác, rồi ra quyết định. Nhưng trên môi trường thật, các máy không đồng ý với nhau về thời gian hiện tại, việc hiệu chỉnh đồng hồ có thể đẩy thời gian tiến hoặc lùi giữa lúc đang xử lý, một khoảng thời lượng khác hoàn toàn với một ngày trên lịch, và một dấu thời gian không phải bằng chứng đáng tin rằng sự kiện này xảy ra trước sự kiện kia. Luận điểm chính của bài là thời gian không phải một công cụ duy nhất mà là bốn loại câu hỏi khác nhau: thời gian đồng hồ để ghi nhận một sự kiện xảy ra lúc nào, thời gian đơn điệu để đo thời lượng và đặt hạn chờ, thời gian logic để quyết định thứ tự giữa các cập nhật cạnh tranh, và thời gian nghiệp vụ để mang ý nghĩa của kỳ thanh toán, giờ mở cửa hay múi giờ của người dùng.
Tác giả dùng một ví dụ xuyên suốt: luồng thanh toán giữ hàng cho người dùng trong mười lăm phút. Đoạn mã if now() > reservation.expiresAt: release(...) nghe rất hợp lý, nhưng nó giả định rằng now() mang cùng ý nghĩa ở mọi nơi quyết định được thực thi. Nếu đồng hồ của tiến trình dọn dẹp nhanh hơn hai phút, nó sẽ giải phóng hàng trong khi người dùng vẫn đang trả tiền. Bài viết chỉ ra rằng hết hạn không phải một phép so sánh mà là một chuyển trạng thái: dấu thời gian chỉ nên dùng để lọc ra ứng viên, còn lệnh ghi phải có điều kiện — chỉ đặt trạng thái thành expired khi bản ghi vẫn còn là reserved và chưa có thanh toán nào hoàn tất — nhờ vậy một tiến trình chạy trễ hay chạy trùng cũng không gây hại. Tương tự, đo thời lượng bằng cách trừ hai dấu thời gian đồng hồ có thể cho kết quả âm khi đồng hồ bị hiệu chỉnh lùi, và chính lỗi đó đã gây sự cố DNS của Cloudflare vào giây nhuận đầu năm 2017: giá trị âm đi qua bước làm trơn số liệu, vào phần chọn resolver theo trọng số, rồi làm chương trình sập. Điểm đáng nhớ nhất của bài là lỗi thời gian lan đi rất âm thầm — dịch vụ vẫn sống, bảng điều khiển vẫn xanh, nhưng hệ thống đang ra những quyết định hơi sai, và mỗi triệu chứng đều có một lời giải thích cục bộ nghe rất hợp lý.
Everyone Should Know SIMD
SIMD nổi tiếng là phức tạp, và Mitchell Hashimoto nói rằng anh gặp rất nhiều kỹ sư giỏi gạt nó sang một bên vì cho rằng đó là thứ quá khó học hoặc chỉ là tối ưu ngách dành cho phần mềm đòi hỏi hiệu năng cực cao. Anh cho rằng suy nghĩ đó sai. Phần lớn mã nguồn SIMD kiểu “xử lý N giá trị mỗi lần” đều theo đúng một khuôn hình, và khi đã nắm được khuôn hình đó thì viết SIMD gần như dễ ngang viết một vòng lặp for — còn khi nó không dễ nữa thì thường đó là dấu hiệu tốt để tạm bỏ qua. Ý tưởng cơ bản là CPU có thể thao tác trên nhiều giá trị cùng lúc: thay vì so sánh từng byte một, nó so sánh 4, 8 hay nhiều byte hơn chỉ bằng một chỉ thị. Mỗi khi bạn thấy một vòng lặp duyệt qua từng byte, từng ký tự hay từng phần tử mảng, đó là một cơ hội dùng SIMD — với điều kiện lượng dữ liệu đủ lớn, cỡ hàng trăm nghìn hay hàng triệu byte, chứ vài chục byte thì không đáng.
Khuôn hình chung gồm năm bước: phát tán (broadcast) các hằng số cần thiết và khởi tạo bộ tích lũy nếu có; lặp qua đầu vào theo từng khối đúng bằng độ rộng vector; thực hiện phép so sánh hoặc số học song song trên mọi làn (lane); thu gọn hoặc lưu kết quả vector; và cuối cùng xử lý phần dư bằng một vòng lặp vô hướng — chính là vòng lặp bình thường ban đầu của bạn. Tác giả minh họa bằng một ví dụ thật từ Ghostty: tìm điểm kết thúc của chuỗi ký tự in được bằng cách quét tới khi gặp giá trị từ 0xF trở xuống. Bản vô hướng chỉ một dòng, bản vector thêm khoảng 12 dòng, nhưng cho thông lượng nhanh tới 4 lần với ARM NEON, 8 lần với AVX2 và 16 lần với AVX-512; đo thực tế đầu-cuối trên máy Intel AVX2 thì đạt khoảng 5 lần. Về câu hỏi tại sao trình biên dịch không tự làm, anh thừa nhận đôi khi nó làm được với các vòng lặp số học đơn giản, nên hãy luôn biên dịch bản vô hướng có tối ưu và xem kết quả trước đã. Nhưng tự động vector hóa vẫn là bài toán nghiên cứu hàng chục năm chưa giải xong, và quan trọng hơn: khi một vòng lặp đáng để bạn quan tâm tới mức tăng tốc 5 lần, bạn muốn việc vector hóa là tường minh và có thể dự đoán được, chứ không phải thứ có thể lặng lẽ biến mất chỉ vì một thay đổi mã nguồn không liên quan hay một lần nâng cấp trình biên dịch.
htop explained
Tác giả thú nhận rằng suốt một thời gian dài anh không thực sự hiểu những con số hiện trên htop. Anh từng nghĩ load average 1.0 trên máy hai nhân nghĩa là CPU đang dùng 50% — điều đó không đúng — và cũng không hiểu tại sao nó lại hiển thị 1.0. Vì vậy anh quyết định tra cứu mọi thứ rồi viết lại thành một bài giải thích rất dài, đi qua từng cột và từng khu vực của màn hình htop. Điểm hay của bài là anh không chỉ nêu định nghĩa mà luôn truy ngược về nguồn dữ liệu thật: uptime đọc từ tệp /proc/uptime với số thứ nhất là tổng số giây hệ thống đã chạy và số thứ hai là thời gian nhàn rỗi; load average đọc từ /proc/loadavg, trong đó ba cột đầu là tải trung bình của 1, 5 và 15 phút, cột thứ tư là số tiến trình đang chạy trên tổng số tiến trình, cột cuối là ID tiến trình vừa được cấp. Anh còn chỉ cách tự khám phá điều đó bằng strace để xem chương trình mở những tệp nào.
Một hiểu lầm quan trọng được làm rõ: con số tải không phải phần trăm CPU mà đơn giản là số tiến trình đang chạy hoặc chờ tới lượt chạy, cộng với các tiến trình ở trạng thái ngủ không thể ngắt (đang chờ đĩa hoặc mạng). Và nó cũng không phải trung bình cộng thông thường mà là trung bình động suy giảm theo hàm mũ — giá trị một phút thực ra gồm 63% tải của phút vừa rồi cộng 37% tải tích lũy từ lúc khởi động. Bài viết đi tiếp qua các trạng thái tiến trình (R đang chạy hoặc sẵn sàng chạy, S ngủ có thể ngắt, D ngủ không thể ngắt thường do IO, Z tiến trình đã kết thúc mà cha chưa thu hồi), rồi tới niceness và độ ưu tiên: NI là độ ưu tiên phía người dùng từ -20 tới 19 — tiến trình càng “nice” thì càng nhường CPU cho tiến trình khác — còn PRI là độ ưu tiên phía nhân mà bạn không đổi trực tiếp được, liên hệ với nhau qua công thức PR = 20 + NI. Phần về bộ nhớ cũng rất đáng đọc: VIRT tính cả những vùng đã xin mà chưa dùng và cả tệp được ánh xạ vào bộ nhớ nên thường không mấy hữu ích; RES là phần thực sự nằm trong bộ nhớ vật lý nhưng vẫn có thể bị tính trùng giữa các tiến trình do Linux dùng cơ chế sao chép khi ghi; còn MEM% chỉ là RES chia cho tổng RAM.
The Productivity-Experience Paradox
Đây là bài thứ hai trong loạt bài Annie Vella viết về những phát hiện thú vị nhất từ nghiên cứu thạc sĩ của cô về tác động của AI lên ngành phần mềm. Lần này chủ đề là: AI khiến kỹ sư cảm thấy làm được nhiều việc hơn, và tại sao cảm giác năng suất đó có thể đang che giấu một điều đáng lo. Nghiên cứu chạy từ tháng 10/2024 tới tháng 4/2025, theo dõi cùng một nhóm kỹ sư qua hai mốc thời gian cách nhau sáu tháng. Về năng suất, tin tốt rất ổn định: 84% người tham gia nói AI cải thiện năng suất ở cả hai mốc, và hơn ba phần tư cho đúng cùng một mức đánh giá tích cực cả hai lần. Nhưng trải nghiệm lập trình viên lại đi theo hướng ngược lại. Dùng khung DevEx với ba chiều — vòng phản hồi, tải nhận thức và trạng thái dòng chảy (flow) — cô chia người tham gia thành nhóm tích cực, tiêu cực và pha trộn. Nhóm tiêu cực tăng gần gấp đôi, từ 14% lên 27%, còn nhóm tích cực chỉ giữ được 37% số người ban đầu. Đáng chú ý là nhóm tiêu cực rất “dính”: không một ai đã rơi vào đó quay lại được mức hoàn toàn tích cực.
Phát hiện trung tâm là sự tách rời của hai thứ mà ngành vốn tin là luôn đi cùng nhau. Nếu so sánh các kỹ sư tại cùng một thời điểm thì đúng là ai có trải nghiệm tốt hơn cũng thấy mình năng suất hơn. Nhưng nếu theo dõi cùng một người qua sáu tháng, trải nghiệm tụt xuống mà năng suất không hề tụt theo — hệ số tương quan giữa thay đổi về flow và thay đổi về năng suất chỉ là 0,02, gần như bằng không. Điều này đáng lo vì lập luận để lãnh đạo đầu tư vào trải nghiệm lập trình viên xưa nay luôn là “cải thiện trải nghiệm thì năng suất sẽ theo sau”. Khi trải nghiệm xói mòn mà sản lượng vẫn ổn, không có gì buộc vấn đề phải lộ ra: bảng điều khiển vẫn đẹp nhưng con người thì không, và áp lực ẩn đó cuối cùng đi vào kiệt sức và nghỉ việc. Đi sâu vào ba chiều, flow là thứ tụt mạnh nhất (tỉ lệ đánh giá tệ hơn tăng từ 7% lên 20%), tải nhận thức xấu đi nhẹ, còn vòng phản hồi lại cải thiện — và tác giả ngờ rằng chính hai điều này liên quan tới nhau: AI trả lời sau vài giây nhưng mỗi câu trả lời là một lần chuyển ngữ cảnh, đọc, đánh giá, sửa rồi nhắc lại. Cô mượn khái niệm “junk flow” của Csikszentmihalyi để mô tả cảm giác giả này: nó giống flow thật lúc đầu nhưng dần thành thứ gây nghiện chứ không giúp bạn trưởng thành, giống như kéo cần máy đánh bạc. Và mượn triết gia Alasdair MacIntyre, cô kết luận rằng AI tuyệt vời với các “lợi ích ngoại tại” — tiền bạc, địa vị, sản lượng — nhưng có thể ăn mòn các “lợi ích nội tại” là kỹ năng, sự tinh thông và niềm vui của nghề.
Prefactoring: Clear the Way for Your New Feature
Bài viết ngắn này của Google Testing Blog mở đầu bằng một câu diễn giải từ Kent Beck: “trước hết hãy làm cho thay đổi trở nên dễ, rồi mới thực hiện thay đổi dễ đó”. Tình huống quen thuộc là bạn đang làm một tính năng mới nhưng mã nguồn hiện có vốn không được viết để đón những thay đổi sau này, nên cố nhồi tính năng vào thẳng sẽ rối rất nhanh — sửa chỗ này kéo theo chỗ kia, và chưa kịp nhận ra thì bạn đã lún sâu vào vài tệp mà ban đầu không hề định đụng tới. Prefactoring, viết tắt của “preparatory refactoring”, là thói quen sắp xếp lại mã nguồn hiện có cho phù hợp với thay đổi sắp tới trước khi thực sự viết tính năng mới, thay vì dọn dẹp như một việc phát sinh sau đó.
Lợi ích được nêu khá rõ ràng: tính năng mới lắp vào tự nhiên hơn vì cấu trúc đã sẵn sàng; việc rà soát nhanh hơn vì phần tái cấu trúc và phần tính năng nằm ở hai thay đổi riêng biệt; ít lỗi hơn vì việc dọn dẹp được tách khỏi phần logic chức năng; và quay lui an toàn hơn vì các thay đổi nhỏ, tập trung thì dễ hoàn tác. Ví dụ minh họa rất gọn: thay đổi thứ nhất chỉ trích xuất hàm get_display_name(user) để bỏ đoạn mã lặp ở trang hồ sơ và mẫu email; thay đổi thứ hai mới là tính năng thật, thêm hỗ trợ tên đệm bằng đúng một dòng sửa trong hàm đó. Bài cũng lưu ý rằng bạn có thể tách phần dọn dẹp ra thành một thay đổi nền ngay cả khi thay đổi hiện tại đang trong quá trình rà soát, và không phải việc dọn dẹp nào cũng cần prefactoring — nếu nó không chặn tính năng của bạn thì làm ở thay đổi sau cũng được, hoặc gộp luôn vào cùng thay đổi nếu đủ nhỏ.