Cái nhìn dễ tiếp cận về tư tưởng của Rich Hickey: đơn giản, bất biến và các mặc định tốt hơn—bài học thực tế để xây hệ thống phức tạp bình tĩnh và an toàn hơn.

Phần mềm hiếm khi trở nên phức tạp ngay lập tức. Nó đến dần qua từng quyết định “hợp lý”: một cache nhanh để kịp deadline, một đối tượng mutable chia sẻ để tránh sao chép, một ngoại lệ vì “cái này đặc biệt”. Mỗi lựa chọn trông nhỏ, nhưng khi ghép lại chúng tạo ra hệ thống nơi thay đổi trở nên rủi ro, bug khó tái tạo, và việc thêm tính năng mất nhiều thời gian hơn xây nó.
Độ phức tạp thắng vì nó mang lại sự thoải mái ngắn hạn. Thường nhanh hơn khi thêm một dependency mới hơn là đơn giản hóa một cái đã có. Dễ hơn khi vá trạng thái hơn là hỏi tại sao trạng thái được phân tán qua năm dịch vụ. Và người ta thường dựa vào quy ước và kiến thức nội bộ khi hệ thống lớn nhanh hơn tài liệu.
Đây không phải là tutorial Clojure, và bạn không cần biết Clojure để nhận giá trị từ nó. Mục tiêu là mượn một loạt ý tưởng thực tế thường gắn với tác phẩm của Rich Hickey—những ý tưởng bạn có thể áp dụng cho quyết định kỹ thuật hàng ngày, bất kể ngôn ngữ.
Phần lớn độ phức tạp không đến từ code bạn viết có chủ đích; nó đến từ những gì công cụ làm dễ theo mặc định. Nếu mặc định là “đối tượng mutable ở khắp nơi”, bạn sẽ kết thúc với coupling ẩn. Nếu mặc định là “trạng thái sống trong bộ nhớ”, bạn sẽ gặp khó khi debug và truy vết. Mặc định hình thành thói quen, và thói quen hình thành hệ thống.
Chúng ta sẽ tập trung vào ba chủ đề:
Những ý tưởng này không loại bỏ độ phức tạp khỏi miền bài toán, nhưng chúng có thể ngăn phần mềm của bạn nhân lên độ phức tạp đó.
Rich Hickey là một lập trình viên và nhà thiết kế phần mềm lâu năm, nổi tiếng vì tạo ra Clojure và những bài nói thách thức các thói quen lập trình phổ biến. Ông không chạy theo xu hướng—mà chú tâm đến những lý do lặp lại khiến hệ thống khó thay đổi, khó suy nghĩ, và khó tin cậy khi chúng lớn lên.
Clojure là một ngôn ngữ hiện đại chạy trên các nền tảng phổ biến như JVM (runtime của Java) và JavaScript. Nó được thiết kế để làm việc với hệ sinh thái sẵn có trong khi khuyến khích một phong cách cụ thể: biểu diễn thông tin như dữ liệu thuần, ưu tiên giá trị không thay đổi, và tách rời “những gì đã xảy ra” khỏi “những gì hiển thị trên màn hình”.
Bạn có thể nghĩ về nó như một ngôn ngữ khuyến khích bạn dùng các khối xây dựng rõ ràng hơn và tránh các hiệu ứng phụ ẩn.
Clojure không được tạo ra để làm các script nhỏ ngắn hơn. Nó nhắm đến những nỗi đau lặp lại trong dự án:
Mặc định của Clojure đẩy theo hướng ít bộ phận chuyển động hơn: cấu trúc dữ liệu ổn định, cập nhật rõ ràng, và công cụ làm cho việc phối hợp an toàn hơn.
Giá trị không chỉ nằm ở việc đổi ngôn ngữ. Những ý tưởng cốt lõi của Hickey—đơn giản hóa bằng cách loại bỏ phụ thuộc không cần thiết, coi dữ liệu như các sự thật bền vững, và giảm thiểu trạng thái mutable—có thể cải thiện hệ thống trong Java, Python, JavaScript và hơn thế nữa.
Rich Hickey phân biệt rõ ràng giữa đơn giản và dễ—và đó là ranh giới nhiều dự án đi qua mà không nhận ra.
Dễ là về cảm nhận ngay lúc đó. Đơn giản là về có bao nhiêu phần và chúng bị rối với nhau đến mức nào.
Trong phần mềm, “dễ” thường nghĩa là “nhanh để gõ hôm nay”, trong khi “đơn giản” nghĩa là “khó vỡ vào tháng tới”.
Nhóm thường chọn lối tắt giảm ma sát tức thì nhưng thêm cấu trúc vô hình phải bảo trì:
Mỗi lựa chọn có thể trông nhanh, nhưng nó tăng số phần chuyển động, trường hợp đặc biệt, và phụ thuộc chéo. Đó là cách hệ thống trở nên mong manh mà không có một sai lầm lớn duy nhất.
Ra tính năng nhanh có thể tốt—nhưng tốc độ mà không đơn giản hóa thường nghĩa là bạn đang vay nợ cho tương lai. Lãi suất xuất hiện dưới dạng bug khó tái tạo, onboarding kéo dài, và thay đổi đòi hỏi “phối hợp cẩn thận”.
Hỏi các câu này khi review thiết kế hoặc PR:
“Trạng thái” đơn giản là những thứ trong hệ thống có thể thay đổi: giỏ hàng người dùng, số dư tài khoản, cấu hình hiện tại, bước đang thực hiện trong workflow. Điểm khó là không phải thay đổi tồn tại—mà là mỗi thay đổi tạo cơ hội cho mọi thứ không khớp.
Khi người ta nói “trạng thái gây lỗi”, họ thường có ý: nếu cùng một thông tin có thể khác nhau theo thời điểm (hoặc nơi), mã của bạn phải liên tục trả lời: “Phiên bản nào là thật ngay bây giờ?” Trả lời sai gây lỗi thấy như ngẫu nhiên.
Mutability nghĩa là một đối tượng bị sửa tại chỗ: “cùng” thứ trở nên khác theo thời gian. Nghe có vẻ hiệu quả, nhưng làm cho việc lập luận khó hơn vì bạn không thể tin những gì đã thấy một lát trước.
Ví dụ dễ hiểu là một bảng tính chia sẻ. Nếu nhiều người có thể sửa cùng ô cùng lúc, hiểu biết của bạn bị vô hiệu ngay: tổng đổi, công thức hỏng, hoặc một hàng biến mất vì ai đó tổ chức lại. Dù không ai cố ý, tính “chia sẻ, có thể chỉnh sửa” là nguồn nhầm lẫn.
Trạng thái phần mềm hành xử tương tự. Nếu hai phần của hệ thống đọc cùng một giá trị mutable, một phần có thể im lặng thay đổi nó trong khi phần kia tiếp tục với giả định lỗi thời.
Trạng thái mutable biến việc debug thành khảo cổ. Báo cáo lỗi hiếm khi nói “dữ liệu bị thay đổi sai lúc 10:14:03.” Bạn chỉ thấy kết quả cuối: số sai, trạng thái bất ngờ, request đôi khi thất bại.
Vì trạng thái thay đổi theo thời gian, câu hỏi quan trọng nhất trở thành: chuỗi chỉnh sửa nào dẫn đến đây? Nếu bạn không thể dựng lại lịch sử đó, hành vi trở nên không thể đoán:
Đây là lý do Hickey coi trạng thái như nhân tố tăng: khi dữ liệu vừa được chia sẻ và mutable, số các tương tác có thể xảy ra tăng nhanh hơn khả năng bạn theo dõi chúng.
Bất biến đơn giản nghĩa là dữ liệu không đổi sau khi được tạo. Thay vì chỉnh sửa một thông tin tồn tại tại chỗ, bạn tạo ra một thông tin mới phản ánh cập nhật.
Hãy nghĩ về một hóa đơn: khi in ra, bạn không xóa dòng rồi ghi lại tổng. Nếu có thay đổi, bạn phát hành hóa đơn điều chỉnh. Hóa đơn cũ vẫn tồn tại, và hóa đơn mới rõ ràng là “phiên bản mới nhất”.
Khi dữ liệu không thể bị sửa lén, bạn không phải lo lắng về việc có những chỉnh sửa vô hình. Điều đó làm cho việc lý luận hàng ngày dễ hơn nhiều:
Đây là phần lớn lý do Hickey nói về đơn giản: ít hiệu ứng phụ ẩn nghĩa là ít nhánh tinh thần phải theo dõi.
Tạo phiên bản mới nghe có vẻ tốn, cho đến khi bạn so sánh với phương án khác. Sửa tại chỗ khiến bạn phải hỏi: “Ai đã thay đổi? Khi nào? Trước đó ra sao?” Với dữ liệu bất biến, thay đổi rõ ràng: một phiên bản mới tồn tại và phiên bản cũ vẫn có để debug, audit, hoặc rollback.
Clojure ủng hộ điều này bằng cách làm cho việc coi cập nhật như tạo giá trị mới trở nên tự nhiên.
Bất biến không miễn phí. Bạn có thể tạo nhiều đối tượng hơn, và nhóm quen với “chỉ cập nhật cái đó” có thể cần thời gian thích nghi. Tin tốt là các thực thi hiện đại thường chia sẻ cấu trúc ở mức độ dưới nắp để giảm chi phí bộ nhớ, và lợi ích thường là hệ thống bình tĩnh hơn với ít sự cố khó giải thích.
Concurrency chỉ là “nhiều thứ xảy ra cùng lúc”. Một app web xử lý hàng ngàn request, một hệ thống thanh toán cập nhật số dư trong khi sinh hóa đơn, hay app di động đồng bộ nền—tất cả đều là concurrency.
Điểm khó không phải nhiều thứ cùng xảy ra, mà là chúng thường chạm vào cùng dữ liệu.
Khi hai worker đều đọc rồi sửa cùng một giá trị, kết quả cuối cùng có thể phụ thuộc vào thời điểm. Đó là race condition: lỗi khó tái tạo, xuất hiện khi hệ thống bận.
Ví dụ: hai request cố cập nhật tổng đơn hàng.
Không có gì “crash”, nhưng bạn mất cập nhật. Khi tải tăng, những cửa sổ thời gian này phổ biến hơn.
Cách truyền thống—khóa, synchronized—hoạt động, nhưng buộc mọi người phải phối hợp. Phối hợp tốn kém: làm chậm throughput và trở nên mong manh khi codebase lớn.
Với dữ liệu bất biến, một giá trị không bị sửa tại chỗ. Thay vào đó bạn tạo giá trị mới biểu diễn thay đổi.
Sự thay đổi nhỏ này loại bỏ cả một loại vấn đề:
Bất biến không làm concurrency miễn phí—bạn vẫn cần quy tắc về phiên bản nào là hiện tại. Nhưng nó làm các chương trình đồng thời trở nên dễ dự đoán hơn, vì bản thân dữ liệu không còn là mục tiêu chuyển động. Khi lưu lượng tăng hoặc job nền dồn đống, bạn ít gặp lỗi phụ thuộc thời điểm bí ẩn.
“Mặc định tốt hơn” nghĩa là lựa chọn an toàn xảy ra tự động, và bạn chỉ chịu rủi ro thêm khi bạn chủ động tắt nó đi.
Nghe có vẻ nhỏ, nhưng mặc định hướng dẫn người ta viết gì vào một sáng thứ Hai, người review chấp nhận gì vào chiều thứ Sáu, và người mới học được gì từ codebase đầu tiên họ chạm.
“Mặc định tốt hơn” không phải quyết định hộ bạn mọi thứ. Nó là khiến con đường phổ biến ít lỗi hơn.
Ví dụ:
Không thứ nào trong số này loại bỏ độ phức tạp, nhưng chúng ngăn nó lan rộng.
Nhóm không chỉ theo tài liệu—they theo cách code “muốn” bạn làm.
Khi mutate trạng thái chia sẻ dễ, nó trở thành lối tắt bình thường, và reviewer phải tranh luận về ý định: “Điều này an toàn ở đây không?” Khi bất biến và hàm thuần là mặc định, reviewer có thể tập trung vào logic và tính đúng đắn, vì các động thái rủi ro nổi bật.
Nói cách khác, mặc định tốt tạo chuẩn cơ bản khỏe mạnh: hầu hết thay đổi có vẻ nhất quán, và mẫu bất thường đủ rõ để bị đặt câu hỏi.
Bảo trì lâu dài chủ yếu là đọc và thay đổi code hiện có một cách an toàn.
Mặc định tốt giúp người mới lên tay vì ít quy tắc ẩn (“cẩn thận, hàm này bí mật cập nhật cái bản đồ global kia”). Hệ thống dễ lý giải hơn, giảm chi phí cho mọi tính năng, sửa lỗi, và refactor tương lai.
Một chuyển đổi tư duy hữu ích trong các bài nói của Hickey là tách sự thật (điều đã xảy ra) khỏi view (những gì ta hiện tin là đúng). Hầu hết hệ thống trộn chúng bằng cách chỉ lưu giá trị mới nhất—ghi đè hôm qua bằng hôm nay—và điều đó làm thời gian biến mất.
Một fact là bản ghi bất biến: “Đơn #4821 được đặt lúc 10:14”, “Thanh toán thành công”, “Địa chỉ thay đổi”. Những thứ này không bị sửa; bạn thêm fact mới khi thực tế thay đổi.
Một view là những gì app cần bây giờ: “Địa chỉ giao hàng hiện tại là gì?” hoặc “Số dư khách hàng là bao nhiêu?” Views có thể được tính lại từ facts, cache, index hoặc materialize để tăng tốc.
Khi bạn giữ facts, bạn có:
Ghi đè bản ghi giống như cập nhật ô bảng tính: bạn chỉ thấy số mới nhất.
Một log append-only giống sổ ghi séc: mỗi mục là một fact, và “số dư hiện tại” là view tính từ các mục.
Bạn không cần chuyển sang kiến trúc event-sourced đầy đủ để hưởng lợi. Nhiều nhóm bắt đầu nhỏ: giữ bảng audit append-only cho thay đổi quan trọng, lưu các “sự kiện thay đổi” cho một vài workflow rủi ro cao, hoặc giữ snapshot cộng với cửa sổ lịch sử ngắn. Cốt lõi là thói quen: coi facts như bền vững, và trạng thái hiện tại như projection tiện lợi.
Một trong những ý tưởng thực tế nhất của Hickey là data-first: coi thông tin hệ thống như giá trị thuần (facts), và coi hành vi là thứ bạn chạy trên những giá trị đó.
Dữ liệu bền. Nếu bạn lưu thông tin rõ ràng, tự chứa, bạn có thể diễn giải lại sau, chuyển giữa dịch vụ, reindex, audit, hoặc cấp cho tính năng mới. Hành vi kém bền hơn—code thay đổi, giả định thay đổi, dependency thay đổi.
Khi bạn trộn chúng, hệ thống trở nên dính: bạn không thể tái dùng dữ liệu mà không kéo theo hành vi đã tạo nó.
Tách facts khỏi actions giảm coupling vì các thành phần có thể đồng thuận về hình dạng dữ liệu mà không cần đồng thuận về đường dẫn code dùng chung.
Một job báo cáo, công cụ hỗ trợ, và dịch vụ thanh toán có thể tiêu thụ cùng dữ liệu đơn hàng, mỗi cái áp logic riêng. Nếu bạn nhúng logic trong đại diện lưu trữ, mọi consumer trở nên phụ thuộc vào logic đó—và thay đổi nó trở thành rủi ro.
Dữ liệu sạch (dễ tiến hóa):
{
"type": "discount",
"code": "WELCOME10",
"percent": 10,
"valid_until": "2026-01-31"
}
Tiểu-chương-trình trong storage (khó tiến hóa):
{
"type": "discount",
"rule": "if (customer.orders == 0) return total * 0.9; else return total;"
}
Phiên bản thứ hai trông linh hoạt, nhưng đẩy độ phức tạp vào layer dữ liệu: bạn cần một evaluator an toàn, quy tắc versioning, rào bảo mật, công cụ debug, và kế hoạch migration khi ngôn ngữ quy tắc thay đổi.
Khi thông tin lưu giữ đơn giản và rõ ràng, bạn có thể thay đổi hành vi theo thời gian mà không viết lại lịch sử. Bản ghi cũ vẫn đọc được. Dịch vụ mới có thể được thêm vào mà không phải “hiểu” quy tắc thực thi cũ. Và bạn có thể giới thiệu cách diễn giải mới—giao diện UI mới, chiến lược giá mới, phân tích mới—bằng cách viết code mới, không bằng cách mutate ý nghĩa dữ liệu.
Hầu hết hệ thống doanh nghiệp không hỏng vì một module “xấu”. Chúng hỏng vì mọi thứ liên kết với nhau.
Coupling chặt xuất hiện như thay đổi “nhỏ” nhưng yêu cầu test lại cả tuần. Một trường mới thêm vào service này phá ba consumer downstream. Schema DB chia sẻ trở thành nút thắt phối hợp. Một cache mutable hoặc đối tượng singleton “config” lén trở thành dependency của nửa codebase.
Thay đổi dây chuyền là kết quả tự nhiên: khi nhiều phần cùng chia sẻ một thứ thay đổi, bán kính ảnh hưởng mở rộng. Nhóm phản ứng bằng cách thêm quy trình, thêm quy tắc, và nhiều bàn giao—thường làm chậm việc giao hàng hơn.
Bạn có thể áp dụng ý tưởng của Hickey mà không đổi ngôn ngữ hay viết lại toàn bộ:
Khi dữ liệu không bị thay đổi dưới chân bạn, bạn tốn ít thời gian debug “nó vào trạng thái này bằng cách nào?” và nhiều thời gian để lý luận code làm gì.
Mặc định là nơi bất nhất lẻn vào: mỗi nhóm tự tạo format timestamp riêng, shape lỗi riêng, chính sách retry riêng, và cách tiếp cận concurrency riêng.
Mặc định tốt trông như: schema sự kiện versioned, DTO bất biến chuẩn, quyền sở hữu ghi rõ ràng, và một tập thư viện được chấp nhận cho serialization, validation, và tracing. Kết quả là ít tích hợp bất ngờ và ít fix một-off.
Bắt đầu nơi thay đổi đã diễn ra:
Cách này cải thiện độ tin cậy và phối hợp nhóm trong khi giữ hệ thống chạy—và giữ scope nhỏ đủ để hoàn thành.
Dễ áp dụng những ý tưởng này hơn khi workflow của bạn hỗ trợ lặp nhanh, rủi ro thấp. Ví dụ, nếu bạn xây tính năng mới trong Koder.ai (một nền tảng vibe-coding dựa trên chat cho web, backend, và mobile), hai tính năng map trực tiếp tới tư duy “mặc định tốt hơn”:
Dù stack bạn là React + Go + PostgreSQL (hoặc Flutter cho mobile), điểm cốt lõi vẫn vậy: công cụ bạn dùng hàng ngày dạy một cách làm việc mặc định. Chọn công cụ khiến truy vết, rollback và lập kế hoạch rõ ràng trở thành thường quy có thể giảm áp lực “chỉ vá tạm” ở phút chót.
Đơn giản và bất biến là mặc định mạnh mẽ, không phải quy tắc đạo đức. Chúng giảm số thứ có thể bất ngờ thay đổi, giúp khi hệ thống lớn. Nhưng dự án thực có ngân sách, deadline và ràng buộc—và đôi khi mutable là công cụ đúng.
Mutability có thể là lựa chọn thực tế ở điểm nóng hiệu năng (vòng lặp chặt, parsing throughput cao, đồ họa, tính toán số). Nó cũng ổn khi phạm vi được kiểm soát: biến cục bộ, cache riêng tư ẩn sau interface, hoặc component đơn luồng với ranh giới rõ ràng.
Quy tắc then chốt là cô lập. Nếu “đối tượng mutable” không bao giờ lọt ra, nó không thể lan rộng độ phức tạp sang codebase.
Ngay cả trong phong cách chủ yếu là hàm, nhóm vẫn cần quyền sở hữu rõ ràng:
Đây là nơi thiên kiến của Clojure về dữ liệu và ranh giới rõ ràng hữu dụng, nhưng kỷ luật là kiến trúc hơn là ngôn ngữ.
Không ngôn ngữ nào sửa được yêu cầu kém, mô hình miền mơ hồ, hoặc team không thống nhất khái niệm “xong”. Bất biến không làm workflow khó hiểu trở nên rõ ràng, và code “functional” vẫn có thể mã hóa sai luật nghiệp vụ—chỉ là trình bày gọn hơn.
Nếu hệ thống đã production, đừng xem những ý tưởng này như rewrite tất-cả-hay-không. Tìm bước nhỏ nhất giảm rủi ro:
Mục tiêu không phải là tinh khiết—mà là ít bất ngờ hơn cho mỗi thay đổi.
Đây là checklist cỡ sprint bạn có thể áp dụng mà không đổi ngôn ngữ, framework, hay cấu trúc nhóm.
Tìm tài liệu về sự khác biệt giữa simplicity và ease, quản lý trạng thái, thiết kế hướng giá trị, bất biến, và cách “lịch sử” (facts theo thời gian) giúp gỡ lỗi và vận hành.
Đơn giản không phải là tính năng dán vào—nó là chiến lược bạn thực hành qua những lựa chọn nhỏ, lặp lại.
Độ phức tạp tích tụ qua những quyết định nhỏ, hợp lý lúc đó (cờ phụ, cache, ngoại lệ, hàm trợ giúp dùng chung) tạo ra chế độ và sự phụ thuộc.
Một tín hiệu rõ ràng là khi một “thay đổi nhỏ” đòi hỏi chỉnh sửa phối hợp trên nhiều module hoặc service, hoặc khi người review phải dựa vào kiến thức nội bộ để đánh giá tính an toàn.
Bởi vì các lối tắt tối ưu cho ma sát hôm nay (thời gian ra mắt) trong khi đẩy chi phí sang tương lai: thời gian gỡ lỗi, chi phí phối hợp, và rủi ro khi thay đổi.
Một thói quen hữu ích là hỏi trong thiết kế/PR: “Việc này tạo ra những bộ phận chuyển động hay trường hợp đặc biệt nào mới, và ai sẽ duy trì chúng?”
Các mặc định định hình hành vi của kỹ sư khi áp lực. Nếu mutable là mặc định, trạng thái chia sẻ sẽ lan rộng. Nếu “lưu trong bộ nhớ là đủ” là mặc định, khả năng truy vết sẽ biến mất.
Cải thiện mặc định bằng cách khiến con đường an toàn là con đường ít kháng cự nhất: dữ liệu bất biến ở ranh giới, timezone/null/retry rõ ràng, và quyền sở hữu trạng thái được xác định.
Trạng thái là bất cứ thứ gì thay đổi theo thời gian. Vấn đề là mỗi thay đổi tạo cơ hội xảy ra bất đồng: hai thành phần có thể giữ các giá trị “hiện tại” khác nhau.
Lỗi xuất hiện dưới dạng hành vi phụ thuộc thời gian ("chạy trên máy tôi được", lỗi lặt vặt ở production) vì câu hỏi trở thành: chúng ta đã hành động trên phiên bản dữ liệu nào?
Bất biến nghĩa là bạn không sửa một giá trị tại chỗ; thay vào đó bạn tạo giá trị mới mô tả cập nhật.
Thực tế, điều này giúp:
Không phải lúc nào cũng. Mutability có thể là lựa chọn tốt khi nó được cô lập:
Quy tắc then chốt: đừng để cấu trúc mutable lọt ra ngoài biên giới mà nhiều phần có thể đọc/ghi.
Các điều kiện chạy đua thường đến từ dữ liệu chia sẻ, có thể thay đổi mà nhiều bên đọc rồi ghi.
Bất biến giảm bề mặt cần phối hợp vì writer tạo các phiên bản mới thay vì sửa cùng một đối tượng. Bạn vẫn cần cơ chế để công bố phiên bản hiện tại, nhưng dữ liệu tự nó không còn là mục tiêu di chuyển.
Đối xử với facts như bản ghi append-only của những gì đã xảy ra, và dùng view làm dự kiến trạng thái hiện tại từ những facts đó.
Bắt đầu nhỏ mà không cần event sourcing đầy đủ:
Lưu thông tin dưới dạng dữ liệu rõ ràng, thuần túy (giá trị), và chạy hành vi trên chúng. Tránh nhúng quy tắc thực thi bên trong bản ghi lưu trữ.
Điều này làm hệ thống dễ tiến hóa vì:
Chọn một workflow thay đổi nhiều và áp dụng ba bước:
Đo lường thành công bằng ít lỗi lặt vặt hơn, bán kính ảnh hưởng nhỏ hơn cho mỗi thay đổi, và ít cần phối hợp cẩn thận khi release.