Mời bạn thưởng thức Newsletter #132.
Perfection is not over-engineering
Câu “chúng ta không muốn làm cho hoàn hảo” xuất hiện trong các cuộc họp nhiều tới mức tác giả không đếm xuể. Anh hiểu sự dè chừng ấy vì thiết kế quá mức (over-engineering) từng làm nhiều đội kiệt sức, nhưng cho rằng cả ngành đang đánh đồng hai chuyện khác nhau. Thiết kế quá mức đơn giản là giải sai bài toán, thường với thiện chí và kèm một đống phức tạp phát sinh. Còn giải pháp hoàn hảo thì có thật, miễn là yêu cầu rõ ràng và mọi ràng buộc được đặt lên bàn: siết ràng buộc đủ chặt thì chỉ còn một lựa chọn, và nó hoàn hảo chính vì là thứ duy nhất vừa khít. Chọn serverless với Python để khỏi bước biên dịch có thể đúng với người này nhưng sai với người không biết Python hoặc cần ưu tiên hiệu năng.
Theo tác giả, mọi hệ thống đều là sản phẩm: thư viện, API hay công cụ nội bộ đều có người dùng với nhu cầu thật. Dấu hiệu rõ nhất của thiết kế quá mức là khi hỏi “tại sao lại xây như thế này?” mà câu trả lời không đứng vững. Ví dụ quen thuộc là đội ba người duy trì năm microservice dùng chung dữ liệu: khóa ngoại vốn do cơ sở dữ liệu bảo đảm thành chuỗi id rời rạc, dịch vụ này xóa bản ghi mà dịch vụ kia không hay biết. Đổi lại họ được triển khai độc lập, một vấn đề họ chưa từng gặp. Kết luận: đó là thất bại trong thu thập yêu cầu, và kẻ thù chưa bao giờ là sự hoàn hảo mà là yêu cầu mơ hồ.
Claude Is Not a Compiler
Josh Bleecher Snyder khẳng định Claude không phải trình biên dịch, vì nó còn tốt hơn thế. Theo cách nhìn lý tưởng, phần mềm được xây theo từng lớp: 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, mỗi bước là vô số quyết định do một vai trò khác nhau đảm nhận. Nhưng thực tế các lớp luôn rò rỉ và cọ vào nhau, và làm việc xuyên lớp mới tạo ra giá trị lớn, như cách tòa Empire State Building được dựng xong trong chưa đầy một năm nhờ những cuộc họp gom đủ mọi bên liên quan. Claude làm được việc đó theo chiều dọc: bàn chiến lược, kiến trúc, mã nguồn, mã máy mà không cần xếp lịch họp.
Ví dụ là máy chủ DNS phân tán của exe.dev. Các quyết định chiến lược và kiến trúc được chốt trực tiếp giữa người với người; phần còn lại anh 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ử, rồi nhờ agent khác so sánh các bản cài đặt. Điều gây sốc là rất nhiều quyết định quan trọng được agent tự đưa ra mà không hỏi, và mỗi agent quyết một kiểu, chẳng hạn cách xử lý khi cơ sở dữ liệu bị lùi trạng thái. Lặp quy trình này ba lần, anh chắt lọc được một tài liệu ngắn đủ dẫn agent qua mọi quyết định quan trọng. Sau khoảng một tuần, gần như không đọc mã, anh trả lời được mọi câu hỏi của đồng nghiệp và một tháng sau số sự cố DNS là 0. Đó là vibe-engineering, và theo anh sắp tới nó chỉ đơn giản là kỹ thuật phần mềm.
In defense of not understanding your codebase
Kỹ sư phải hiểu kho mã nguồn của mình tới đâu? Sean Goedecke cho rằng câu trả lời tùy vào môi trường. Người làm với kho mã nhỏ như Redis sẽ nói phải hiểu hết; người làm ở hệ thống khổng lồ như Google hay GitHub sẽ nói điều đó bất khả thi. Nhóm đầu áp đảo các cuộc thảo luận trên mạng, nên tác giả đứng ra bảo vệ nhóm sau: với hệ thống lớn, hiểu một phần không có gì sai, thậm chí là mức tốt nhất có thể đạt được. Ở quy mô đủ lớn, ai cũng làm việc với một hình dung chưa đúng hẳn, nên kỹ sư giỏi phải dám phỏng đoán có cơ sở.
Anh phản bác bài “Programming as Theory Building” của Peter Naur. Anh đồng ý rằng sản phẩm thật của lập trình viên là “lý thuyết về chương trình” trong đầu họ, nhưng bác bỏ kết luận rằng khi đội cũ ra đi thì nên bỏ mã cũ và làm lại từ đầu. Thứ nhất, hệ thống lớn chứa hàng nghìn tình huống kỳ quặc không thể tái hiện, và mọi lần viết lại thành công đều bắt đầu bằng việc chia nhỏ hệ thống cũ. Thứ hai, các kho mã bị bỏ hoang vẫn được hồi sinh thường xuyên: bắt đầu bằng việc hiểu trọn một luồng xử lý rồi mở rộng dần. Cuối cùng, giữ được lý thuyết chỉ là một giá trị trong nhiều giá trị, bị đánh đổi mỗi khi người khác cùng viết mã hay khi phải nâng phiên bản để vá lỗ hổng. Đi làm là nhận tiền để theo bộ giá trị của công ty, giống như đôi khi phải chấp nhận viết mã chậm cho kịp hạn.
Beyond Happy Path Engineering: the Network
Bài mở đầu loạt “Beyond Happy Path Engineering” của Gabor Koos, xem xét những giả định mà môi trường phát triển luôn chiều chuộng nhưng môi trường thật thì không, bắt đầu với mạng. Một lời gọi mạng trông như yêu cầu và phản hồi đơn giản, nhưng thực chất băng qua ranh giới sang một hệ thống ta không kiểm soát. Với ví dụ bước xác thực thanh toán khi đặt hàng, lỗi “sạch” như không mở được kết nối dễ xử lý hơn nhiều so với timeout sau khi đã gửi yêu cầu: thao tác có thể đã xong, đã hỏng hoặc chưa hề bắt đầu. Nguyên tắc then chốt là không nhận được phản hồi không có nghĩa là thao tác chưa xảy ra. Độ trễ cũng là một dạng lỗi: dịch vụ chậm vẫn giữ luồng và kết nối, giá trị trung bình che mất phần đuôi, và sự chậm chạp thường dẫn tới sập hệ thống.
Các kỹ thuật được đề xuất: coi timeout là quyết định thiết kế dựa trên ngân sách thời gian của mỗi yêu cầu; chỉ thử lại khi an toàn, thường nhờ khóa idempotency gửi kèm mọi lần thử, cộng backoff và jitter để các client không đồng loạt thử lại; truyền deadline và tín hiệu hủy xuống tầng dưới. Circuit breaker và load shedding bảo vệ hệ thống khi quá tải, còn graceful degradation tách việc bắt buộc như thanh toán khỏi việc có thể hoãn như gửi email, kèm trạng thái đơn hàng rõ ràng như “đang chờ”. Sự cố GitHub năm 2018, khi gián đoạn mạng chưa tới một phút gây suy giảm dịch vụ hơn một ngày, minh họa rõ cái giá của những giả định này.
Beyond Happy Path Engineering: Time
Phần thứ hai của loạt bài chuyển sang thời gian, thứ trông như phụ thuộc đơn giản nhất: lấy thời điểm hiện tại, so sánh, rồi quyết định. Ví dụ xuyên suốt là luồng thanh toán giữ hàng mười lăm phút với câu lệnh if now() > reservation.expiresAt. Nếu đồng hồ của tiến trình dọn dẹp nhanh hơn hai phút, hàng sẽ bị nhả ra khi người dùng còn đang trả tiền. Đồng hồ có thể bị chỉnh tiến hoặc lùi giữa chừng khiến thời lượng đo được bị âm, còn dấu thời gian thì không chứng minh được thứ tự sự kiện. Đáng sợ nhất là lỗi thời gian lan âm thầm: dịch vụ vẫn chạy, bảng điều khiển vẫn xanh, nhưng hệ thống đang ra những quyết định hơi sai.
Luận điểm chính là thời gian trả lời bốn loại câu hỏi khác nhau: thời gian đồng hồ ghi nhận sự kiện xảy ra lúc nào, thời gian đơn điệu để đo thời lượng và hạn chờ, thời gian logic để xếp thứ tự các cập nhật cạnh tranh, và thời gian nghiệp vụ gắn với kỳ thanh toán, giờ chốt đơn hay múi giờ người dùng. Hết hạn nên được coi là một chuyển trạng thái: dấu thời gian chỉ để lọc ứng viên, còn lệnh ghi có điều kiện chỉ chuyển sang expired khi bản ghi vẫn đang reserved, nhờ vậy tiến trình chạy trễ hay chạy trùng cũng vô hại. Khi thời gian không đủ để phân xử thì đối soát với nguồn chính thống như nhà cung cấp thanh toán. Sự cố DNS của Cloudflare dịp giây nhuận đầu năm 2017, khi phép trừ hai mốc time.Now() cho ra số âm và gây panic, là minh họa điển hình.
Beyond Happy Path Engineering: Databases
Phần thứ ba của loạt bài bàn về cơ sở dữ liệu, tầng thường được coi là đáng tin nhất. Ví dụ xuyên suốt là luồng thanh toán của một cửa hàng trực tuyến, chạy trơn tru khi kiểm thử cục bộ vì không có lưu lượng cạnh tranh. Môi trường thật thì khác: thanh toán, trang quản trị, tác vụ nền, báo cáo và migration cùng tranh nhau kết nối và khóa. Một truy vấn chậm có thể làm cạn pool kết nối, yêu cầu xếp hàng, người dùng thử lại, và cuối cùng “thanh toán sập” dù chẳng có gì hỏng hẳn. Hai yêu cầu có thể cùng thấy còn một sản phẩm và cùng bán nó; transaction không tự giải quyết được, vì quy tắc nghiệp vụ phải được thực thi tường minh. Ngoài ra còn bản sao đọc bị trễ, timeout không cho biết lệnh ghi đã commit hay chưa, deadlock, và migration chạy khi nhiều phiên bản ứng dụng cùng hoạt động.
Tác giả đề xuất bắt đầu từ bất biến cần giữ, như “không bán vượt tồn kho”, và thực thi nó ngay trong câu lệnh ghi, kèm ràng buộc trong cơ sở dữ liệu thay vì chỉ trong mã ứng dụng. Giữ transaction ngắn, không gọi mạng bên trong và chỉ thử lại lỗi tạm thời. Thao tác ghi cần idempotent, mỗi endpoint cần quy định mức độ “tươi” của dữ liệu đọc, migration nên chạy theo nhiều bước tương thích, và khôi phục phải được diễn tập trước. Sự cố của Atlassian năm 2022 và GitHub năm 2018 cho thấy khả năng khôi phục quan trọng không kém độ bền dữ liệu.
Beyond Happy Path Engineering: Storage
Phần thứ tư của loạt bài bàn về object storage, thứ trông đơn giản hơn cơ sở dữ liệu nhưng ẩn chứa một cái bẫy: cơ sở dữ liệu và kho đối tượng là hai hệ thống riêng biệt, không có transaction chung. Ví dụ là tính năng tải tài liệu lên: ghi tệp vào kho, thêm bản ghi vào cơ sở dữ liệu, rồi trả về URL. Nếu tệp đã lưu mà bản ghi thất bại sẽ sinh ra đối tượng mồ côi; ngược lại, bản ghi tồn tại mà tệp không có thì mọi lượt tải đều trả về 404. Multipart upload bỏ dở vẫn âm thầm tốn tiền, presigned URL vẫn hợp lệ dù bản ghi đã bị xóa, hai lần ghi vào cùng một khóa thì lần sau đè lần trước, và CDN tiếp tục phục vụ tệp cũ. Độ bền dữ liệu cũng không đồng nghĩa với khả năng khôi phục, vì lệnh xóa nhầm cũng được lưu “bền vững” y như vậy.
Tác giả đề xuất chọn thứ tự tải lên sao cho khi hỏng thì dễ sửa, theo dõi trạng thái tường minh (pending, confirmed, failed…) và chỉ phục vụ nội dung đã xác nhận. Khóa đối tượng nên dùng UUID hoặc băm nội dung thay vì tên tệp; tệp lớn nên tải thẳng lên kho qua presigned URL ngắn hạn; việc xóa đi theo trình tự xóa mềm bản ghi rồi mới xóa đối tượng, kèm job đối soát định kỳ. Vụ Code Spaces năm 2014, công ty đóng cửa sau khi kẻ tấn công xóa sạch cả bản sao lưu, cho thấy bản sao lưu phải nằm ngoài phạm vi của cùng một thông tin xác thực.
Everyone Should Know SIMD
SIMD mang tiếng phức tạp, và Mitchell Hashimoto gặp nhiều kỹ sư giỏi gạt nó đi vì nghĩ đó là thứ khó học hoặc chỉ dành cho phần mềm cần hiệu năng cực cao. Anh cho rằng nghĩ vậy là sai: phần lớn mã SIMD kiểu “xử lý N giá trị mỗi lần” đều theo cùng một khuôn, nắm được khuôn đó thì viết SIMD gần dễ như viết vòng lặp for. SIMD cho phép CPU xử lý nhiều giá trị bằng một chỉ thị, nên mỗi vòng lặp duyệt từng byte, từng ký tự hay từng phần tử đều là một cơ hội, miễn là dữ liệu đủ lớn, cỡ hàng nghìn tới hàng triệu byte.
Khuôn chung gồm năm bước: phát tán (broadcast) hằng số và khởi tạo bộ tích lũy nếu cần; lặp qua đầu vào theo từng khối bằng độ rộng vector; so sánh hoặc tính toán song song trên mọi làn (lane); thu gọn hoặc lưu kết quả; và xử lý phần dư bằng chính vòng lặp vô hướng ban đầu. Ví dụ lấy từ Ghostty, dùng Zig: tìm điểm kết thúc của chuỗi ký tự in được bằng cách quét tới giá trị 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 có thể nhanh gấp 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ế trên máy Intel AVX2 được khoảng 5 lần. Trình biên dịch đôi khi tự vector hóa được, nên hãy kiểm tra trước. Nhưng khi vòng lặp đáng để quan tâm tới mức tăng tốc 5 lần, tác giả muốn việc vector hóa tường minh và dự đoán được chứ không lặng lẽ biến mất sau một lần nâng cấp trình biên dịch.
htop explained
Tác giả thừa nhận suốt một thời gian dài không hiểu hết các con số trên htop, chẳng hạn từng nghĩ load average 1.0 trên máy hai nhân nghĩa là CPU bận 50%. Vì vậy anh tra cứu và viết lại tất cả thành một bài rất dài, đi qua từng khu vực của màn hình. Điểm quý là anh luôn truy về nguồn dữ liệu thật: uptime đọc từ /proc/uptime, load average đọc từ /proc/loadavg, và cách tự tìm ra điều đó là dùng strace để xem chương trình mở những tệp nào.
Load average không phải phần trăm CPU mà là số tiến trình đang chạy hoặc chờ chạy, cộng cả tiến trình ngủ không thể ngắt đang chờ đĩa hay mạng. Nó cũng là trung bình suy giảm theo hàm mũ: giá trị một phút gồm khoảng 63% tải của phút vừa qua và 37% phần tích lũy từ lúc khởi động. Bài giải thích tiếp các trạng thái tiến trình (R đang chạy, S ngủ có thể ngắt, D ngủ không thể ngắt, Z zombie đã kết thúc nhưng tiến trình cha chưa thu hồi), độ “nice” từ -20 tới 19 với quan hệ PR = 20 + NI, cùng các cột bộ nhớ: VIRT gồm cả vùng đã xin mà chưa dùng nên ít hữu ích, RES là phần nằm trong RAM thật nhưng có thể bị tính trùng do cơ chế sao chép khi ghi, còn MEM% là RES chia tổng RAM. Cuối bài, anh lần lượt xem từng tiến trình mặc định trên Ubuntu Server để quyết định cái nào có thể gỡ bỏ.
The Productivity-Experience Paradox
Đây là bài thứ hai Annie Vella viết từ nghiên cứu thạc sĩ về tác động của AI lên kỹ sư. Nghiên cứu theo dõi cùng một nhóm kỹ sư ở hai thời điểm cách nhau sáu tháng (10/2024 tới 4/2025). Năng suất là tin vui ổn định: 84% nói AI giúp họ làm được nhiều hơn ở cả hai lần khảo sát. Nhưng trải nghiệm, đo bằng khung DevEx gồm vòng phản hồi, tải nhận thức và trạng thái dòng chảy (flow), lại đi xuống: nhóm có trải nghiệm tiêu cực tăng từ 14% lên 27%, và không ai rơi vào nhóm tiêu cực quay lại được mức hoàn toàn tích cực.
Phát hiện trung tâm là hai thứ vốn được tin là đi cùng nhau đang tách rời: theo dõi cùng một người, trải nghiệm tụt mà năng suất không tụt, hệ số tương quan giữa thay đổi về flow và năng suất chỉ 0,02. Điều đó đáng lo vì lý do đầu tư vào trải nghiệm lập trình viên xưa nay là “trải nghiệm tốt thì năng suất sẽ theo sau”; khi sản lượng vẫn ổn, áp lực bị che giấu cho tới lúc thành kiệt sức. Flow giảm mạnh nhất (tỉ lệ đánh giá tệ hơn từ 7% lên 20%) trong khi vòng phản hồi lại cải thiện, và tác giả ngờ hai điều liên quan: mỗi câu trả lời nhanh của AI là một lần chuyển ngữ cảnh. Cô mượn khái niệm “junk flow” để mô tả cảm giác giống flow nhưng gây nghiện như máy đánh bạc, và kết luận rằng AI tuyệt vời cho lợi ích bên ngoài như sản lượng nhưng có thể bào mòn lợi ích nội tại là kỹ năng và niềm vui của nghề.
Prefactoring: Clear the Way for Your New Feature
Bài viết ngắn từ loạt “Tech on the Toilet” của Google mở đầu bằng lời 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ễ đó”. Khi làm tính năng mới trên mã nguồn vốn không được viết để đón thay đổi, cố nhồi thẳng vào sẽ rối rất nhanh: sửa chỗ này kéo theo chỗ kia, và bạn lún vào những tệp 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ã hiện có cho hợp với thay đổi sắp tới trước khi viết tính năng, thay vì dọn dẹp sau cùng.
Lợi ích gồm: tính năng mới lắp vào tự nhiên hơn, rà soát nhanh hơn vì tái cấu trúc và tính năng nằm ở hai thay đổi riêng, ít lỗi hơn vì dọn dẹp được tách khỏi logic chức năng, và quay lui an toàn hơn vì thay đổi nhỏ dễ hoàn tác. Ví dụ minh họa: thay đổi đầu 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 thêm hỗ trợ tên đệm bằng một dòng sửa trong hàm đó. Bạn cũng có thể tách phần dọn dẹp mà người rà soát gợi ý thành một thay đổi nền ngay khi thay đổi hiện tại đang được rà soát. Tuy vậy không phải việc dọn dẹp nào cũng cần làm trước: nếu nó không chặn tính năng thì để sau, hoặc gộp luôn nếu đủ nhỏ.
Bài viết đã được viết lại bởi Claude Code với Opus 5.5 vào ngày 27/09/2026.