Nút rollback là một phần của feature, không phải bước sau
Tôi từng coi rollback là việc tính sau khi ship. Một sáng Chủ nhật ở Remolution dạy tôi rằng nếu không nghĩ về nó trước, sẽ không kịp nghĩ.
Buổi sáng Chủ nhật ở Remolution
Cuối 2023. Tôi vừa ship một feature nhỏ ở Remolution — cập nhật cách map dữ liệu ứng viên giữa hệ thống của khách hàng và pipeline tuyển dụng của họ. Code review qua, staging xanh, deploy lúc thứ Sáu chiều vì "chỉ là chỉnh cách hiển thị mấy field". Tôi đóng máy đi ăn lẩu với vợ.
Sáng Chủ nhật, một khách hàng enterprise ở Singapore mở ticket priority cao. Tên ứng viên của họ bị ghép sai với hồ sơ — không phải bug hiển thị, mà là dữ liệu thật bị viết đè trong DB qua một background job mà tôi quên flag lại. Họ có khoảng vài trăm bản ghi đã bị xáo trộn. Tôi mở máy, tay lạnh, và đi tìm cái nút rollback.
Không có nút rollback.
Có git revert cho code. Có snapshot DB hằng đêm — nhưng snapshot chứa cả dữ liệu mới mà khách hàng đã nhập sau khi bug chạy, nên restore thẳng tức là xoá luôn việc tuyển dụng cuối tuần của họ. Cái tôi cần là một cách đảo ngược chỉ phần ghi sai của job đó. Tôi không có. Phải tự viết script SQL ngay tại chỗ, đối chiếu từng dòng với audit log, rồi gọi cho khách hàng giải thích vì sao họ nên đợi thêm bốn tiếng.
Bốn tiếng đó là bốn tiếng tôi đáng lẽ phải dành ra hai tuần trước — lúc tôi viết feature.
Cái tôi đã hiểu sai về rollback
Trước hôm đó, trong đầu tôi rollback là một bước hậu kỳ. Một việc "nếu có sự cố thì sẽ tính" — kiểu như backup, kiểu như monitoring. Một thứ ở ngoài feature, do platform lo.
Sai. Rollback là một đường đi mà feature mở ra trong dữ liệu. Khi tôi viết một migration thêm cột, đường rollback là drop cột. Khi tôi viết một job ghi đè bản ghi, đường rollback là khả năng phục hồi giá trị cũ — nghĩa là job đó phải lưu lại giá trị cũ ở đâu đó trước khi ghi đè. Khi tôi gửi một email, đường rollback là... không có, vì email đã bay đi rồi — nên feature phải có một cổng dừng trước khi gửi, không phải sau.
Nếu tôi không thiết kế con đường rollback đó vào lúc viết feature, thì sau này nó không tự nhiên mọc ra. Nó phải được dựng lại từ con số không, dưới áp lực thời gian, với khách hàng đang chờ trên điện thoại.
Rollback không phải việc tôi làm sau khi feature hỏng. Nó là việc tôi đã hoặc chưa làm khi viết feature.
Đổi cách tôi viết spec
Sau cú đó, tôi thêm một mục cố định vào mọi spec mình viết — kể cả spec một dòng cho task nhỏ. Mục đó tên là Đường lùi, và nó luôn đứng trước phần mô tả happy path, không phải sau.
Story
Tôi học được rằng nếu phần "nếu hỏng thì làm sao" nằm cuối spec, nó sẽ bị cắt khi deadline siết. Đặt nó lên đầu khiến tôi không thể giả vờ rằng feature đã xong khi phần đó còn trống.
Đường lùi trả lời ba câu hỏi cụ thể:
- Feature này ghi vào đâu? (DB row nào, file nào, external API nào, queue nào)
- Nếu một ghi sai, tôi đảo ngược nó bằng cách nào? (script có sẵn, undo endpoint, soft-delete flag, audit log đủ để reconstruct)
- Tôi biết phải đảo ngược trong bao lâu? (5 phút sau khi merge, hay 3 ngày sau khi customer phát hiện)
Nếu một trong ba câu trên không có câu trả lời cụ thể, feature chưa xong — kể cả khi tests xanh và Figma đã match.
Rollback không chỉ là git revert
Cái thứ hai tôi hiểu ra là git revert chỉ đảo ngược code, không đảo ngược trạng thái mà code đã tạo ra trong thế giới thực. Sau khi feature chạy được vài giờ trong production, thế giới thực đã không còn giống lúc bạn deploy. Có data mới đã được ghi với schema mới. Có email đã được gửi. Có webhook đã được nhận bởi hệ thống bên thứ ba.
Tôi bắt đầu chia rollback thành ba lớp khi review code của chính mình:
Lớp 1 — Code
Đây là lớp dễ nhất. git revert hoặc redeploy version trước. Nếu code được viết kiểu backward-compatible (cột mới mặc định nullable, feature flag bao quanh nhánh mới), việc này tốn dưới một phút.
Lớp 2 — Dữ liệu
Đây là lớp Chủ nhật-ở-Remolution. Code revert được, nhưng những gì code đã viết vào DB vẫn ở đó. Để lớp này khả thi, cần có ít nhất một trong: audit log đầy đủ trước khi ghi đè, soft-delete thay vì hard-delete, migration được viết kèm script rollback thật sự (không phải comment // TODO: rollback).
Lớp 3 — Side-effect ra ngoài
Email đã gửi, payment đã capture, webhook đã fire. Lớp này hầu như không rollback được — bạn chỉ có thể bù trừ: gửi email đính chính, refund, gọi compensating API. Vì vậy với side-effect, đường lùi là một cổng dừng trước khi side-effect xảy ra — confirmation step, dry-run mode, một hàng đợi có thể pause được. Bạn không sửa được sau, bạn phải làm chậm trước.
Câu hỏi tôi hỏi trước khi merge
Bây giờ trước khi merge bất cứ PR nào của mình, tôi hỏi một câu duy nhất: Nếu phát hiện bug nghiêm trọng 15 phút sau khi cái này lên production, tôi sẽ làm gì — bằng những lệnh cụ thể nào, dùng những file cụ thể nào?
Nếu câu trả lời là "tôi sẽ figure out lúc đó", PR chưa sẵn sàng merge. Không phải vì code sai, mà vì tôi chưa hoàn thành feature — tôi mới hoàn thành nửa happy path.
Cái nửa kia, sau Chủ nhật ở Singapore, không bao giờ là việc tính sau với tôi nữa.