Mỗi lần mình mô tả công việc của mình, câu đáp lại hay gặp nhất là một phiên bản lịch sự của "tức là gọi API của người khác hả". Đúng về mặt kỹ thuật, theo kiểu "compiler là chương trình đọc file text" cũng đúng về mặt kỹ thuật. Mình muốn viết ra hình dạng thật của việc này, vì chính cái hình dạng đó là lập luận.
Con số trước đã
Nền tảng mình đang làm gom supplier du lịch trên năm dòng sản phẩm — hotel, flight, tour, transfer, car hire. Khoảng 150 supplier code chạy trên chừng 90 tích hợp khác nhau; riêng mảng hotel, 75 code ngồi trên 31 module. Khoảng cách giữa hai con số đó là white-label: một tích hợp được bán lại dưới nhiều brand code, phần khác nhau nằm trong config chứ không fork code.
Nếu làm tích hợp là "gọi API" thì mấy con số kia là số lần gọi API. Không phải. Chúng là số quan điểm — 90 quan điểm khác nhau về thế nào là một cái phòng, thế nào là một cái giá, thế nào là một cái lỗi, và "đồng ý" nghĩa là gì — mà tất cả phải cùng trả lời một câu hỏi qua một contract.
Một contract, chín mươi quan điểm
Quyết định cấu trúc đầu tiên: mỗi supplier có đúng một module, viết theo một contract cố định, và module được đăng ký bằng code của nó trong một registry duy nhất. Mục đích không phải cho gọn. Mục đích là khi một supplier sai — mà trong chín mươi cái thì lúc nào cũng có một cái đang sai — phần khác biệt giữa supplier đó và phần còn lại nằm trong một file. Không phải đi đào xem hành vi riêng của supplier rò rỉ vào code chung ở chỗ nào, vì cái contract đã biến vụ rò rỉ đó từ một bài khảo cổ thành một lỗi lộ ra ngay lúc viết.
Hai cách nói "một cái phòng"
Nghe qua thì tưởng mô hình phòng-và-giá là phần dễ. Nó lại là chỗ rẽ nhánh sâu nhất. Có hai pattern offer được ghi thành tài liệu, vì mô hình của các supplier khác nhau thật, và không có phép mapping nào biến chúng thành một:
Ở pattern A, supplier trả về một token duy nhất đặt được N phòng — một offer, một lần confirm, xong. Ở pattern B, giá của từng phòng độc lập với nhau, và một "offer" cho lượt search hai phòng chưa hề tồn tại cho tới khi mình materialise nó: danh sách offer là cái ma trận tổ hợp hợp lệ được tính ra. Cùng một lượt search, cùng một màn hình, mà bên dưới, một supplier đang bán cho bạn một món đồ còn supplier kia đang bán từng linh kiện của nó.
Giả vờ hai cái này là một pattern sẽ đẻ ra những booking mà phòng hai lặng lẽ ăn giá của phòng một. Ghi hẳn thành hai, và bắt mọi tích hợp mới khai báo mình thuộc loại nào, là một quyết định thiết kế — loại quyết định không bao giờ xuất hiện trong buổi demo.
Hai mô hình thực thi sau một cánh cửa
Protocol trải từ JSON qua HTTP tới SOAP/XML; phía XML được map bằng camaro và xmlbuilder chứ không đi bộ qua DOM, vì ở lưu lượng search thì parser là một khoản chi phí. Nhưng vết cắt lớn hơn nằm ở thời gian.
Search hotel, flight và car chạy bất đồng bộ: một queue, mỗi supplier một job, mỗi job được một Redis lock canh, worker ghi kết quả vào một cache hash dùng chung cộng một meta hash đánh dấu đã xong. Tầng HTTP gom những gì đã về và báo một tỉ lệ tiến độ, dưới một deadline tuyệt đối 30 giây — quá mốc đó, ai chưa trả lời thì không có mặt trong kết quả. Search tour và transfer chạy đồng bộ: một process server duy nhất, không worker, không polling.
Client không nhìn thấy gì trong số đó. Cả hai mô hình ngồi sau cùng một public API contract. Câu đó ngắn mà trả giá không ít, vì "cùng một contract" nghĩa là phía async phải trả lời những câu hỏi phía sync không bao giờ gặp — "xong một nửa" thì trông như thế nào, supplier trả lời ở giây thứ 31 thì đi đâu — và các câu trả lời phải vô hình khi nhìn từ ngoài vào.
Phần không ai đem đi quảng cáo
Đây mới là cái lõi thật của nghề: phần lớn công việc là quyết định làm gì khi phía bên kia sai, chậm, hoặc im lặng.
Có supplier trả 200 kèm một error body — status thành công bọc lấy một thất bại, nghĩa là ý kiến của tầng transport về cú gọi đó vô giá trị, và mọi response cần một lượt đọc thứ hai theo cách riêng của từng supplier. Có supplier timeout đúng lúc tiền đang bay giữa đường, để lại cho mình câu hỏi booking có tồn tại hay không. Có supplier trả cái giá không khớp với cái giá chính nó vừa trả ở lượt search trước. Không cái nào là edge case; ở quy mô 90 tích hợp thì đó là thời tiết. Mình chưa từng đếm bao nhiêu phần code tích hợp lo cho đường xấu so với đường đẹp — nên mình sẽ không đặt con số nào vào đây — nhưng đường đẹp là phần viết xong sớm nhất và nghĩ tới ít nhất.
Còn một lựa chọn cấu trúc nữa thuộc về mục này: state giữa các lần gọi nằm ngay trong một offer ID tổ hợp có version — source, contract, hotel, uuid — được decode ở bước prebook. Không có session phía server. Mọi dữ kiện mà bước kế tiếp cần đều đi bên trong cái ID client đang cầm sẵn, nghĩa là một cú prebook rơi vào instance nào cũng được, sống sót qua một lần deploy ngay giữa lúc khách đang checkout, và debug được chỉ từ chính cái định danh đó.
Vì sao đây là kiến trúc
Nhìn lại danh sách: một contract cố định với mỗi quan điểm một module; hai pattern offer thay vì một cái giả; hai mô hình thực thi sau một bộ mặt chung; ngữ nghĩa lỗi dựng lại trên một tầng transport biết nói dối; state nén vào định danh thay vì session. Không mục nào trong đó là "gọi API". Mỗi mục là một ranh giới — một đường kẻ nơi có người quyết định phần còn lại của hệ thống được phép giả định điều gì, và nhận trách nhiệm cho những ca giả định đó vỡ.
Thợ ống nước dẫn thứ nước hành xử như nước. Việc này liên kết chín mươi bên mà mỗi bên hành xử một kiểu, rồi bắt tất cả trả lời một câu hỏi, trong 30 giây, theo một khuôn. Mấy cái ống là phần dễ.