Get a quote

Building a Real-Time Order Tracking System: State Machines and Live Updates for SaaS Operations

Order tracking systems fail when state transitions are unguarded and updates rely on polling. This is the architecture we use: explicit state machines, event logs, and Server-Sent Events for real-time delivery to clients.

Any SaaS platform that handles orders needs a reliable order tracking system. The problem is rarely the existence of order states; every system has those. The problem is managing state transitions correctly and delivering updates to clients in real time without hammering the database with polling requests. This is the architecture we build for SaaS platforms across Lebanon and the MENA region.

The state machine problem: why unguarded status columns break production

The most common failure pattern in order tracking systems is storing the current order status as a plain text column in a database table with no transition rules enforced in the application layer. A developer writes a status update query directly to change a row from "cancelled" back to "preparing" to recover from an error. The query succeeds because the database does not know that "cancelled" is a terminal state.

The result: corrupted order history, inconsistent downstream events, and support tickets where the order shows as both completed and in progress simultaneously.

The fix is an explicit state machine defined in the backend code that validates every transition before any database write:

var allowedTransitions = map[OrderStatus][]OrderStatus{
    StatusPending:    {StatusConfirmed, StatusCancelled},
    StatusConfirmed:  {StatusPreparing, StatusCancelled},
    StatusPreparing:  {StatusReady},
    StatusReady:      {StatusDelivering, StatusCompleted},
    StatusDelivering: {StatusCompleted},
    StatusCompleted:  {},
    StatusCancelled:  {},
}

func (s OrderStatus) CanTransitionTo(next OrderStatus) bool {
    for _, a := range allowedTransitions[s] {
        if a == next {
            return true
        }
    }
    return false
}

Before any status change, the service layer validates that the transition is allowed. If not, it returns a typed error that surfaces to the caller as a 422 Unprocessable Entity response. The database never sees an invalid transition.

Storing state transitions as events, not just current state

Storing only the current order status answers "what state is this order in now?" but not "how did it get here, when, and who changed it?" The latter questions are the ones that matter in an operational support context.

Add an event log table alongside the current state:

CREATE TABLE order_status_events (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id    UUID NOT NULL REFERENCES orders(id),
  tenant_id   UUID NOT NULL,
  from_status TEXT,
  to_status   TEXT NOT NULL,
  changed_by  UUID,
  occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  metadata    JSONB
);

Every status transition writes one row to this table atomically with the update to the orders table (single transaction, both writes or neither). This log enables full audit trails, per-tenant SLA analysis (average time from confirmed to ready by restaurant, by shift, by day), and debugging of production anomalies.

Delivering real-time updates with Server-Sent Events

The naive implementation: the client polls the API every 2 seconds for order status. With 1000 concurrent active orders, this is 500 database queries per second of constant load for the sole purpose of delivering the information that the order is still in the same state it was 2 seconds ago.

Server-Sent Events (SSE) eliminate the polling load. The server keeps an HTTP connection open to the client and pushes updates only when they happen. The client receives updates in real time and the server generates zero polling load.

func (h *Handler) StreamOrderStatus(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")

    orderID := chi.URLParam(r, "orderID")
    updates := h.hub.Subscribe(orderID)
    defer h.hub.Unsubscribe(orderID, updates)

    for {
        select {
        case event := <-updates:
            fmt.Fprintf(w, "data: %s\n\n", event)
            w.(http.Flusher).Flush()
        case <-r.Context().Done():
            return
        }
    }
}

When an order's status changes, the service layer publishes to the hub after committing the database transaction. The hub fans out the update to all open SSE connections subscribed to that order ID.

Handling reconnection and missed events

SSE connections drop when clients move between networks, put devices to sleep, or experience brief connectivity interruptions. SSE's native reconnection mechanism sends the client's last received event ID in a Last-Event-ID header when reconnecting.

Include an event ID in every SSE message:

id: 1748358060-abc123
event: status_update
data: {"order_id":"abc123","status":"ready","updated_at":"2026-05-28T12:34:00Z"}

On reconnection, query the order_status_events table for all events newer than the Last-Event-ID timestamp for this order and stream them before resuming the live event stream. The client catches up on everything it missed during the disconnection window.

Multi-tenant concerns for MENA SaaS

In a multi-tenant SaaS deployment, SSE connections must be scoped to the authenticated tenant. Enforce this in the hub subscription layer, not just at the HTTP authentication middleware level. A subscriber registering for order ID updates must be verified as a member of the tenant that owns that order before the subscription is accepted.

Use the tenant context propagation pattern to carry tenant identity from the HTTP middleware through the SSE handler to the hub subscription check.


Key lessons from production

Define order state transitions explicitly in code, not as arbitrary database column values. Log every transition to an event table, not just the current status. Use SSE instead of polling for real-time delivery. Include event IDs in SSE messages to support catch-up on reconnection. Scope hub subscriptions to the tenant at the subscription layer.


Need help building order tracking for your platform?

Voxire builds SaaS backends and real-time operational systems for companies across Lebanon and the MENA region. If your platform handles orders and needs reliable real-time state management, we can help with the architecture.

https://voxire.com/get-a-quote/

Free PDF Download

Enjoying this article?

Enter your email and get a clean, formatted PDF of this article - free, no spam.

Free. No spam. Unsubscribe any time.

Back to blog
Chat on WhatsApp