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/
Enjoying this article?
Enter your email and get a clean, formatted PDF of this article - free, no spam.


