SS2SYSTEMS
How an order moves through my delivery platform
What this is
A delivery platform I built: a customer orders from a merchant, the nearest free driver is sent to deliver it, and the customer watches them arrive on a map. It is my own project, not a client's, and it is the one written up in SS1 / Work.
- Built with
- Go, PostgreSQL with PostGIS, and a Flutter app for web, Android and iOS
- Shape
- Eight modules: identity, merchants, jobs, payments, dispatch, notifications, reporting and a gateway
- The hard part
- Orders that fail halfway: a declined card, a kitchen that says no, no driver free
How to read the diagram
- Boxes are the apps and modules.
- The bar is the event log. Modules talk by publishing events to it and reading from it, not by calling each other's databases.
- The moving dot is the message for the current step.
- Green dashed boxes marked FAIL or HOLD are where something has gone wrong.
Follow one order
- GATEWAY
- token checked
- JOBS
- token checked again
01 / 07
A customer places an order
The app sends the order to the gateway, the only public port. The gateway checks the login token and passes the request on, and the jobs module checks the token again itself. Nothing behind the gateway trusts a header.
POST /jobs02 / 07
Jobs prices it and saves it
Prices come from the merchant’s catalogue, never from the app. The job and an outbox row are written in one database transaction, then a relay publishes the event, so it cannot be lost between the two.
GET /cataloguejobs.created03 / 07
Payment and the merchant answer
Both read the same event on their own. Payments charges the order, keyed by the job id, so a repeated event can never charge twice. The merchant re-checks the items and prices and accepts.
jobs.createdpayments.completedmerchant.job-accepted04 / 07
Confirmed, then cooked
The job only becomes confirmed once it holds both facts, paid and accepted. Staff mark it ready when the food is done, and the job reaches the ready-for-dispatch milestone.
jobs.status-changedmerchant.job-ready05 / 07
Dispatch finds a driver
Dispatch asks PostGIS for the nearest free driver to the pickup, using a spatial index, and locks that driver’s row with FOR UPDATE SKIP LOCKED. Two orders can never take the same driver.
jobs.status-changeddispatch.assigned06 / 07
The customer watches it move
The driver’s phone sends its position. Dispatch publishes it and the gateway pushes it down a WebSocket to the customer’s map. Only the job’s customer, merchant, driver or an admin can open that socket. Notifications turns the same events into in-app messages.
PATCH /agents/{id}/locationdispatch.locationWebSocket07 / 07
Delivered and closed
When the driver completes the last drop-off, the job closes. Reporting updates its own read models for revenue, completion times and driver stats, without touching anyone else’s tables.
dispatch.stop-completedjobs.status-changed
Step 1 of 7: A customer places an order
Now break something
Each of these is handled in the real code. Pick one and step through it.
- PAYMENTS
- declined
01 / 03
The charge is declined
Payments reads jobs.created and the charge fails.
payments.failed
Decline the card, step 1 of 3: The charge is declined
Simplified: each module also has its own database tables and an outbox relay, left out of the picture. The full write-up is in the case study.