Bỏ qua tới nội dung
Kiên

Quyết định

Các quyết định kiến trúc đằng sau những hệ thống trên site này — lúc đó có gì trên bàn, đã chọn gì, và trả giá ra sao. Kể cả cái đã phải rollback.

  1. Trạng thái giữa các bước nằm trong chính id, không nằm trong session server

    2024vẫn giữ
    Bối cảnh
    Search, prebook và book là ba HTTP call riêng, instance nào rảnh thì nhận — nhưng lúc prebook vẫn phải biết offer đến từ supplier nào, contract nào, credential nào.
    Trên bàn lúc đó
    • Session phía server, key bằng token và hết hạn trong Redis — lối mòn ai cũng đi đầu tiên.
    • Prebook tự suy lại ngữ cảnh từ kết quả search trong cache — vẫn là session, chỉ đổi tên, và vẫn dính đủ chuyện hết hạn với lệch giữa các instance.
    Quyết định
    Offer id ghép `source · contract · hotel · uuid`, prebook decode ra là biết dùng credential nào.
    Hệ quả
    Không có gì để hết hạn, không có gì để lệch giữa các instance. Mặt trái: layout của id giờ là một phần của hợp đồng công khai — client giữ nó giữa các call, nên thứ nằm trong đó không bao giờ được đổi lẳng lặng.
  2. Đúng hai pattern tạo offer, không phải mỗi supplier một kiểu

    2024vẫn giữ
    Bối cảnh
    Mỗi wholesaler mô hình hoá phòng, giá và chính sách huỷ một kiểu, nhưng module nào cũng phải trả lời chung một hợp đồng API.
    Trên bàn lúc đó
    • Để mỗi module trả đúng kiểu supplier của nó rồi normalise ở phía gọi — lối mòn biến mọi consumer thành một trình phiên dịch.
    • Một pattern vạn năng duy nhất — không đứng nổi, vì có supplier trả một token đặt được N phòng trong khi supplier khác tính giá từng phòng độc lập.
    Quyết định
    Hai pattern viết thành tài liệu và không có pattern thứ ba — pattern A khi supplier trả một token đặt N phòng, pattern B khi danh sách offer là ma trận vật chất hoá của các tổ hợp phòng-giá hợp lệ.
    Hệ quả
    Tích hợp mới nào cũng bắt đầu bằng việc xếp supplier vào A hay B thay vì thiết kế một shape mới. Cái giá là dung lượng — pattern B viết rõ từng tổ hợp hợp lệ thành một offer riêng, và offer là thứ search phải cõng: từng có một lượt search giữ khoảng 10 triệu offer và ghi 1,2 GB vào Redis, trước khi cấu trúc lưu trữ được sửa.
  3. Graph cho danh tính khách sạn, không phải bảng join từng cặp

    2022vẫn giữ
    Bối cảnh
    Cùng một toà nhà mang mỗi supplier một mã khác nhau, và danh tính có tính bắc cầu — A trùng B, B trùng C thì A trùng C. Resolve sai một cái là cả một vùng search trả về không có khách sạn nào.
    Trên bàn lúc đó
    • Bảng join theo từng cặp supplier — lối mòn, nơi mỗi lần tra bắc cầu thành một chuỗi self-join không biết sâu bao nhiêu, và mỗi supplier mới lại đào sâu thêm.
    Quyết định
    Giữ bảng dòng cho dữ liệu, nhưng phần tra cứu danh tính chạy trên graph Neo4j với quan hệ `SAME_AS` và truy vấn độ sâu thay đổi được.
    Hệ quả
    Vẫn đứng vững từ 2022 — bảng danh tính ~1,9 triệu dòng và các campaign bắc cầu năm 2026 đều ngồi trên nó. Cái giá là một kho thứ hai phải giữ đồng bộ với chính những dòng nó phản chiếu.
  4. Khác biệt white-label đẩy xuống dữ liệu, không fork code

    2024vẫn giữ
    Bối cảnh
    Cùng một tích hợp supplier được bán lại dưới nhiều thương hiệu, mỗi thương hiệu có credential riêng và cột mã khách sạn riêng trong catalogue.
    Trên bàn lúc đó
    • Mỗi thương hiệu một bản copy của module — lối mòn nhân mọi lần fix bug lên theo số thương hiệu.
    Quyết định
    Mỗi supplier một module; một bảng setting thưa chỉ chứa key khác default, cộng một map từ mã supplier sang cột mã khách sạn tương ứng. Thương hiệu trở thành alias đăng ký bằng mã.
    Hệ quả
    75 mã supplier chạy trên 31 module. Mặt trái của việc đẩy hành vi xuống dữ liệu là một dòng setting sai trở thành bug production mà không diff code nào chỉ ra được — phải đọc lại dữ liệu mới bắt được.
  5. Snapshot toàn bộ cây workflow lúc submit

    2026vẫn giữ
    Bối cảnh
    Một luồng duyệt nhiều cấp, cấu hình khác nhau theo từng tenant — và cấu hình cứ đổi trong khi đơn đang chạy giữa chừng.
    Trên bàn lúc đó
    • Đọc cấu hình sống ở từng cấp — lối mòn, cho đến ngày một lần sửa giữa chừng viết lại luôn danh sách người lẽ ra phải duyệt một đơn đã tồn tại.
    Quyết định
    Lúc submit, snapshot toàn bộ cây workflow kèm deadline từng cấp; suốt phần đời còn lại, đơn chỉ trả lời theo bản snapshot của nó.
    Hệ quả
    Sửa cấu hình về sau không viết lại được lịch sử — nhưng cũng không chữa được lịch sử: đơn đang chạy giữ nguyên snapshot kể cả khi lần sửa đó chính là bản fix. Đánh đổi này là cố ý.
  6. Job delay chỉ là đồng hồ, không bao giờ là nguồn sự thật

    2026vẫn giữ
    Bối cảnh
    Nhắc SLA từng cấp chạy trên hàng đợi delayed-job, trong khi các cấp mà nó canh thì bị duyệt, đổi người, leo thang ngay bên dưới.
    Trên bàn lúc đó
    • Huỷ rồi đặt lại job ở mỗi lần đổi trạng thái — lối mòn biến hàng đợi thành nguồn sự thật thứ hai, không được phép lỡ một cập nhật nào.
    Quyết định
    Job chỉ là đồng hồ báo thức: nổ thì worker đọc lại database, cấp đã xử lý rồi thì không làm gì. Id job tất định, nên reschedule không đẻ mail trùng.
    Hệ quả
    Không job nào cần huỷ, và Redis chết thì mất nhắc nhở nhưng không bao giờ chặn duyệt. Cái giá là mỗi lần job nổ đều tốn một lượt đọc database, kể cả những lần hoá ra chẳng phải làm gì.
  7. Độ giống tên cộng toạ độ là đủ bằng chứng để ghi danh tính

    2026đã thay
    Bối cảnh
    Các campaign danh tính khách sạn cần một luật cho câu hỏi khi nào một ứng viên trùng khớp được phép ghi. Kỹ thuật bắc cầu lấy số supplier độc lập cùng xác nhận làm điểm tin cậy — nhưng có một campaign đã ghi chỉ dựa trên độ giống tên và toạ độ.
    Trên bàn lúc đó
    • Ghi khi vượt ngưỡng giống tên và khoảng cách, không cần supplier nào làm chứng — phủ rộng hơn, bằng chứng yếu hơn.
    • Chỉ ghi mapping có ít nhất một supplier độc lập xác nhận — phủ hẹp hơn, nhưng dòng nào cũng có nhân chứng.
    Quyết định
    Luật chỉ-cần-vượt-ngưỡng được dùng cho một campaign 11.953 dòng.
    Hệ quả
    Sau đó nhóm quyết định chỉ giữ mapping có bằng chứng từ supplier — cả 11.953 dòng bị chủ động rollback từng dòng, trạng thái sau rollback verify lại còn đúng 286.105 dòng non-null. Bị thay bởi chính sách chỉ-nhận-bằng-chứng-supplier, và mọi campaign từ đó chạy theo luật này.
  8. Client alerting không phụ thuộc thư viện nào

    2026vẫn giữ
    Bối cảnh
    Một package alerting được cài trong cả chục service; nó kéo theo thứ gì thì cả chục service cõng thứ đó mãi mãi, kể cả trong báo cáo audit.
    Trên bàn lúc đó
    • Kéo một HTTP client và một SDK cho mỗi kênh — lối mòn tiết kiệm một buổi chiều và trả giá bằng một cây phụ thuộc bắc cầu vĩnh viễn.
    Quyết định
    Block `dependencies` rỗng — khoảng 180 dòng trên `node:http`/`node:https`, response theo dáng axios để chỗ gọi đọc y như cũ.
    Hệ quả
    Không còn gì bắc cầu để audit — đổi lại, client giờ là code mà package phải tự test, kể cả với input ác ý. Và một default của chính package này đã được ghi nhận là chọn sai: `strictMode` mặc định tắt để khỏi vỡ chỗ gọi khi upgrade, trong khi chế độ an toàn phải là thứ có được mà không cần nghĩ.

Cách tôi chạy một buổi design review

Bộ câu hỏi mang dấu vân tay của domain này — mỗi câu nằm trong danh sách vì sự vắng mặt của nó từng phải trả giá, và cái giá đó đã được viết lại ở chỗ khác trên site.

Những câu hỏi bắt buộc phải có lời đáp:

  • Supplier trả HTTP 200 kèm body lỗi thì chuyện gì xảy ra? Một lỗi bị nuốt ở đúng chỗ này từng làm ba tính năng chết lặng lẽ mà không có lấy một dòng error.
  • book có idempotent khi client retry không? Timeout cộng một lần retry không được phép thành hai booking.
  • State này module nào sở hữu, và ai được phép đọc? Nếu câu trả lời là "ai cũng được" thì sáu tháng nữa các module hàn dính vào nhau.
  • Lệnh rollout mang nhãn blast-radius nào — chỉ đọc, ghi vào dữ liệu production, hay deploy? Nhãn phải nằm chỗ mắt chạm đầu tiên, không phải ở đoạn văn thứ ba.
  • Tính năng này go-live qua N repo thế nào — thứ tự merge ra sao, cặp MR nào phải lên cùng nhịp? Sót một repo là thu thiếu tiền của khách mà không ném ra lỗi nào.
  • Con số nào trong tài liệu là đo được, con số nào là ước lượng? Số nào cũng phải mang nhãn; một ước lượng bị tưởng nhầm là số đo là cách nhanh nhất để cả tài liệu mất uy tín.
  • Trong lúc tính năng đang hỏng, client nhìn thấy gì — một lỗi, hay một kết quả rỗng trông y như một câu trả lời?

Điều kiện vào — buổi review chưa bắt đầu khi chưa có:

  • vấn đề được viết ra giấy, kèm những con số đang thực sự có, từng số dán nhãn đo được hay ước lượng;
  • failure mode được gọi tên — người dùng thấy gì khi nó vỡ, chứ không chỉ chỗ nào ném exception;
  • các phương án là phương án thật, nêu kèm ràng buộc của chúng, không phải bù nhìn dựng lên để thua.

Điều kiện ra — buổi review chưa xong khi chưa có:

  • mọi đường ghi đều chạy dry-run → in kế hoạch → xác nhận → apply → đọc lại để verify, và chính công cụ ép chuyện đó thay vì trông vào trí nhớ con người;
  • mọi thứ rời khỏi process — log, alert, analytics — đều có allow-list, vì egress là một đường biên;
  • một mục "điều tôi chưa chắc" — tốn đúng một đoạn văn, và là khác biệt giữa tài liệu đồng nghiệp xây tiếp được với tài liệu họ phải verify lại từ đầu;
  • nếu thay đổi trải trên nhiều repo, danh sách repo phải viết đủ và thứ tự merge phải chốt xong trước khi bất cứ thứ gì được ship.

Ở đây không có con số "đã chạy bao nhiêu buổi review", vì chưa ai đo — bản checklist mới là thứ được giao.

Bản đồ tích hợp

Mọi thứ phía trên ngồi trên một tầng tổng hợp duy nhất: ~150 mã supplier trên ~90 tích hợp, trải trên 5 dòng sản phẩm. Bản đồ dưới đây cố ý chỉ vẽ cấu trúc — các trục, không phải ma trận đếm.

Trục Giá trị
Dòng sản phẩm Khách sạn · Vé máy bay · Tour · Xe đưa đón · Thuê xe
Họ giao thức REST-JSON · SOAP-XML · push-cache · polled availability
Pattern tạo offer A — một token đặt N phòng · B — ma trận vật chất hoá của các tổ hợp phòng-giá hợp lệ
Runtime Node.js/TypeScript (stack hằng ngày) · Python (adapter FastAPI) · Go (adapter)

Cố ý không có số đếm theo từng dòng, từng giao thức: những con số đó nằm trong registry supplier, và một con số chỉ xuất hiện trên site này sau khi nó thực sự được kéo ra và đo.

09/2026