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
  1. 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 /jobs

  2. 02 / 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.created

  3. 03 / 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-accepted

  4. 04 / 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-ready

  5. 05 / 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.assigned

  6. 06 / 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.locationWebSocket

  7. 07 / 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.

Choose a failure
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.