Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 23 — API Versioning, Deprecation, và Breaking Changes

Technical Product Manager Bài 23/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là Technical PM của một ví điện tử như MoMo hay ZaloPay. API thanh toán của bạn đang phục vụ hàng nghìn merchant — từ chuỗi cà phê lớn cho đến tiệm tạp hóa nhỏ tích hợp qua một plugin WordPress mà chủ tiệm cài từ ba năm trước. Một sáng đẹp trời, đội engineering muốn "dọn dẹp" response của API: đổi tên field amount thành total_amount cho rõ nghĩa hơn. Nghe có vẻ vô hại. Nhưng đến trưa, hàng trăm merchant gọi điện báo "không thanh toán được", doanh thu của họ đứng hình, và bộ phận hỗ trợ của bạn cháy máy.

Đó chính xác là một breaking change — và đây là một trong những nỗi đau lớn nhất khi bạn vận hành một API có người khác đang phụ thuộc vào.

Khác với một tính năng trong app mà bạn kiểm soát toàn bộ (cả frontend lẫn backend), API là một hợp đồng (contract). Một khi đã công bố và có người tích hợp, bạn không còn toàn quyền thay đổi nó nữa. Bạn đang giữ trong tay một lời hứa với những lập trình viên mà bạn chưa từng gặp mặt, viết code mà bạn không bao giờ nhìn thấy, chạy trên hệ thống mà bạn không kiểm soát.

Bài này dạy bạn ba thứ đan xen nhau: nhận diện đâu là breaking change (để không vô tình gây ra), thiết kế chiến lược versioning (để có chỗ thay đổi an toàn), và vận hành quy trình deprecation (để khai tử cái cũ một cách văn minh). Đây là kiến thức bắt buộc nếu bạn muốn làm Platform PM hay quản lý bất kỳ sản phẩm API nào.

Khái niệm cốt lõi

Breaking change là gì?

Một breaking change là bất kỳ thay đổi nào khiến client cũ — vốn đang hoạt động bình thường — đột nhiên hỏng mà không cần họ động vào code của họ. Nói cách khác: bạn thay đổi, nhưng người gánh hậu quả là người tích hợp.

Những thay đổi sau gần như luôn là breaking:

  • Xóa một field trong response. Client đang đọc user.phone, bạn xóa field đó đi — code của họ nhận undefined và đổ vỡ.
  • Đổi kiểu dữ liệu của field. Trước đây id là số nguyên (12345), giờ bạn đổi sang chuỗi ("usr_12345"). Code client parse số sẽ lỗi ngay.
  • Thêm một field bắt buộc (required) vào request. Trước đây gọi API không cần idempotency_key, giờ bạn bắt buộc phải có — mọi client cũ gửi request thiếu field này sẽ bị từ chối.
  • Đổi tên field hoặc endpoint. amount thành total_amount, hay /v1/users thành /v1/customers.
  • Thay đổi nghĩa của một giá trị. Trước status trả về "done", giờ trả "completed". Field không đổi tên, nhưng enum value đổi — client so sánh === "done" sẽ luôn sai.
  • Siết chặt validation. Trước chấp nhận số điện thoại có hoặc không có mã vùng, giờ bắt buộc định dạng E.164. Request từng hợp lệ bỗng bị reject.
  • Đổi mã lỗi hoặc HTTP status code mà client đang dựa vào để xử lý logic.

Vậy thay đổi nào AN TOÀN (non-breaking)?

Tin tốt là không phải mọi thay đổi đều nguy hiểm. Những thay đổi additive (cộng thêm, không lấy đi) thường an toàn:

  • Thêm một field mới vào response. Client cũ chỉ đơn giản bỏ qua field họ không biết.
  • Thêm một field tùy chọn (optional) vào request. Không gửi cũng không sao.
  • Thêm một endpoint mới hoàn toàn.
  • Thêm một enum value mớivới điều kiện client của bạn được thiết kế để xử lý "giá trị lạ" một cách an toàn (đây là điểm gây tranh cãi, sẽ nói ở phần lỗi thường gặp).
Nguyên tắc vàng để nhớ: "Be conservative in what you send, be liberal in what you accept" (Robustness Principle của Postel). Khi là người cung cấp API, hãy thêm chứ đừng bớt, đừng đổi.

Ba chiến lược versioning phổ biến

Khi buộc phải tạo breaking change, bạn cần một "phiên bản mới" để chứa nó, đồng thời giữ phiên bản cũ sống cho client chưa kịp nâng cấp. Có ba cách đặt version:

1. URI versioning — nhét version vào đường dẫn: api.momo.vn/v1/paymentsapi.momo.vn/v2/payments. Đây là cách phổ biến nhất vì nó hiển nhiên, dễ test bằng trình duyệt, dễ route ở tầng gateway. Nhược điểm: về lý thuyết nó "không thuần REST" vì cùng một tài nguyên (payment) lại có hai URL.

2. Header versioning — version nằm trong HTTP header: Accept: application/vnd.momo.v2+json. Sạch sẽ về mặt URL, nhưng khó test thủ công và ít trực quan với người mới.

3. Query parameter versioningapi.momo.vn/payments?version=2. Đơn giản nhưng dễ bị bỏ sót và khó cache.

Với phần lớn sản phẩm tại Việt Nam và Đông Nam Á, URI versioning là lựa chọn thực dụng nhất vì nó dễ giải thích cho đối tác tích hợp — nhiều người trong số họ không phải kỹ sư backend chuyên sâu.

Semantic Versioning — ngôn ngữ chung của thay đổi

Bạn sẽ thường xuyên nghe đến SemVer với định dạng MAJOR.MINOR.PATCH (ví dụ 2.4.1):

  • MAJOR (số đầu): tăng khi có breaking change. Từ 1.x lên 2.0.0.
  • MINOR (số giữa): tăng khi thêm tính năng mới nhưng vẫn tương thích ngược.
  • PATCH (số cuối): tăng khi sửa lỗi, không đổi hành vi.
Với API public, thông thường chỉ số MAJOR mới xuất hiện trong URL (/v1, /v2). MINOR và PATCH được phát hành "âm thầm" vì chúng không phá vỡ client.

Tình huống thực tế

Ví dụ 1 — Stripe và nghệ thuật versioning bằng ngày tháng

Stripe (nền tảng thanh toán toàn cầu) nổi tiếng vì gần như không bao giờ ép khách hàng nâng cấp. Thay vì /v1, /v2, họ gắn version theo ngày phát hành, ví dụ 2024-06-20. Mỗi tài khoản được "ghim" vào version mà nó đăng ký lần đầu. Khi Stripe thay đổi API, họ phát hành một version-ngày mới, nhưng tài khoản cũ vẫn chạy đúng hành vi của ngày họ tích hợp.

Bên dưới, Stripe duy trì một lớp "compatibility transformations" — mỗi breaking change được viết thành một hàm chuyển đổi giữa version cũ và mới. Một request đến từ tài khoản cũ sẽ được "dịch ngược" qua chuỗi các hàm này.

Bài học rút ra: Nếu sản phẩm API của bạn cực kỳ quan trọng với khách hàng (như thanh toán), hãy đầu tư để khách không bao giờ bị ép nâng cấp đột ngột. Cách làm này tốn kém về kỹ thuật, nhưng đổi lại là niềm tin tuyệt đối. Là PM, bạn cần biết đánh đổi này tồn tại để cân nhắc cho sản phẩm của mình.

Ví dụ 2 — Một fintech Việt Nam và cú đổi field gây sự cố

Giả định một công ty fintech tại TP.HCM cung cấp API cho khoảng 400 đối tác merchant. Đội backend quyết định đổi field customer_id trong webhook từ kiểu số nguyên (8842301) sang chuỗi có tiền tố ("cus_8842301") để chống nhầm lẫn nội bộ. Họ deploy thẳng lên production vào một chiều thứ Sáu mà không tăng version.

Kết quả: khoảng 120 merchant đang lưu customer_id vào cột kiểu INTEGER trong database của họ. Webhook gửi chuỗi "cus_8842301", hệ thống của họ ném lỗi parse, và toàn bộ giao dịch cuối tuần bị "kẹt" không cập nhật trạng thái. Đội support nhận hơn 300 ticket trong 48 giờ, và công ty phải rollback khẩn cấp lúc 11 giờ đêm.

Bài học rút ra: Đổi kiểu dữ liệu là một trong những breaking change âm thầm và nguy hiểm nhất — vì nó không gây lỗi cú pháp ở phía bạn, request vẫn "trông ổn", nhưng nó nổ ở phía client. Đáng lẽ thay đổi này phải được đưa vào một version mới hoặc thêm một field song song customer_ref thay vì sửa field cũ. Và đừng bao giờ deploy thay đổi rủi ro vào chiều thứ Sáu.

Ví dụ 3 — GitHub deprecate Authorization qua URL

GitHub từng cho phép truyền token xác thực trực tiếp trong query string của URL. Vì lý do bảo mật (token bị lộ trong log server), họ cần khai tử cách này. Thay vì tắt đột ngột, họ làm một chiến dịch deprecation kéo dài: gửi email cảnh báo, thêm header Sunset báo ngày "tắt máy", trả về cảnh báo trong response, và thậm chí cố tình tạo "brownout" — chủ động chặn API trong vài giờ vào ngày được báo trước để các đội còn ngái ngủ "giật mình" nhận ra mình cần sửa, trước khi tắt vĩnh viễn.

Bài học rút ra: Deprecation không phải là một cái công tắc, mà là một chiến dịch truyền thông có thời hạn. Là PM, công việc của bạn ở đây nhiều phần là giao tiếp hơn là kỹ thuật: thông báo sớm, nhắc nhở nhiều kênh, và cho khách hàng đủ thời gian.

Hướng dẫn từng bước

Đây là quy trình bạn nên áp dụng mỗi khi đối mặt với một thay đổi API:

Bước 1 — Phân loại thay đổi. Trước mọi thứ, hỏi: "Thay đổi này có khiến client cũ hỏng mà họ không động vào code không?" Nếu có, đó là breaking. Lập một checklist nội bộ (xóa field? đổi type? thêm required? đổi enum?) và yêu cầu mọi PR động vào API phải tự đánh giá.

Bước 2 — Tìm cách biến nó thành non-breaking. Trước khi nhảy sang version mới, hãy hỏi: có cách nào làm additive không? Thay vì đổi amount thành total_amount, hãy thêm total_amount và giữ amount chạy song song một thời gian. Phần lớn breaking change có thể né được nếu bạn chịu khó thiết kế.

Bước 3 — Nếu buộc phải breaking, tạo version mới. Quyết định chiến lược (thường là URI: /v2). Đảm bảo /v1 vẫn sống và không bị ảnh hưởng. Hai version cùng tồn tại trong một khoảng giao thoa.

Bước 4 — Công bố lịch deprecation. Đặt rõ ba mốc: ngày announce (thông báo), ngày deprecated (không khuyến khích dùng, không thêm tính năng mới), và ngày sunset (tắt vĩnh viễn). Với API public quan trọng, khoảng cách announce-to-sunset thường tối thiểu 6–12 tháng. Với API nội bộ, có thể ngắn hơn.

Bước 5 — Truyền thông đa kênh. Email đến developer đã đăng ký, changelog công khai, header Deprecation: trueSunset: <ngày> trong response, banner trên developer portal. Đừng giả định khách đọc email — hãy nhắc qua nhiều kênh.

Bước 6 — Đo lường mức độ sử dụng version cũ. Đây là phần PM hay quên. Bạn cần dashboard theo dõi: còn bao nhiêu % traffic, bao nhiêu khách hàng còn dùng /v1? Đừng tắt khi vẫn còn 30% traffic — hãy tiếp cận trực tiếp các khách hàng "cứng đầu" để hỗ trợ họ migrate.

Bước 7 — Sunset có kiểm soát. Cân nhắc brownout trước ngày tắt thật. Sau khi tắt, trả về mã lỗi rõ ràng (ví dụ 410 Gone) với thông điệp hướng dẫn nâng cấp, thay vì để khách nhận lỗi mơ hồ.

Lỗi thường gặp & mẹo

Lỗi 1 — Tưởng "thêm field" luôn an toàn. Phần lớn là an toàn, nhưng có ngoại lệ: nếu client của họ dùng "strict schema validation" (từ chối mọi field lạ), thì thêm field cũng làm họ hỏng. Mẹo: ngay từ đầu, ghi rõ trong tài liệu rằng "client phải bỏ qua field không nhận diện được". Đặt kỳ vọng từ ngày một.

Lỗi 2 — Tạo version mới cho mọi thay đổi nhỏ. Mỗi version sống là một gánh nặng bảo trì — bạn phải test, vá lỗi, vận hành song song. Nếu cứ thay đổi nhỏ là lên version, bạn sẽ sớm có /v7 và đội ngũ kiệt sức. Mẹo: chỉ tăng MAJOR version khi thực sự có breaking change không thể né.

Lỗi 3 — Deprecate nhưng không bao giờ dám tắt. Nhiều công ty công bố deprecation rồi để version cũ sống mãi vì sợ mất khách. Hậu quả: gánh nặng kỹ thuật tích lũy vô hạn. Mẹo: đặt deadline cứng ngay từ đầu và gắn nó với cam kết. Một version "deprecated nhưng bất tử" còn tệ hơn không deprecate.

Lỗi 4 — Quên rằng khách Việt Nam/SEA thường tích hợp một lần rồi không đụng tới. Nhiều merchant nhỏ thuê freelancer làm tích hợp rồi không có đội kỹ thuật in-house. Họ sẽ không đọc email tiếng Anh, không theo dõi changelog. Mẹo: với nhóm này, hãy truyền thông bằng tiếng Việt, gọi điện trực tiếp cho top khách hàng theo doanh thu, và cho thời gian dài hơn bình thường.

Mẹo vàng: Hãy coi API như một sản phẩm có vòng đời, không phải một đoạn code. Developer experience của người tích hợp chính là "khách hàng" của bạn. Một chiến lược versioning tốt là chiến lược mà khách hàng gần như không nhận ra nó tồn tại — vì mọi thứ luôn chạy.

Bài tập thực hành

Bài 1 — Phân loại breaking/non-breaking. Với mỗi thay đổi sau, xác định nó là breaking hay không và giải thích ngắn:

  • Thêm field loyalty_points vào response /v1/orders.
  • Đổi delivery_fee từ 15000 (số) sang "15000" (chuỗi).
  • Thêm header optional X-Request-Id mà client có thể gửi kèm.
  • Đổi enum order_status từ "shipping" sang "in_transit".
  • Thêm endpoint mới /v1/orders/{id}/refund.
Bài 2 — Thiết kế kế hoạch deprecation. Bạn là PM của một API giao hàng (logistics) phục vụ 250 cửa hàng tại Việt Nam. Bạn cần khai tử /v1 để chuyển sang /v2 (có cấu trúc địa chỉ mới theo đơn vị hành chính). Hãy viết một timeline deprecation gồm các mốc thời gian cụ thể, kênh truyền thông, và cách bạn đo lường mức độ sẵn sàng tắt /v1.

Bài 3 — Né breaking change. Đội engineering muốn đổi field phone (đang là chuỗi tự do) thành object có cấu trúc {country_code, national_number}. Hãy đề xuất một cách triển khai non-breaking để vừa giới thiệu cấu trúc mới vừa không phá vỡ client cũ.

Tóm tắt

  • Breaking change là thay đổi khiến client cũ hỏng dù họ không động vào code: xóa field, đổi kiểu dữ liệu, đổi tên, thêm required field, đổi nghĩa enum, siết validation. Đây là điều bạn phải nhận diện được bằng phản xạ.
  • Thay đổi additive (thêm field optional, thêm endpoint) thường an toàn. Nguyên tắc: thêm chứ đừng bớt, đừng đổi.
  • Khi buộc phải breaking, hãy tạo version mới (URI versioning như /v2 là thực dụng nhất cho bối cảnh Việt Nam) và giữ version cũ sống song song.
  • SemVer (MAJOR.MINOR.PATCH) là ngôn ngữ chung: MAJOR cho breaking, MINOR cho tính năng tương thích, PATCH cho sửa lỗi.
  • Deprecation là một chiến dịch truyền thông có thời hạn, không phải cái công tắc. Công bố sớm, nhắc đa kênh, đo lường traffic version cũ, và sunset có kiểm soát.
  • Là Technical PM, giá trị lớn nhất bạn mang lại không phải là viết code chuyển đổi, mà là bảo vệ niềm tin của người tích hợp — biến mọi thay đổi thành thứ êm ái đến mức khách hàng gần như không nhận ra.