VNMOVE
Logistics và du lịch biển đảo — đặt xe, vé tàu, khách sạn và tour trên một backend.
export const project = {
type: "fullstack",
stack: [
"Express",
"MongoDB",
"Redis",
"Socket.IO",
"React",
"zmp-ui",
"Ant Design Pro",
"Flutter",
"Goong Maps",
"Zalo Checkout SDK",
"VNPay / MoMo",
"Zalo ZNS",
"Firebase Cloud Messaging",
"Turborepo + pnpm",
],
shipped: true,
};- Năm
- 2026
- Khách hàng
- VNMOVE
- Vai trò
- Lead engineer
- Thời gian
- Đang chạy
Kiến trúc
Backend
- 29 module: ride, ferry, hotel, tour, wallet, promo, referral, dispute, enforcement
- Hai tiến trình — vnmove-api chạy cluster cho HTTP và socket, vnmove-worker fork cho jobs
- Log JSON winston đổ vào Loki/Promtail/Grafana trên VPS
Client
- Zalo Mini App cho khách (React + zmp-ui)
- Console vận hành (Ant Design Pro) và PWA cho đối tác khách sạn
- App tài xế Flutter nhận điều phối realtime
Thanh toán & thông báo
- Zalo Checkout SDK trong Mini App, ký chữ ký ở server
- VNPay và MoMo trên web, COD ở cả hai, chung một máy trạng thái đơn
- ZNS cho khách, FCM cho tài xế
Monorepo
- Design token và type TypeScript tách thành package riêng
- Turborepo + pnpm workspace cho bảy app
- Goong Maps cho định tuyến và geocoding
Ghi chú kỹ thuật
Đề bài
VNMOVE bán những chuyến ra đảo phía nam — và một chuyến đi không bao giờ là một sản phẩm. Khách đặt xe ra bến, vé tàu, khách sạn ngoài đảo, có khi thêm tour sáng hôm sau. Đặt bốn thứ đó ở bốn chỗ khác nhau là cách người ta bỏ cuộc giữa chừng. Đề bài là một luồng đặt chỗ cho tất cả, mà tài xế, khách sạn và bàn điều hành đều làm việc trên cùng một nguồn dữ liệu.
Cách làm
Một monorepo với một backend và bốn client: Zalo Mini App cho khách, console Ant Design Pro cho vận hành, PWA cho đối tác khách sạn, và app Flutter cho tài xế. Design token và type TypeScript dùng chung nằm trong package riêng, nên một enum thêm ở backend đi tới các client web dưới dạng lỗi biên dịch, chứ không phải một báo cáo lỗi.
Backend là modular monolith — 29 module: ride, ferry, hotel, tour, wallet, promo, referral, dispute, enforcement và những cái còn lại — chạy thành hai tiến trình. vnmove-api chạy cluster, giữ HTTP và các kết nối Socket.IO; vnmove-worker là một fork duy nhất ôm phần jobs. Điều phối chuyến không thể xếp hàng sau một tác vụ dựng báo cáo, và việc tách tiến trình biến điều đó thành ràng buộc cấu trúc thay vì trông vào kỷ luật.
Chỗ khó
Điều phối realtime. Một yêu cầu chuyến phải tới đúng nhóm tài xế, đúng thứ tự, và ngừng tới ngay khoảnh khắc có người nhận. Đó là một race condition có khách hàng ngồi nhìn, nên việc nhận chuyến được chốt trong Redis chứ không trong request handler, và tầng socket chỉ báo lại một quyết định đã xong rồi.
Thanh toán khác nhau theo từng mặt tiền. Trong Mini App, Zalo Checkout SDK là bắt buộc và mọi đơn phải được ký ở server; trên web, VNPay và MoMo chuyển hướng theo cách thông thường; COD có ở cả hai. Ba đường thanh toán, một máy trạng thái đơn — cả ba đều đổ về cùng một chỗ xác thực webhook và cùng một bước chuyển trạng thái idempotent.
Thông báo cũng tách y như vậy: ZNS cho khách, những người sống trong Zalo, và FCM cho tài xế, những người sống trong app với màn hình luôn sáng.
Nếu làm lại
Tôi sẽ viết package type dùng chung trước client đầu tiên thay vì sau client thứ hai. Một tuần ngồi hoà giải ba cách hiểu hơi khác nhau về trạng thái một booking là một tuần tôi đã được cảnh báo trước.