Khám phá tư tưởng của John Ousterhout về thiết kế phần mềm thực tế, di sản Tcl, tranh luận Ousterhout vs Brooks, và cách độ phức tạp làm sản phẩm thất bại.

John Ousterhout là một nhà khoa học máy tính và kỹ sư có công việc bao phủ cả nghiên cứu và hệ thống thực tế. Ông tạo ra ngôn ngữ lập trình Tcl, góp phần định hình các hệ thống file hiện đại, và sau đó đúc kết nhiều thập kỷ kinh nghiệm thành một khẳng định đơn giản nhưng hơi khó chịu: độ phức tạp là kẻ thù chính của phần mềm.
Thông điệp này vẫn có giá trị vì hầu hết đội ngũ thất bại không phải do thiếu tính năng hay thiếu nỗ lực—mà là vì hệ thống (và tổ chức) trở nên khó hiểu, khó thay đổi và dễ bị phá vỡ. Độ phức tạp không chỉ làm chậm kỹ sư. Nó thấm vào quyết định sản phẩm, tự tin về lộ trình, niềm tin khách hàng, tần suất sự cố, và thậm chí tuyển dụng—vì việc onboard mất nhiều tháng.
Khung nhìn của Ousterhout mang tính thực dụng: khi một hệ thống tích tụ các trường hợp đặc biệt, ngoại lệ, phụ thuộc ẩn và các sửa chữa “chỉ lần này thôi”, chi phí không chỉ giới hạn trong mã nguồn. Toàn bộ sản phẩm trở nên đắt đỏ hơn để phát triển. Tính năng mất nhiều thời gian hơn, QA khó hơn, phát hành rủi ro hơn, và đội ngũ bắt đầu tránh các cải tiến vì chạm vào bất cứ thứ gì đều cảm thấy nguy hiểm.
Đây không phải lời kêu gọi cho sự tinh khiết học thuật. Đây là lời nhắc rằng mọi đường tắt đều có khoản lãi phải trả—và độ phức tạp là món nợ lãi suất cao nhất.
Để làm ý tưởng cụ thể (không chỉ mang tính truyền cảm hứng), ta sẽ nhìn thông điệp của Ousterhout qua ba góc:
Bài viết không chỉ dành cho những người mê ngôn ngữ. Nếu bạn xây sản phẩm, lãnh đạo đội, hoặc đưa ra đánh đổi lộ trình, bạn sẽ tìm thấy các cách hành động để phát hiện độ phức tạp sớm, ngăn nó trở thành thể chế hóa, và coi tính đơn giản là một ràng buộc hạng nhất—không phải điều tốt để có sau khi ra mắt.
Độ phức tạp không phải là “nhiều mã” hay “toán học khó”. Nó là khoảng cách giữa những gì bạn nghĩ hệ thống sẽ làm khi bạn thay đổi và những gì nó thực sự làm. Một hệ thống trở nên phức tạp khi các chỉnh sửa nhỏ cảm thấy rủi ro—vì bạn không thể dự đoán phạm vi ảnh hưởng.
Trong mã lành mạnh, bạn có thể trả lời: “Nếu ta thay đổi thứ này, còn gì có thể hỏng?” Độ phức tạp là thứ biến câu hỏi đó thành tốn kém.
Nó thường ẩn trong:
Đội ngũ cảm nhận độ phức tạp qua giao hàng chậm hơn (tốn nhiều thời gian điều tra), nhiều lỗi hơn (vì hành vi gây bất ngờ), và hệ thống mong manh (thay đổi cần phối hợp nhiều người và dịch vụ). Nó cũng làm nặng quá trình onboard: người mới không thể xây mô hình tâm lý, nên tránh chạm vào luồng cốt lõi.
Một số độ phức tạp là thiết yếu: quy tắc kinh doanh, yêu cầu tuân thủ, các trường hợp biên trong thế giới thực. Bạn không thể xóa chúng.
Nhưng nhiều thứ là ngẫu nhiên: API khó hiểu, logic trùng lặp, cờ “tạm thời” trở thành vĩnh viễn, và module rò rỉ chi tiết nội bộ. Đây là loại độ phức tạp được tạo bởi lựa chọn thiết kế—và là thứ bạn có thể giảm đều đặn.
Tcl bắt đầu với mục tiêu thực tế: làm cho việc tự động hoá phần mềm và mở rộng ứng dụng hiện có trở nên dễ dàng mà không phải viết lại. John Ousterhout thiết kế nó để các đội có thể thêm “khả năng lập trình vừa đủ” vào công cụ—rồi trao quyền đó cho người dùng, vận hành, QA, hoặc bất kỳ ai cần viết script workflow.
Tcl phổ biến hoá khái niệm ngôn ngữ glue: một lớp scripting nhỏ, linh hoạt nối các thành phần viết bằng ngôn ngữ thấp hơn, nhanh hơn. Thay vì nhồi mọi tính năng vào một monolith, bạn có thể export một tập lệnh lệnh, rồi kết hợp chúng thành hành vi mới.
Mô hình đó có ảnh hưởng vì nó khớp với cách công việc thực sự diễn ra. Người ta không chỉ xây sản phẩm; họ xây hệ thống build, harness test, công cụ admin, bộ chuyển đổi dữ liệu, và các tự động hoá một lần. Một lớp scripting nhẹ biến những nhiệm vụ đó từ “ghi ticket” thành “viết script.”
Tcl coi việc nhúng interpreter là vấn đề hàng đầu. Bạn có thể thả một interpreter vào app, export giao diện lệnh sạch, và ngay lập tức có cấu hình và vòng lặp phản hồi nhanh.
Mẫu này xuất hiện ngày nay trong hệ thống plugin, ngôn ngữ cấu hình, API mở rộng, và runtime scripting nhúng—dù cú pháp script không giống Tcl. Nó cũng củng cố thói quen thiết kế quan trọng: tách các nguyên thủy ổn định (khả năng lõi của host app) khỏi phần thành phần có thể thay đổi (script). Khi hoạt động tốt, công cụ tiến hóa nhanh hơn mà không làm lõi bất ổn.
Cú pháp của Tcl và mô hình “mọi thứ là chuỗi” có thể gây cảm giác không trực quan, và codebase Tcl lớn đôi khi khó suy luận nếu không có quy ước chặt chẽ. Khi hệ sinh thái mới hơn cung cấp thư viện chuẩn rộng hơn, tooling tốt hơn và cộng đồng lớn hơn, nhiều đội di chuyển tự nhiên.
Điều đó không xoá di sản của Tcl: nó giúp chuẩn hoá ý tưởng rằng khả năng mở rộng và tự động hoá không phải là phụ kiện—chúng là tính năng sản phẩm có thể giảm đáng kể độ phức tạp cho người dùng và người duy trì hệ thống.
Tcl xây quanh ý tưởng tưởng đơn giản nhưng nghiêm ngặt: giữ lõi nhỏ, làm cho khả năng kết hợp mạnh, và giữ script đủ dễ đọc để mọi người có thể làm việc chung mà không phải liên tục dịch.
Thay vì phát hành một bộ lớn các tính năng chuyên biệt, Tcl dựa vào một tập nguyên thủy gọn (chuỗi, lệnh, quy tắc đánh giá đơn giản) và mong người dùng kết hợp chúng.
Triết lý đó thúc đẩy nhà thiết kế hướng tới ít khái niệm hơn, được tái sử dụng trong nhiều ngữ cảnh. Bài học cho thiết kế sản phẩm và API rõ ràng: nếu bạn có thể giải quyết mười nhu cầu bằng hai hoặc ba khối xây dựng nhất quán, bạn thu nhỏ diện tích bề mặt mà mọi người phải học.
Cạm bẫy chính là tối ưu cho sự tiện lợi của người xây dựng. Một tính năng có thể dễ triển khai (copy option, thêm cờ đặc biệt, vá góc) đồng thời làm sản phẩm khó dùng hơn.
Tcl nhấn mạnh ngược lại: giữ mô hình tâm lý chặt, dù implementation phải làm nhiều việc phía sau. Khi bạn review đề xuất, hỏi: điều này có giảm số khái niệm người dùng phải nhớ không, hay nó thêm một ngoại lệ nữa?
Tối giản chỉ hữu ích khi nguyên thủy nhất quán. Nếu hai lệnh trông giống nhau nhưng hành xử khác trong các trường hợp biên, người dùng phải nhớ chi tiết vụn. Một tập công cụ nhỏ có thể trở thành “lưỡi sắc” khi quy tắc thay đổi tinh tế.
Hãy nghĩ về bếp: một con dao tốt, chảo và lò giúp bạn nấu nhiều món bằng cách kết hợp kỹ thuật. Một đồ dùng chuyên dụng thái bơ quả chỉ làm một việc—bán dễ, nhưng lấp ngăn kéo. Triết lý Tcl lập luận cho dao và chảo: công cụ chung kết hợp sạch, nên bạn không cần gadget mới cho mỗi công thức.
Năm 1986, Fred Brooks viết một bài luận có kết luận khiêu khích: không có một đột phá duy nhất—không có “viên đạn bạc”—sẽ khiến phát triển phần mềm nhanh hơn, rẻ hơn và đáng tin cậy hơn gấp bội trong một bước nhảy.
Ý ông không bảo tiến bộ là không thể. Ông muốn nói phần mềm là một phương tiện nơi ta có thể làm hầu như mọi thứ, và tự do đó mang theo một gánh nặng đặc biệt: chúng ta liên tục định nghĩa thứ đang xây khi đang xây nó. Công cụ tốt giúp, nhưng không xoá phần khó nhất của công việc.
Brooks chia độ phức tạp thành hai thùng:
Công cụ có thể nghiền phức tạp ngẫu nhiên. Hãy nghĩ đến lợi ích từ ngôn ngữ bậc cao hơn, version control, CI, container, cơ sở dữ liệu quản lý, và IDE tốt. Nhưng Brooks lập luận phức tạp thiết yếu chiếm ưu thế, và nó không biến mất chỉ vì tooling cải thiện.
Ngay cả với nền tảng hiện đại, đội vẫn tiêu phần lớn năng lượng để thương lượng yêu cầu, tích hợp hệ thống, xử lý ngoại lệ, và giữ hành vi nhất quán theo thời gian. Diện tích bề mặt có thể thay đổi (API cloud thay vì driver thiết bị), nhưng thách thức cốt lõi vẫn là: dịch nhu cầu con người thành hành vi chính xác và dễ duy trì.
Điều này tạo nên căng thẳng mà Ousterhout nhấn mạnh: nếu phức tạp thiết yếu không thể bị xoá, liệu thiết kế kỷ luật có giảm đáng kể phần nào đó lọt vào code—và vào đầu những người phát triển hàng ngày?
Mọi người đôi khi phóng đại thành một cuộc đấu giữa lạc quan và thực tế. Tốt hơn hãy đọc như hai kỹ sư giàu kinh nghiệm mô tả các phần khác nhau của cùng một vấn đề.
Lập luận “No Silver Bullet” của Brooks rằng không có phép màu xóa mọi khó khăn. Ousterhout không mâu thuẫn với điều đó.
Sự phản bác của ông cụ thể và thực dụng: đội thường coi phức tạp là điều tất yếu trong khi nhiều phần trong đó là tự gây ra.
Theo Ousterhout, thiết kế tốt có thể giảm phức tạp một cách đáng kể—không phải làm cho phần mềm “dễ” mà là làm cho nó ít gây nhầm lẫn khi thay đổi hơn. Đó là khẳng định quan trọng, vì sự bối rối là thứ biến công việc hàng ngày thành công việc chậm chạp.
Brooks nhấn mạnh độ khó thiết yếu: phần mềm phải mô hình hoá những thực tế lộn xộn, yêu cầu thay đổi, và các ngoại lệ tồn tại ngoài codebase. Dù có công cụ tốt, bạn không thể xoá chúng—chỉ quản lý.
Họ trùng nhiều hơn là khác:
Đừng hỏi “Ai đúng?” mà hỏi: Phức tạp nào chúng ta có thể kiểm soát trong quý này?
Đội không thể kiểm soát thay đổi thị trường hay độ khó cốt lõi của miền. Nhưng họ có thể kiểm soát việc liệu tính năng mới có thêm trường hợp đặc biệt, API có bắt caller nhớ quy tắc ẩn, và module có che được phức tạp hay làm rò rỉ nó.
Đó là khoảng giữa hành động: chấp nhận phức tạp thiết yếu, và chọn lọc không khoan nhượng với phức tạp ngẫu nhiên.
Một deep module là một thành phần làm nhiều việc, đồng thời phơi bày một giao diện nhỏ, dễ hiểu. “Độ sâu” là lượng phức tạp module này gánh thay bạn: caller không cần biết chi tiết rối rắm, và giao diện không bắt họ phải biết.
Một shallow module trái lại: có thể bọc logic nhỏ, nhưng đẩy phức tạp ra ngoài—qua nhiều tham số, cờ đặc biệt, thứ tự gọi bắt buộc, hoặc quy tắc “bạn phải nhớ…”
Hãy tưởng tượng nhà hàng. Deep module là bếp: bạn gọi “pasta” từ menu đơn giản và không quan tâm nhà cung cấp, thời gian luộc, hay trình bày.\
Shallow module là “bếp” đưa bạn nguyên liệu thô kèm hướng dẫn 12 bước và yêu cầu bạn mang chảo. Công việc vẫn xảy ra—nhưng nó chuyển cho khách.
Lớp thêm có thể tốt nếu chúng gộp nhiều quyết định thành một lựa chọn rõ ràng.
Ví dụ, một lớp lưu trữ cung cấp save(order) và xử lý retry, serialization, indexing phía trong là deep.
Lớp gây hại khi chỉ đổi tên hay thêm tuỳ chọn. Nếu abstraction mới introduce nhiều cấu hình hơn là nó loại bỏ—ví dụ save(order, format, retries, timeout, mode, legacyMode)—thì có thể là shallow. Mã trông “gọn” nhưng tải nhận thức xuất hiện tại mỗi call site.
useCache, skipValidation, force, legacy.\Deep modules không chỉ “bao đóng mã.” Chúng bao đóng quyết định.
API “tốt” không chỉ là cái có thể làm nhiều việc. Nó là cái người có thể giữ trong đầu khi làm việc.
Lăng kính thiết kế của Ousterhout bắt bạn đánh giá API theo nỗ lực tinh thần nó đòi hỏi: bao nhiêu quy tắc phải nhớ, bao nhiêu ngoại lệ phải dự đoán, và dễ gây sai lầm thế nào.
API thân người dùng thường nhỏ, nhất quán, và khó bị dùng sai.
Nhỏ không có nghĩa thiếu sức mạnh—mà là diện tích bề mặt tập trung vào vài khái niệm dễ kết hợp. Nhất quán nghĩa cùng mẫu hoạt động khắp hệ thống (tham số, xử lý lỗi, đặt tên, kiểu trả về). Khó bị dùng sai nghĩa API hướng bạn vào con đường an toàn: bất biến rõ ràng, kiểm tra biên, và mặc định khiến lỗi xảy ra sớm.
Mỗi cờ, chế độ, hay cấu hình “phòng khi cần” trở thành thuế trên tất cả người dùng. Dù chỉ 5% caller cần, 100% caller phải biết nó tồn tại, tự hỏi có cần không, và diễn giải hành vi khi nó tương tác với các tuỳ chọn khác.
Đó là cách API tích tụ phức tạp ẩn: không ở một cuộc gọi duy nhất, mà ở các kết hợp.
Mặc định là lòng tốt: cho phép hầu hết caller bỏ qua quyết định nhưng vẫn có hành vi hợp lý. Quy ước (một cách rõ ràng để làm) giảm nhánh trong tâm trí người dùng. Đặt tên cũng quan trọng: chọn động từ và danh từ phù hợp ý định người dùng, và giữ tên tương tự cho các thao tác tương tự.
Một nhắc nhớ: API nội bộ quan trọng ngang API công khai. Phần lớn độ phức tạp sản phẩm nằm đằng sau—ranh dịch vụ, thư viện chia sẻ, và module “trợ giúp”. Đối xử với những giao diện đó như sản phẩm, có review và kỷ luật phiên bản.
Phức tạp hiếm khi đến dưới dạng một “quyết định tệ” duy nhất. Nó tích tụ qua các bản vá nhỏ trông hợp lý—đặc biệt khi đội chịu áp lực hạn chót và mục tiêu trước mắt là giao hàng.
Một bẫy là cờ tính năng khắp nơi. Cờ hữu ích cho rollout an toàn, nhưng khi tồn tại lâu, mỗi cờ nhân lên số hành vi có thể. Kỹ sư bắt đầu suy nghĩ về “hệ thống, trừ khi cờ A bật và người dùng ở phân khúc B.”
Một bẫy nữa là logic trường hợp đặc biệt: “Khách enterprise cần X”, “Trừ khi ở vùng Y”, “Trừ khi tài khoản trên 90 ngày.” Những ngoại lệ này thường trải khắp codebase, và sau vài tháng không ai biết còn cần hay không.
Thứ ba là abstraction rò rỉ. Một API bắt caller hiểu chi tiết nội bộ (thời gian, định dạng lưu trữ, quy tắc cache) đẩy phức tạp ra ngoài. Thay vì một module chịu gánh, mọi caller học quirks.
Lập trình chiến thuật tối ưu cho tuần này: sửa nhanh, thay đổi tối thiểu, “vá rồi gửi.”
Lập trình chiến lược tối ưu cho năm tới: chỉnh sửa nhỏ ngăn chặn cùng lớp bug và giảm công việc tương lai.
Nguy cơ là “lãi bảo trì.” Một cách giải nhanh có vẻ rẻ giờ, nhưng bạn trả lại với lãi: onboard chậm, phát hành mong manh, và phát triển theo nỗi sợ nơi không ai muốn động vào mã cũ.
Thêm các prompt nhẹ vào code review: “Điều này có thêm trường hợp đặc biệt mới không?” “API có che được chi tiết này không?” “Chúng ta bỏ lại phức tạp gì phía sau?”
Giữ hồ sơ quyết định ngắn cho các đánh đổi không tầm thường (vài gạch đầu dòng là đủ). Và dành một ngân sách refactor nhỏ vào mỗi sprint để sửa chiến lược không bị xem là việc ngoài lề.
Độ phức tạp không bị giam trong engineering. Nó rò rỉ vào tiến độ, độ tin cậy, và cách khách hàng trải nghiệm sản phẩm.
Khi hệ thống khó hiểu, mọi thay đổi mất nhiều thời gian hơn. Thời gian ra thị trường trượt vì mỗi phát hành cần phối hợp nhiều, test hồi quy nhiều hơn, và nhiều vòng review “cho an toàn.”
Độ tin cậy cũng kém hơn. Hệ thống phức tạp tạo ra tương tác mà không ai dự đoán đủ, nên lỗi xuất hiện ở các trường hợp biên: checkout chỉ lỗi khi coupon, giỏ lưu, và luật thuế vùng kết hợp theo cách đặc biệt. Những lỗi này khó tái tạo và chậm sửa nhất.
Onboarding trở thành lực kéo ẩn. Người mới không xây được mô hình hữu ích, nên tránh chạm vào vùng rủi ro, sao chép mẫu họ không hiểu, và vô tình thêm phức tạp.
Khách hàng không quan tâm hành vi xuất phát từ “trường hợp đặc biệt” trong code. Họ cảm nhận nó như sự không nhất quán: cài đặt không áp dụng mọi nơi, luồng thay đổi tuỳ theo cách bạn đến, tính năng hoạt động “đa số thời gian”.
Niềm tin giảm, churn tăng, và việc chấp nhận sản phẩm bị đình trệ.
Support trả giá bằng ticket dài hơn và nhiều trao đổi hơn để thu thập bối cảnh. Ops trả bằng nhiều alert, nhiều runbook, và triển khai cẩn trọng hơn. Mọi ngoại lệ trở thành điều phải giám sát, tài liệu hoá và giải thích.
Hãy tưởng tượng yêu cầu “thêm luật thông báo nữa.” Thêm có vẻ nhanh, nhưng nó introduce một nhánh hành vi nữa, nhiều copy UI, thêm test case, và nhiều cách người dùng cấu hình sai.
So sánh với việc đơn giản hoá luồng thông báo hiện có: ít loại rule hơn, mặc định rõ hơn, và hành vi nhất quán giữa web và mobile. Bạn có thể phát hành ít nút hơn, nhưng giảm bất ngờ—làm sản phẩm dễ dùng hơn, dễ hỗ trợ hơn, và tiến hoá nhanh hơn.
Đối xử với độ phức tạp như performance hoặc security: lập kế hoạch, đo lường và bảo vệ nó. Nếu bạn chỉ nhận ra phức tạp khi giao hàng chậm, bạn đã bắt đầu trả lãi.
Bên cạnh scope tính năng, định nghĩa bao nhiêu phức tạp mới một release được phép thêm. Ngân sách có thể đơn giản: “không thêm khái niệm mới trừ khi ta loại bỏ một cái,” hoặc “tích hợp mới phải thay thế đường dẫn cũ.”
Làm rõ những đánh đổi khi lập kế hoạch: nếu một tính năng cần ba chế độ cấu hình mới và hai ngoại lệ, nó nên “tốn” nhiều hơn so với tính năng phù hợp khái niệm hiện có.
Bạn không cần con số hoàn hảo—chỉ các tín hiệu đi đúng hướng:
Theo dõi theo release, và gắn với quyết định: “Chúng ta thêm hai tuỳ chọn công khai; đã loại bỏ hay đơn giản hoá gì để bù lại?”
Prototype thường bị đánh giá bằng “Xây được không?” Thay vào đó, dùng chúng để trả lời: “Cái này có cảm giác đơn giản khi dùng và khó dùng sai không?”
Hãy để người không quen với tính năng thử nhiệm vụ thực tế với prototype. Đo thời gian hoàn thành, câu hỏi họ hỏi, và chỗ họ hiểu sai. Đó là điểm nóng độ phức tạp.
Đây cũng là nơi các workflow hiện đại có thể giảm phức tạp ngẫu nhiên—nếu chúng giữ vòng lặp ngắn và dễ quay lui. Ví dụ, khi đội dùng nền tảng vibe-coding như Koder.ai để phác thảo công cụ nội bộ hoặc luồng mới qua chat, các tính năng như planning mode (làm rõ ý định trước khi sinh) và snapshots/rollback (hoàn tác thay đổi nhanh) có thể khiến thử nghiệm ban đầu an toàn hơn—không phải cam kết đống abstraction nửa vời. Nếu prototype trưởng thành, bạn vẫn có thể xuất source code và áp dụng nguyên tắc “deep module” và kỷ luật API ở trên.
Complexity is the gap between what you expect will happen when you change the system and what actually happens.
You feel it when small edits seem risky because you can’t predict the blast radius (tests, services, configs, customers, or edge cases you might break).
Look for signals that reasoning is expensive:
Essential complexity comes from the domain (regulations, real-world edge cases, core business rules). You can’t remove it—only model it well.
Accidental complexity is self-inflicted (leaky abstractions, duplicated logic, too many modes/flags, unclear APIs). This is the part teams can reliably reduce through design and simplification work.
A deep module does a lot while exposing a small, stable interface. It “absorbs” messy details (retries, formats, ordering, invariants) so callers don’t have to.
A practical test: if most callers can use the module correctly without knowing internal rules, it’s deep; if callers must memorize rules and sequences, it’s shallow.
Common symptoms:
legacy, skipValidation, force, mode).Prefer APIs that are:
When you’re tempted to add “just one more option,” first ask whether you can redesign the interface so most callers don’t need to think about that choice at all.
Use feature flags for controlled rollout, then treat them as debt with an end date:
Long-lived flags multiply the number of “systems” engineers must reason about.
Make complexity explicit in planning, not just in code review:
The goal is to force tradeoffs into the open before complexity becomes institutionalized.
Tactical programming optimizes for this week: quick patches, minimal change, “ship it.”
Strategic programming optimizes for the next year: small redesigns that remove recurring classes of bugs and reduce future work.
A useful heuristic: if a fix requires caller knowledge (“remember to call X first” or “set this flag in prod only”), you probably need a more strategic change to hide that complexity inside the module.
Tcl’s lasting lesson is the power of a small set of primitives plus strong composition—often as an embedded “glue” layer.
Modern equivalents include:
The design goal is the same: keep the core simple and stable, and let change happen through clean interfaces.
Shallow modules often look organized but move complexity outward to every caller.