trong.
Full-stack2026

VNMOVE

A whole island trip booked once — ride, ferry, hotel, tour, motorbike.

vnmove.ts
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,
};
Year
2026
Client
VNMOVE
Role
Lead engineer
Duration
Ongoing

Architecture

01

02

03

04

Highlight 01
5
Highlight 02
8
Highlight 03
90s

The story

Before this existed

VNMOVE sells trips to Vietnam's southern islands. But a customer's trip isn't one product — it's five: a car to the pier, a fast-ferry ticket, a hotel on the island, sometimes a tour, sometimes a motorbike to ride around on.

Five things, booked in five different places, typing the same name and phone number five times. Most people give up around the third one.

It was no tidier on the inside: drivers took rides by phone, hotels confirmed rooms over Zalo, the operations desk kept a notebook. Everyone held their own copy of the truth, and no two copies matched.

What got built

One system behind all five services: the customer books once, pays once, follows it in one place.

Around that sit eight applications for four groups of people — travellers (phone app, Zalo Mini App, web), drivers, hotel partners, and the operations desk. Eight different front doors, all reading and writing the same data, so nobody is working off their own private numbers any more.

Dispatch: fast, without the scramble

The easy way to dispatch is to blast the request at every driver and let the fastest tap win. It's convenient for whoever writes the software and bad for everyone else: drivers fight over it, and the customer has no idea who is coming.

This does the opposite. The system picks one driver — nearest, not yet asked for this trip, already heading the same way — and offers it to them alone. That driver gets 20 seconds. Decline, or let it lapse, and it moves to the next one immediately.

The whole search is capped at 90 seconds. Past that the customer is told plainly that no car is available, instead of watching a spinner and guessing.

Two things matter here operationally: two drivers can never end up on the same ride — the system settles who got it, once and for all; and a driver who loses signal and taps Accept again keeps their fare instead of getting an error and assuming they lost it.

For shared rides, a driver already running a pooled trip in the same direction gets first refusal, and a second customer is held for up to 8 seconds to be put in the same car — as long as the two pickups are within 2 km.

Money: collected properly, no orders left hanging

The four travel services moved to prepaid. Rides still take cash, because most real rides are still paid in cash to the driver, and removing that would have killed the service.

Blocking cash happens on the server, not by hiding a button in the app. The reason is practical: a customer who hasn't updated still sees the old button, taps it, creates an order — and only then hits a dead end at payment, holding something they can't complete and can't explain.

Every unpaid order now carries its own deadline, and the app counts down to that exact moment. Bank transfers get 30 minutes, because the customer has to open a banking app, scan, enter an OTP and wait for it to clear; card and wallet gateways get 15. Orders used to be cancelled on a deadline the app knew nothing about, so customers watched their order vanish mid-scan — that was a bug, fixed, not a feature.

What runs by itself

A good deal of the operation needs nobody to press anything:

  • Unpaid orders expire on their own, and the ferry seat and the hotel room go back on sale
  • Customers get a warning before their payment window closes
  • Finished tours and hotel stays close themselves, and money moves into the partner's wallet, the platform fee already taken out
  • Tour departure schedules extend themselves, and the desk is warned when sailings are running low
  • The ferry timetable is built thirty days ahead

Each of these runs on its own rhythm — releasing a seat is measured in minutes, building a timetable in days.

What partners and operations can see

Hotels manage their own rooms and rates instead of phoning someone to update them. Drivers get their own app with the trip, the route and their wallet. The operations desk sees every order, trip and payment on one screen.

All of it reads from the same source, so the partner's numbers and the desk's numbers are the same numbers.

What's still owed

The system runs comfortably at today's volume. Growing several times over means paying off two known pieces of work first — identified and planned for, not the kind of thing discovered during an outage.

And four phone apps means every release waits on Apple and Google review. That's time to plan a feature schedule around, not something you can negotiate.

Diagrams & screens

01Who uses what, and what they all look at
Four audiences, one source of truthTravellers3 ways to bookphone app · Zalo Mini Appand the webDriversTheir own apptrips, routes and walletPartnersApp and webhotels manage rooms and ratesOperationsApp and webevery order, trip and paymentOne shared systemfive services running througha single booking flowServing peoplebooking, payment,live dispatchRunning the backgroundreleasing stock, closing orders,paying partners, building schedulesShared datacustomers · orders · trips · walletsDriver positionsupdated continuouslyPhotos and documentsstored in one placeThe people-facing side and the background side are kept apart, so a heavy report never slows down a ride.Partners and operations read the same source, so their numbers always agree — no more two sets of books.

Travellers, drivers, partners and operations each get their own way in, all reading one source — so everyone is looking at the same numbers.

02How one ride finds a driver
Offered in turn, not raced forCustomer booksthe search for a driverbeginsPick one drivernearest, not yet askedalready heading the same wayOffer it to them alone20 seconds to acceptDriver assignedexactly one driver gets it —never two on one rideNo car availablethe customer is told plainlyAcceptsDeclines, or lets 20s pass → offer it to the next one90 seconds for the whole loopRide poolingBefore asking someone new, a driver already holding an open pool group going the same way gets firstrefusal. Two customers booking at almost the same time: the second waits up to 8 seconds to be grouped intothe same car — if the two pickups are within 2 km of each other.

Not a broadcast where the fastest tap wins. One driver at a time, twenty seconds each, ninety seconds for the whole search — and never two drivers on one ride.

03What each service takes
Five services · five ways to payCashVNPayMoMoZaloPayBank transferHold windowRidesno deadlineFerry tickets15′ gateway · 30′ bankHotels15′ gateway · 30′ bankTours15′ gateway · 30′ bankMotorbike hire15′ gateway · 30′ bankRides keep cash because most real rides are still paid in cash to the driver. The four travel services moved toprepaid, while older cash orders still reconcile as normal.Blocked on the server rather than hidden in the app: a customer who has not updated still sees the old button and endsup holding an order they cannot pay.

Five services, five ways to pay, and one row that differs: rides still take cash, the four travel services are prepaid only.

04What the worker sweeps while nobody looks
Five sweeps, five different beats1′Cancel expired ordersferry releases the seat,tours and hotels the slot2′Warn before expirymust tick faster than the5-minute warning window10′Close finished tripsrevenue into the partnerwallet, platform fee removed6hSchedules and stockextend tour departures, warnadmin when sailings run low24hGenerate sailings30 days ahead, from therecurring timetableNone of this needs anyone to press anything, and each rhythm is chosen around what it serves.

A ferry seat gets released, a partner wallet gets credited, a month of tour departures gets built — none of it with anybody pressing a button.

Gallery

Booking flow in the Mini App
Dispatch console
Driver app