Mở đầu — vì sao bài này quan trọng
Bạn vừa học cách viết Problem, Goals, user stories và acceptance criteria. Đó là bộ khung chung cho mọi PRD. Nhưng khi bạn viết spec cho một tính năng mobile, có ba "vùng đất" mà người mới viết spec gần như luôn bỏ sót — và đó cũng chính là ba thứ làm engineer phải quay lại hỏi bạn liên tục, làm QA tìm ra bug ngay tuần đầu, và làm tính năng của bạn ship trễ một sprint: deeplink, push notification, và hành vi offline.
Vì sao mobile lại khác? Trên web, người dùng gõ URL, server trả về trang, mọi thứ tương đối tuyến tính. Trên mobile, app sống trong một thế giới đầy gián đoạn: người dùng nhận một thông báo lúc đang đứng xếp hàng, bấm vào, app phải mở đúng màn hình; họ đặt đơn hàng trong thang máy mất sóng, thao tác phải được "giữ" lại rồi gửi đi khi có mạng; họ click một link Grab gửi qua Zalo, app phải biết mở chi tiết chuyến xe chứ không phải màn hình chủ. Mỗi tình huống này là một nhánh hành vi mà nếu spec không mô tả rõ, engineer sẽ tự đoán — và họ thường đoán sai.
Bài học này dạy bạn cách spec đúng ba thứ đặc thù của mobile đó. Đây không phải lý thuyết: đây là phần spec mà nếu bạn viết tốt, đội kỹ thuật sẽ thầm cảm ơn bạn; nếu viết sót, bạn sẽ trả giá bằng những cuộc họp khẩn ngày ship.
Khái niệm cốt lõi
Một tính năng mobile, ngoài phần "màn hình và logic" giống web, còn cần spec rõ ba lớp đặc thù. Hãy nắm bản chất từng lớp trước.
Deeplink — đường dẫn vào sâu bên trong app
Deeplink là một URL hoặc đường dẫn dẫn người dùng vào thẳng một màn hình cụ thể bên trong app, thay vì chỉ mở app ở màn hình chủ. Có ba loại bạn cần phân biệt:
- Custom scheme deeplink: dạng
grabapp://ride/booking/12345. Đơn giản, nhanh, nhưng nếu app chưa cài thì link chết. - Universal Links (iOS) / App Links (Android): dạng
https://grab.com/ride/12345. Cùng một linkhttps: nếu có app thì mở app, nếu không thì mở web. Đây là chuẩn nên dùng cho link gửi ra ngoài (SMS, email, Zalo). - Deferred deeplink: dành cho người chưa cài app. Họ bấm link, được đưa đến store, cài app, mở lần đầu và app vẫn "nhớ" đưa họ tới đúng nội dung ban đầu. Cực kỳ quan trọng cho marketing và referral.
Push notification — kéo người dùng quay lại
Push notification có hai phần spec mà PM phải chịu trách nhiệm:
- Copy (nội dung): title, body, và đôi khi hình ảnh. Đây là phần PM viết, không phải engineer.
- Targeting & trigger: ai nhận, khi nào gửi, điều kiện nào kích hoạt, tần suất tối đa, và — đừng quên — deeplink đích khi người dùng bấm vào.
Offline behavior — app phải sống được khi mất mạng
Đây là phần khó nhất và bị bỏ sót nhiều nhất. Câu hỏi cốt lõi: khi không có mạng, thao tác của người dùng sẽ ra sao? Có ba chiến lược chính:
- Read-only cache: hiển thị dữ liệu đã tải trước đó, không cho thao tác ghi.
- Optimistic UI + queue: cho người dùng thao tác (like, gửi tin nhắn, thêm vào giỏ), giao diện cập nhật ngay như đã thành công, còn thao tác được xếp hàng (queue) và gửi lên server khi có mạng trở lại.
- Block với thông báo: chặn thao tác, hiện thông báo "Cần kết nối mạng".
Tình huống thực tế
Ví dụ 1 — ShopeeFood và deeplink từ chiến dịch khuyến mãi
Một đội growth của một app giao đồ ăn (lấy bối cảnh tương tự ShopeeFood tại Việt Nam) chạy chiến dịch gửi SMS: "Giảm 40k cho đơn đầu tiên, bấm vào đây". Link trong SMS là https://order.app/promo/FIRST40.
PM ban đầu chỉ spec: "Link mở app vào trang nhập mã FIRST40". Nghe đủ. Nhưng khi launch, support nhận hàng trăm phàn nàn. Vì sao? Phần lớn người nhận SMS là người chưa cài app (đó là mục tiêu của chiến dịch acquisition). Họ bấm link, được đưa tới CH Play, cài app, mở lên — và rơi vào màn hình đăng nhập trống trơn. Mã FIRST40 biến mất. Tỷ lệ áp dụng mã chỉ đạt 8%.
Đội phải làm lại spec, bổ sung phần deferred deeplink: khi người dùng cài app từ link này, app phải lưu lại tham số promo=FIRST40 (qua một dịch vụ attribution như AppsFlyer hoặc Branch), và sau khi đăng nhập lần đầu, tự động áp mã vào giỏ. Họ cũng thêm trạng thái: nếu mã đã hết hạn, hiện thông báo rõ ràng thay vì màn hình trống. Sau khi sửa, tỷ lệ áp dụng mã tăng lên 31%.
Bài học: Khi spec deeplink cho marketing, luôn hỏi "người này đã cài app chưa?". Nếu chiến dịch nhắm người mới, bạn bắt buộc phải spec deferred deeplink — nếu không bạn đang đổ tiền quảng cáo vào một cái phễu thủng đáy.
Ví dụ 2 — Một app ngân hàng số và push notification gây hoảng loạn
Một ngân hàng số tại Đông Nam Á tung tính năng thông báo biến động số dư qua push. Engineer triển khai theo spec: "Khi tài khoản có giao dịch, gửi push thông báo số dư mới".
Sự cố xảy ra vào một sáng thứ Hai. Hệ thống đối soát chạy batch, tạo ra hàng loạt bút toán nội bộ. Mỗi bút toán kích hoạt một push. Một số khách hàng nhận hơn 40 thông báo trong vài phút, nội dung kiểu "Số dư của bạn thay đổi". Họ hoảng, tưởng tài khoản bị hack, gọi tổng đài dồn dập, social media bùng lên.
Vấn đề nằm ở spec thiếu ba thứ: (1) điều kiện trigger quá rộng — không phân biệt giao dịch thực của khách với bút toán nội bộ; (2) không có rate limiting — không giới hạn số push mỗi người mỗi khoảng thời gian; (3) copy mơ hồ — "Số dư thay đổi" thay vì "Bạn vừa nhận 500.000đ từ Nguyen Van A".
Spec làm lại định nghĩa rõ: chỉ trigger trên giao dịch loại customer_facing; gộp (debounce) nhiều giao dịch trong 60 giây thành một thông báo tổng hợp; tối đa 1 push mỗi 30 giây cho cùng một người với loại số dư; và copy cụ thể có số tiền, bên đối tác, thời gian.
Bài học: Push spec không chỉ là viết câu chữ. Phần nguy hiểm nhất là trigger condition và rate limiting. Luôn tự hỏi: "Trong trường hợp xấu nhất, một người dùng có thể nhận bao nhiêu push từ tính năng này trong một phút?" Nếu bạn không trả lời được, spec chưa xong.
Ví dụ 3 — Grab và hành vi offline khi đặt xe
Hình dung một tài xế công nghệ tại TP.HCM đang trong hầm gửi xe của một toà nhà, mất sóng. Anh ta bấm "Hoàn thành chuyến". Nếu app chỉ hiện "Không có mạng, thử lại", anh ta sẽ kẹt: không thể nhận chuyến mới, và khách đã xuống xe.
Spec offline tốt cho tình huống này dùng optimistic UI + queue: khi tài xế bấm "Hoàn thành", app ngay lập tức hiển thị màn hình "Đã hoàn thành", lưu hành động vào queue cục bộ kèm timestamp và GPS cuối cùng. Khi ra khỏi hầm có sóng, app tự gửi sự kiện lên server. Server xử lý conflict: nếu timestamp hợp lệ và chuyến chưa bị hệ thống tự đóng, chấp nhận; nếu không, hiện thông báo và yêu cầu xác nhận lại.
Spec này phải mô tả rõ: queue giữ tối đa bao nhiêu hành động, retry với backoff (thử lại sau 2s, 4s, 8s...), điều gì hiển thị nếu sau 10 phút vẫn không gửi được, và làm sao tránh double submit (gửi hai lần khi mạng chập chờn) bằng một idempotency key duy nhất cho mỗi hành động.
Bài học: Offline không phải tính năng "nice to have" cho app vận hành thực tế ở Việt Nam — nơi sóng yếu trong hầm, thang máy, vùng ven là chuyện thường ngày. Một spec offline tốt biến trải nghiệm bực bội thành liền mạch. Và idempotency là từ khoá bạn phải nhớ: nó ngăn người dùng vô tình tạo hai đơn hàng, hai giao dịch.
Hướng dẫn từng bước
Khi viết phần mobile-specific cho spec, làm theo trình tự sau.
Bước 1 — Liệt kê mọi điểm vào (entry points) bằng deeplink. Với mỗi màn hình quan trọng, hỏi: có ai cần link thẳng vào đây không? Từ push? Từ SMS? Từ chia sẻ của người dùng? Lập một bảng schema:
| Đích | Deeplink | Tham số | Trạng thái cần xử lý |
|---|---|---|---|
| Chi tiết đơn | app.com/order/{id} | id | Chưa login / đơn không tồn tại / không có quyền / chưa cài app |
Bước 3 — Spec từng loại push notification. Với mỗi push, ghi: loại (transactional/marketing), điều kiện trigger chính xác, đối tượng nhận (targeting), tần suất tối đa, copy đầy đủ (title + body, có ví dụ với dữ liệu thật), và deeplink đích khi bấm. Viết luôn cả hành vi khi người dùng đã tắt thông báo.
Bước 4 — Chọn chiến lược offline cho từng thao tác. Phân loại mỗi thao tác của người dùng vào: read-only cache, optimistic + queue, hay block. Với thao tác queue, spec rõ: lưu trữ, retry policy, conflict resolution, idempotency key, và giới hạn queue.
Bước 5 — Viết acceptance criteria cho cả ba lớp. Mỗi trạng thái deeplink, mỗi điều kiện push, mỗi nhánh offline đều cần một tiêu chí kiểm thử rõ ràng để QA có thể verify. (Bạn đã học cách viết acceptance criteria ở bài trước — áp dụng vào đây.)
Lỗi thường gặp & mẹo
Lỗi 1 — Chỉ spec happy path của deeplink. Người mới chỉ viết "link mở chi tiết sản phẩm". Họ quên: sản phẩm hết hàng, đã bị gỡ, người dùng chưa login, hoặc chưa cài app. Mẹo: với mỗi deeplink, luôn duyệt checklist bốn câu hỏi — đã login? nội dung còn không? có quyền không? app đã cài chưa?
Lỗi 2 — Để engineer tự viết copy push. Kết quả là những thông báo cụt lủn, sai chính tả tiếng Việt, không có dấu, hoặc lộ thông tin nhạy cảm trên màn hình khoá. Mẹo: PM viết copy đầy đủ, có ví dụ với dữ liệu thật, và ghi rõ nội dung nào không được hiện trên lock screen (số tiền lớn, mã OTP, thông tin sức khoẻ).
Lỗi 3 — Quên rate limiting và debounce cho push. Đây là lỗi gây khủng hoảng PR như ví dụ ngân hàng ở trên. Mẹo: luôn ghi một dòng "Tối đa X push / người / khoảng thời gian Y" cho mỗi loại thông báo.
Lỗi 4 — Mặc định "có mạng" cho mọi thao tác. Spec viết như thể người dùng luôn online. Mẹo: đọc lại spec và với mỗi nút bấm gửi dữ liệu, tự hỏi "nếu lúc này mất mạng thì sao?".
Lỗi 5 — Quên idempotency. Mạng chập chờn khiến app gửi hai lần, tạo hai đơn hàng. Mẹo: mọi thao tác ghi đi qua queue phải có một khoá duy nhất để server nhận diện và loại trùng.
Mẹo tổng: Dùng một bảng "trạng thái x hành vi" cho cả ba lớp. Bảng buộc bạn phải nghĩ tới mọi ô — đó chính là chỗ engineer hay hỏi và QA hay tìm thấy bug.
Bài tập thực hành
Chọn một tính năng mobile cụ thể (ví dụ: "Chia sẻ một bài viết yêu thích cho bạn bè" trong một app đọc tin tức Việt Nam). Viết phần mobile-specific spec gồm:
- Bảng deeplink schema cho màn hình bài viết: URL, tham số, và hành vi cho cả bốn trạng thái (chưa login / bài đã gỡ / không có quyền / chưa cài app — bao gồm cả deferred deeplink khi người được chia sẻ chưa có app).
- Spec hai push notification: một cái transactional ("Bài viết bạn lưu đã có phần tiếp theo") và một cái marketing ("5 bài đọc hot tối nay"). Với mỗi cái, viết: trigger, targeting, tần suất tối đa, copy đầy đủ (có ví dụ dữ liệu thật), và deeplink đích.
- Chiến lược offline cho thao tác "Lưu bài viết": chọn read-only cache / optimistic + queue / block, rồi spec retry policy và idempotency key.
Tóm tắt
Spec cho tính năng mobile cần thêm ba lớp đặc thù mà web không có. Deeplink: định nghĩa URL schema rõ ràng và — quan trọng hơn — hành vi cho mọi trạng thái (chưa login, nội dung mất, không quyền, chưa cài app, kể cả deferred deeplink). Push notification: PM chịu trách nhiệm copy và phần targeting/trigger; đừng bao giờ quên rate limiting và debounce, vì đó là nguồn gốc của những khủng hoảng như "40 thông báo trong vài phút". Offline: chọn chiến lược (read-only / optimistic + queue / block) cho từng thao tác, và với queue thì luôn spec retry, conflict resolution và idempotency key để tránh double submit.
Mẫu số chung: với mỗi lớp, lập một bảng "trạng thái x hành vi". Bảng đó buộc bạn nghĩ tới mọi ô — chính là những chỗ engineer hay hỏi và QA hay tìm thấy bug. Viết tốt ba lớp này, spec mobile của bạn sẽ ship đúng hạn và đội kỹ thuật sẽ thầm cảm ơn bạn.