Có những phần mềm được thiết kế, xây dựng và phát hành như một sản phẩm hoàn chỉnh. Nhưng cũng có những phần mềm lớn lên chậm rãi qua nhiều năm, hấp thụ thêm yêu cầu mới, developer mới, công nghệ mới và cả những giới hạn mới.
Ứng dụng mà tôi đang maintain thuộc nhóm thứ hai.
Hệ thống này đã hoạt động từ năm 2005. Trong khoảng thời gian đó, thế giới web đã thay đổi rất nhiều. PHP liên tục phát triển, frontend chuyển mình mạnh mẽ, API trở thành một phần quan trọng của hầu hết ứng dụng, AI bắt đầu xuất hiện trong các sản phẩm thực tế, còn quy trình deploy hiện đại dần thay thế những thao tác thủ công.
Tuy nhiên, ứng dụng này không bị rewrite hoàn toàn sau mỗi vài năm. Nó vẫn tiếp tục chạy, trong khi những lớp công nghệ mới được thêm vào bên trên nền tảng cũ. Nó đã đi qua quá trình chuyển đổi từ FTP sang GitHub Actions, từ procedural PHP sang object-oriented code, từ những trang được render ở server sang React.js, từ các request đồng bộ sang background queue, và gần đây là thêm cả AI-powered feature.
Maintain hệ thống này đôi khi giống như đang đọc lịch sử phát triển của web thông qua một codebase duy nhất.
Khởi đầu với PHP và FTP
Ở giai đoạn đầu, ứng dụng phản ánh khá rõ cách nhiều website được xây dựng vào thời điểm đó. PHP xử lý business logic và tạo ra nội dung HTML. Apache chịu trách nhiệm phục vụ ứng dụng. Các file .htaccess xử lý URL rewrite và những hành vi ở tầng web server. Còn việc deploy được thực hiện bằng FTP.
Khi đó chưa có một pipeline hiện đại tự động chạy sau mỗi commit. Developer chỉnh sửa file ở local rồi upload lên server. Thậm chí server đôi khi cũng trở thành một phần trong quá trình phát triển: đó là nơi code được kiểm tra, điều chỉnh và làm cho hoạt động đúng.
Cách làm này có thể không thanh lịch theo tiêu chuẩn ngày nay, nhưng nó phù hợp với hoàn cảnh lúc bấy giờ. Quan trọng hơn, hệ thống đã tạo ra giá trị cho người dùng và tiếp tục hoạt động.
Đây là điều cần nhớ khi nhìn vào một phần mềm cũ. Một codebase tồn tại hơn hai mươi năm không chỉ là tập hợp của những quyết định tồi. Nó cũng là bằng chứng cho thấy những quyết định đó, dù có giới hạn, đã giúp một doanh nghiệp vận hành trong thời gian rất dài.
Một codebase được xây dựng theo từng lớp
Khi ứng dụng trở nên phức tạp hơn, object-oriented programming dần được đưa vào bên cạnh những đoạn procedural code cũ. Vì vậy, hệ thống hình thành sự pha trộn giữa nhiều phong cách lập trình: một số phần vẫn theo cách tổ chức cũ, trong khi các tính năng mới được xây dựng bằng class và object.
Sự pha trộn này có thể khiến những developer thích một kiến trúc hoàn toàn nhất quán cảm thấy khó chịu. Nhưng đó cũng là kết quả tự nhiên của quá trình tiến hóa phần mềm lâu dài. Code mới không phải lúc nào cũng thay thế code cũ. Rất nhiều khi, nó phải cùng tồn tại với code cũ.
Frontend cũng phát triển theo cách tương tự. Ứng dụng không đột nhiên trở thành một single-page application hiện đại. JavaScript và React.js được đưa vào từng phần, tại những nơi chúng mang lại trải nghiệm tốt hơn hoặc giúp xây dựng một tính năng cụ thể dễ dàng hơn.
Kết quả không hoàn toàn “thuần kiến trúc”. Nhưng có lẽ chính điều đó khiến nó thú vị hơn. Mỗi phần của hệ thống phản ánh một giai đoạn, một ưu tiên và một khả năng kỹ thuật khác nhau.
Mỗi ngôn ngữ giải quyết một vấn đề khác nhau
Một trong những điểm thú vị nhất của hệ thống là PHP không phải ngôn ngữ duy nhất xuất hiện trong đó.
Java được thêm vào để render PDF. Bash script hỗ trợ các tác vụ trên server và những công việc chạy theo lịch. Về sau, Java tiếp tục được sử dụng cho việc gửi SMS phục vụ xác thực hai yếu tố. Rồi hệ thống được tích hợp thêm nhiều third-party API, mỗi API có phương thức xác thực, định dạng request, cách xử lý lỗi và yêu cầu vận hành riêng.
Điều này cho thấy các hệ thống thực tế hiếm khi chỉ được định nghĩa bởi một ngôn ngữ. Một ngôn ngữ thường được lựa chọn vì nó giải quyết tốt một vấn đề cụ thể ở một thời điểm cụ thể.
Mục tiêu không nhất thiết là loại bỏ mọi ngôn ngữ và chỉ giữ lại một ngôn ngữ duy nhất. Điều quan trọng hơn là hiểu các thành phần giao tiếp với nhau như thế nào, trách nhiệm nằm ở đâu và làm sao thay đổi một phần mà không vô tình phá hỏng phần khác.
AI xuất hiện trong một hệ thống được xây dựng trước thời đại AI
Một trong những class mới nhất được thêm vào hệ thống chỉ vài tháng trước là AI-powered report builder.
Tính năng này đại diện cho một giai đoạn rất khác trong lịch sử của ứng dụng. Hệ thống ban đầu được thiết kế từ rất lâu trước khi AI assistant và large language model trở thành những công cụ phổ biến. Thế nhưng cùng một ứng dụng đó cuối cùng cũng được mở rộng để sử dụng AI trong quy trình xây dựng report.
Đưa AI vào một ứng dụng legacy không đơn giản chỉ là gọi một model. Tính năng mới vẫn phải phù hợp với dữ liệu hiện có, quyền truy cập, logic report và kỳ vọng của người dùng. Nó cũng phải hoạt động trong một codebase chứa PHP cũ, database structure đã tồn tại nhiều năm và những production environment không thể nâng cấp ngay lập tức.
AI report builder vì thế trở thành một lớp mới trong lịch sử dài của ứng dụng. Nó không xóa bỏ hệ thống cũ. Nó bổ sung một cách tiếp cận mới cho những khả năng đã được xây dựng qua nhiều thế hệ code.
Sự tương phản giữa cũ và mới cũng trở nên rõ ràng hơn: một hệ thống bắt đầu với những trang PHP và FTP deployment giờ đây đã có AI-assisted functionality. Công nghệ thay đổi, nhưng trách nhiệm vẫn giữ nguyên: bổ sung khả năng hữu ích mà không làm mất đi sự ổn định của ứng dụng.
Từ deploy bằng tay đến GitHub Actions
Quy trình deploy cũng kể lại một phần lịch sử tiến hóa của ứng dụng.
Trước đây, deploy có nghĩa là kết nối tới server bằng FTP rồi upload file thủ công. Ngày nay, code được commit vào Git và deploy thông qua GitHub Actions theo một quy trình có kiểm soát.
Đây không chỉ là một sự tiện lợi. Quy trình mới giúp việc deploy có tính lặp lại, dễ quan sát và đáng tin cậy hơn. Mỗi lần deploy có thể gắn với một commit cụ thể. Workflow có thể được review. Cùng một tập hợp các bước được thực hiện nhất quán, thay vì phụ thuộc vào việc một developer có nhớ cần upload những file nào hay không.
Chuyển từ FTP sang CI/CD không biến ứng dụng cũ thành một ứng dụng hiện đại ngay lập tức. Những giới hạn của code, database và server vẫn còn đó. Nhưng nó tạo ra một cách an toàn và đáng tin cậy hơn để đưa thay đổi vào hệ thống.
Đây là một ví dụ rõ ràng về việc hiện đại hóa xung quanh một ứng dụng legacy: đôi khi cải tiến tốt nhất không phải là lập tức rewrite phần lõi, mà là cải thiện quy trình bao quanh nó.
Khi synchronous request không còn đủ sức
Trong nhiều năm, ứng dụng có thể xử lý công việc ngay trong một web request thông thường. Nhưng cách làm đó bắt đầu gặp vấn đề khi người dùng yêu cầu generate một bảng báo cáo nhiều dữ liệu, hoặc toàn bộ dữ liệu trên hệ thống sau 20 năm hoạt động.
Một report lớn có thể tiêu tốn rất nhiều memory và thời gian thực thi. Trình duyệt có thể bị treo, request có thể timeout hoặc process có thể thất bại trước khi người dùng nhận được file. Workflow ban đầu được thiết kế cho những tác vụ nhỏ hơn, nhưng dữ liệu ngày càng lớn cuối cùng đã bộc lộ giới hạn của nó.
Giải pháp là đưa background report queue vào hệ thống. Thay vì bắt trình duyệt phải chờ, ứng dụng tạo một job, xử lý job đó ở một tiến trình riêng, lưu kết quả và cho frontend kiểm tra trạng thái.
Đây là một pattern hiện đại được thêm vào một môi trường rất cũ. Nó không yêu cầu toàn bộ ứng dụng phải được xây dựng lại. Nhưng nó đòi hỏi phải phân chia trách nhiệm rõ ràng: web request tạo job, worker thực hiện công việc nặng, còn giao diện thông báo trạng thái cho người dùng.
Đó cũng là điều khiến việc maintain legacy system trở nên khó khăn. Ý tưởng có thể hiện đại, nhưng implementation vẫn phải chạy được với các phiên bản PHP cũ, database structure hiện tại, scheduled task và những production server không thể nâng cấp chỉ sau một đêm.
Maintain cũng là một cách bảo tồn
Khi thay đổi một ứng dụng mới, đôi khi chúng ta có thể thay thế một component hoặc tổ chức lại một phần lớn codebase. Nhưng trong một hệ thống đã chạy nhiều năm hay nói cách khác là đã trưởng thành, mỗi thay đổi đều mang theo nhiều lớp lịch sử.
Có thể tồn tại nhiều năm từ dữ liệu khách hàng, tài liệu, transaction, folder, integration và những giả định không được thể hiện rõ trong code. Một thao tác tưởng như chỉ là cleanup đơn giản cũng có thể ảnh hưởng đến nhiều phần khác của hệ thống.
Vì vậy, maintain không chỉ là làm cho code đẹp hơn. Đó còn là việc bảo toàn behavior và bảo vệ dữ liệu. Trước khi thay đổi một thứ gì, cần hiểu tại sao nó tồn tại, những phần nào đang phụ thuộc vào nó và đâu là cách migration an toàn nhất.
Đôi khi giải pháp đúng là một cuộc refactor toàn diện. Đôi khi đó là một compatibility layer nhỏ. Cũng có lúc, một worker hoặc một script riêng sẽ giải quyết được vấn đề mà không làm xáo trộn workflow hiện tại.
Câu trả lời phù hợp phụ thuộc vào context nhiều hơn là xu hướng công nghệ.
Hệ thống vẫn còn sống
Điều đáng chú ý nhất về ứng dụng này không phải là nó chứa PHP cũ, các rule .htaccess, Bash script, Java component, React.js, API integration, AI-powered functionality và background queue.
Điều đáng chú ý nhất là nó vẫn đang chạy.
Nó đã tiếp tục hoạt động qua nhiều thế hệ công nghệ. Nó đã đi qua quá trình chuyển đổi từ FTP sang GitHub Actions. Nó tiếp nhận những phong cách lập trình mới mà không lập tức vứt bỏ code cũ. Nó được bổ sung các service mới, AI và asynchronous processing trong khi vẫn phục vụ người dùng và bảo vệ dữ liệu hiện có.
Điều đó không có nghĩa hệ thống hoàn hảo. Nó vẫn có technical debt, những điểm không nhất quán và nhiều khu vực có thể được thiết kế lại. Nhưng bản thân việc tồn tại lâu dài cũng là một dạng thành công của kỹ thuật phần mềm. Giữ cho một hệ thống hữu ích trong hơn hai mươi năm đòi hỏi nhiều hơn việc viết code. Nó đòi hỏi khả năng thích nghi.
Những gì tôi học được từ codebase này
Làm việc với một ứng dụng PHP đã tồn tại lâu năm thay đổi cách tôi nhìn nhận về software engineering.
Framework hiện đại và clean architecture rất có giá trị, nhưng chúng chỉ là một phần của nghề. Software engineering trong thực tế còn bao gồm việc hiểu các giới hạn, tôn trọng lịch sử của hệ thống, giảm thiểu rủi ro và thực hiện những cải tiến đủ an toàn để tồn tại trong production.
Một legacy application không bị đóng băng trong quá khứ. Nếu nó vẫn được maintain, nó vẫn là một hệ thống sống. Code cũ đại diện cho những quyết định của các thế hệ trước, còn các lớp mới đại diện cho nỗ lực liên tục để giữ cho hệ thống hữu ích.
Software evolution hiếm khi đi theo một đường thẳng. Nó giống một thành phố hơn: những con đường cũ vẫn được giữ lại, các tòa nhà mới tiếp tục xuất hiện, cơ sở hạ tầng được sửa chữa, và nhiều thế hệ vẫn cùng sử dụng một nơi.
Sau hơn hai mươi năm, ứng dụng PHP này đã trở thành một lịch sử sống động của sự tiến hóa phần mềm. Và công việc của tôi không chỉ là bảo tồn quá khứ của nó, mà còn giúp nó tiếp tục bước vào tương lai.
