Một shipment object xuyên kho, điều phối, và chặng cuối

Vì sao ba record cho một kiện hàng vật lý làm vỡ tracking, và một data model shipment tối giản trông ra sao khi dùng chung giữa WMS, TMS, và last-mile.

Phần lớn 3PL WOKA ngồi làm việc cùng đều chạy ba hệ thống cho ba chặng của cùng một kiện hàng vật lý: WMS cho kho, TMS cho điều phối, và một last-mile app cho tài xế. Mỗi hệ thống giữ một record. Mỗi hệ thống cấp ID riêng. Mỗi hệ thống có hàng đợi sự cố riêng.

Chi phí vận hành hiện ra âm thầm: khách gọi hỏi kiện hàng đang ở đâu, ops lead phải bật ba màn hình, đối chiếu ba ID, và ráp lại timeline bằng tay. Một nửa số lần thậm chí không ai chắc hệ thống nào đang giữ sự thật khi các timestamp lệch nhau.

Luận điểm của bài này: một shipment là một object, không phải ba record. Kiện hàng vật lý có một danh tính; data model nên phản ánh đúng điều đó.

Vì sao ba record là default sai

Lý do hấp dẫn khi để ba record là vì mỗi hệ thống có một góc nhìn khác nhau. WMS quan tâm bin và pick status. TMS quan tâm driver assignment và route sequence. Last-mile quan tâm POD và sự cố reason. Nên quyết định engineering ban đầu thường là: để mỗi hệ thống sở hữu object của mình, rồi nối chúng bằng một order_id chung.

Kiến trúc đó chạy ổn cho tới khi vỡ. Những failure mode rất quen thuộc:

  • Hand-off drift. Pick xong lúc 10:42; dispatch chỉ thấy lúc 10:47 vì sync job chạy mỗi 5 phút. Một tài xế rời kho mà không mang theo kiện đã sẵn sàng từ 5 phút trước.
  • Exception rải rác. Lỗi địa chỉ tài xế phát hiện lúc 14h không tới được đội kho — những người duy nhất có thể chuyển SKU sang khách khác trong ngày.
  • Dựng lại timeline. Khi khách escalate, ráp sự thật từ log của ba hệ thống ngốn cả buổi chiều của một support engineer.

Một data contract shipment tối giản

Thay vì ba record, WOKA mô hình hóa một shipment duy nhất với năm trường chịu tải chính và một enum trạng thái:

type Shipment = {
  shipment_id: string;            // opaque, cấp lúc nhận hàng
  stage: 'received' | 'picked' | 'dispatched' | 'in_transit' | 'delivered' | 'sự cố';
  handoff_at: Record<Stage, Date>; // thời điểm từng chặng đóng
  exception_ref: string | null;    // trỏ về hàng đợi sự cố dùng chung
  sla_deadline: Date;              // con số duy nhất mọi người hay tranh
};

WMS, TMS, và last-mile đều đọcghi cùng object này. Mỗi hệ thống vẫn giữ dữ liệu riêng của domain mình — bin, route sequence, ảnh POD — nhưng chúng sống dưới dạng foreign reference, không phải bản sao của stage hay deadline.

Data contract cũng cấm một thứ: không stage nào tiến được nếu thiếu handoff timestamp. Nếu TMS muốn đánh dấu shipment là dispatched, handoff_at.picked phải đã tồn tại. Riêng ràng buộc này loại bỏ phần lớn drift trước đây WOKA phải chạy theo.

Cơ chế bàn giao

Hand-off là chỗ unified model đáng tiền nhất. Vài pattern WOKA chốt:

  • Pick xong → dispatch ready. Khi handoff_at.picked được ghi, TMS thấy một row xuất hiện trong hàng đợi ready ở event loop tiếp theo — không phải chờ sync 5 phút. WOKA dùng một webhook fan-out đơn giản, delivery at-least-once, consumer idempotent.
  • POD → last-mile đóng. Tài xế capture POD sẽ ghi handoff_at.delivered đính kèm ảnh POD. Đội kho không còn thấy shipment này trong danh sách case mở.
  • Exception queue dùng chung. Chỉ có đúng một bảng exceptions. Một pick bị kẹt, một dispatch thất bại, và một ca giao bị từ chối đều nằm chung một schema. Ops phân loại từ một màn hình duy nhất.

Đánh đổi: khi nào KHÔNG nên unify

Unify là một khoản đầu tư. Bạn trả bằng schema design, migration, và những buổi trao đổi cross-team về việc ai sở hữu field nào. Có những trường hợp giữ ba record lại là lựa chọn đúng:

  • 3PL hợp đồng cho một chặng thôi. Nếu bạn chỉ vận hành kho và khách của bạn tự lo dispatch, unify ra ngoài biên của mình là over-engineer. Bạn sẽ bơm dữ liệu vào một model mà mình không thể giữ đồng bộ.
  • Quy định không đồng nhất. Nếu dữ liệu kho phải nằm on-prem vì lý do hải quan và dispatch chạy trên cloud, unified model biến thành cây cầu, không phải một store duy nhất — thêm latency, thêm failure mode.
  • Dưới 20 shipment mỗi ngày. Spreadsheet và group chat vẫn còn chạy được. Engineering effort để chỗ khác xứng đáng hơn.

Một con số WOKA nhìn

Sau khi roll out với một 3PL khách, độ trễ bàn giao WOKA quan tâm nhất — p95(handoff_at.picked → handoff_at.dispatched) — giảm từ trần 5 phút của sync job xuống dưới 15 giây. Không phải vì thao tác vật lý nhanh lên, mà vì sync job thoát ra khỏi critical path. Giờ xuất phát của tài xế đi theo.

WOKA không công bố con số 10× kêu to. Thắng thực tế là việc xử lý sự cố chuyển từ “thám tử ba hệ thống” sang “mở queue, tìm đúng row.” Với WOKA chi phí engineering đáng trả cho điều đó mỗi lần.