Vấn đề
Mỗi wholesaler một giao thức, một hệ mã khách sạn, một mô hình phòng và giá, một cách viết chính sách huỷ — nhưng tất cả phải trả lời chung một hợp đồng API.
Thêm hai chuyện: cùng một tích hợp phải bán lại dưới nhiều thương hiệu, mỗi thương hiệu có credential và cột mã khách sạn riêng; và hai thế hệ của stack khách sạn đang chạy song song trên production, chia trên hai repo, phần lớn supplier tồn tại ở cả hai.
Cách làm
Bốn quyết định gánh toàn bộ thiết kế. Quyết định nào cũng viết theo cùng một khung: lúc đó có gì trên bàn, đã chọn gì, và trả giá bằng gì.
Quyết định — mỗi supplier một module, chung một contract cố định.
Trên bàn: một code path dùng chung, rẽ nhánh theo từng tính năng của từng
supplier — lối mòn ai cũng đi khi wholesaler thứ hai xuất hiện, và cũng là cách
biến mỗi khác biệt giao thức thành thêm một cái if. Đã chọn: mỗi supplier là
một module với contract cố định, đăng ký bằng mã trong một registry duy
nhất. Trả giá: hành vi viết theo từng module, nên fix supplier là fix từng
module một — còn đổi chính cái contract thì đụng cả 31 module cùng lúc.
Quyết định — hai pattern tạo offer có tài liệu, không phải mỗi supplier một luồng riêng. Trên bàn: để mỗi wholesaler giữ nguyên mô hình phòng-giá của nó, mỗi bên bọc một luồng riêng. Đã chọn: mọi supplier được xếp vào một trong hai pattern đã viết thành tài liệu — pattern A khi supplier trả một token đặt được N phòng, pattern B khi mỗi phòng là một giá độc lập và danh sách offer là ma trận các tổ hợp hợp lệ. Trả giá: pattern B trả trước cho sự đều đặn — ma trận được dựng sẵn ngay vào danh sách offer thay vì để sau mới ráp; và supplier nào không khớp pattern nào là một buổi bàn thiết kế, không phải một bản vá nhanh.
Quyết định — trạng thái giữa các bước nằm trong id, không nằm trong session server.
Trên bàn: session phía server — thứ ai cũng với tới trước tiên, và cũng là thứ
hết hạn khi client chậm, lệch nhau giữa các instance. Đã chọn: offer id ghép có
version, encode source · contract · hotel · uuid, prebook decode ra là biết
dùng credential nào — không có gì để hết hạn, không có gì để lệch giữa các
instance. Trả giá: bản thân cái id thành một contract riêng — phải decode được
qua các version, và bước sau cần gì thì thứ đó phải nhét vừa vào id.
Quyết định — deadline tuyệt đối 30 giây thay vì chờ theo phần trăm tiến độ.
Search fan-out bằng hàng đợi: mỗi supplier một job có Redis lock, worker ghi vào
một hash chung kèm hash meta đánh dấu supplier nào xong; tầng HTTP gom lại và
trả phần trăm tiến độ. Trên bàn: trả phần trăm đó cho tới khi supplier cuối
cùng trả lời — tức là giao cho supplier chậm nhất quyền quyết định mọi lượt
search kết thúc lúc nào, và client thì poll mãi. Đã chọn: một deadline tuyệt
đối 30 giây đè lên phần trăm — quá hạn thì báo xong để client thôi poll. Trả
giá: supplier trả lời muộn đơn giản là vắng mặt trong result set lượt đó; offer
của nó bị bỏ khỏi lượt search đó chứ không được chờ.
Chính deadline đó biến mỗi module thành một failure domain riêng:
Quanh bốn quyết định là hai nước đi đỡ. Phần khác nhau giữa các thương hiệu bị đẩy xuống dữ liệu: một bảng setting thưa chỉ chứa key khác default, và một map từ mã supplier sang cột mã khách sạn tương ứng.
Còn migration thì không big-bang: dòng mới là dòng duy nhất viết code mới, dòng cũ read-only, chuyển từng supplier một — và quy tắc ghi rõ là fix supplier phải áp cả hai repo, vì sửa một bên sẽ để bên kia hỏng.
Kết quả
Mình chạy cả release train chứ không chỉ viết code — đưa nhánh integration dùng chung lên production trên cả 8 service, tức là chịu trách nhiệm cả chuyện cái gì được lên và lên lúc nào.
75 mã supplier hiện chạy trên 31 module nhờ alias, mirror giữa service search và service booking. Tính năng cross-cutting được ship ở cả năm gateway trong cùng một ngày.