Logistics companies, restaurant chains, and service businesses across Lebanon and MENA face the same operational challenge: drivers in the field, customers waiting for updates, and managers who need real-time visibility. Third-party fleet tracking platforms solve this at enterprise prices with limited customization. Building a purpose-built tracking system in Go gives you full control over logic, costs, and multi-tenant data isolation.
Logistics companies, restaurant chains, and service businesses across Lebanon and MENA face the same operational challenge: drivers in the field, customers waiting for updates, and managers who need real-time visibility. Third-party fleet tracking platforms solve this at enterprise prices with limited customization. Building a purpose-built tracking system in Go gives you full control over logic, costs, and multi-tenant data isolation.
What a production fleet tracking system actually needs
The visible part of fleet tracking is straightforward: push GPS coordinates from a mobile app to a server and show them on a map. The non-obvious engineering problems are what separate a demo from a production system:
- Drivers disconnect and reconnect frequently. The system must maintain last-known position across reconnections without losing data.
- Location history must be stored efficiently. A fleet of 200 drivers each sending updates every 8 seconds generates significant write volume.
- Dashboard subscribers must receive updates without blocking the ingestion path. One slow browser tab must not delay updates for everyone else.
- In multi-tenant deployments, Company A must never see Company B's driver positions.
- ETA calculations need to be accurate enough to be useful under variable traffic conditions in cities like Beirut, Riyadh, and Cairo.
Go handles the concurrency requirements here better than most alternatives. Each active driver connection is a goroutine. Each dashboard subscriber is a goroutine. The Go scheduler manages thousands of these with minimal memory overhead.
Architecture: three layers with clean boundaries
A fleet tracking backend separates into three layers that scale independently:
Ingestion layer: accepts location updates from mobile clients over WebSocket or HTTP POST. Validates, authenticates, and forwards to the hub. Optimized for write throughput.
Distribution hub: maintains a registry of active drivers and dashboard subscribers, routes updates to the correct tenant's subscribers, and caches last-known positions in Redis.
Persistence layer: writes location history to PostgreSQL using buffered batch inserts that reduce per-update write overhead by 80 to 90 percent compared to individual inserts.
type LocationUpdate struct {
DriverID string `json:"driver_id"`
TenantID string `json:"tenant_id"`
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
Speed float64 `json:"speed"`
Heading float64 `json:"heading"`
Timestamp int64 `json:"timestamp"`
}
type TrackingHub struct {
mu sync.RWMutex
subscribers map[string][]chan LocationUpdate // keyed by tenantID
lastKnown map[string]LocationUpdate // keyed by driverID
}
func (h *TrackingHub) Broadcast(update LocationUpdate) {
h.mu.Lock()
h.lastKnown[update.DriverID] = update
h.mu.Unlock()
h.mu.RLock()
subs := h.subscribers[update.TenantID]
h.mu.RUnlock()
for _, ch := range subs {
select {
case ch <- update:
default:
// subscriber is slow, skip this update rather than block ingestion
}
}
}
The non-blocking send (select default) is critical. A slow dashboard client must never block location updates from reaching other subscribers or slow down driver ingestion.
PostgreSQL schema for movement history
Storing every location update in a single table becomes a performance and maintenance problem at scale. A partitioned table organized by month is the production-safe approach:
CREATE TABLE driver_locations (
id BIGSERIAL,
driver_id UUID NOT NULL,
tenant_id UUID NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lng DOUBLE PRECISION NOT NULL,
speed REAL,
heading REAL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
) PARTITION BY RANGE (recorded_at);
CREATE TABLE driver_locations_2026_06
PARTITION OF driver_locations
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
CREATE INDEX ON driver_locations (driver_id, recorded_at DESC);
CREATE INDEX ON driver_locations (tenant_id, recorded_at DESC);
Monthly partitions mean that dropping old data is a DROP TABLE on a single partition rather than a large DELETE that locks the table.
Buffered batch writes for high-frequency location data
With 200 active drivers each sending a location update every 8 seconds, you have 25 writes per second. That is manageable but grows quickly. Buffered batch writes reduce this to a fraction of the individual write cost:
type LocationBuffer struct {
mu sync.Mutex
pending []LocationUpdate
}
func (b *LocationBuffer) Add(update LocationUpdate) {
b.mu.Lock()
b.pending = append(b.pending, update)
b.mu.Unlock()
}
func (b *LocationBuffer) Flush(ctx context.Context, db *pgxpool.Pool) error {
b.mu.Lock()
batch := b.pending
b.pending = b.pending[:0]
b.mu.Unlock()
if len(batch) == 0 {
return nil
}
return bulkInsertLocations(ctx, db, batch)
}
A ticker runs Flush every 5 seconds. 200 drivers generating 1,600 updates over 5 seconds become one batch insert rather than 1,600 individual round trips.
Fast last-known position with Redis
Dashboard users need the current position of every active driver the moment they open the interface, without waiting for the next WebSocket update. Redis holds the last-known position of every driver with a TTL:
func (s *TrackingService) SetDriverLocation(ctx context.Context, update LocationUpdate) error {
data, err := json.Marshal(update)
if err != nil {
return err
}
key := fmt.Sprintf("driver:loc:%s", update.DriverID)
return s.redis.Set(ctx, key, data, 2*time.Hour).Err()
}
func (s *TrackingService) GetDriverLocation(ctx context.Context, driverID string) (*LocationUpdate, error) {
key := fmt.Sprintf("driver:loc:%s", driverID)
data, err := s.redis.Get(ctx, key).Bytes()
if err == redis.Nil {
// Fall back to PostgreSQL for recent history
return s.db.GetLastDriverLocation(ctx, driverID)
}
if err != nil {
return nil, err
}
var loc LocationUpdate
return &loc, json.Unmarshal(data, &loc)
}
Redis handles latency-sensitive reads. PostgreSQL handles durability and queryable movement history. The 2-hour TTL means drivers that go offline are naturally cleaned from the fast cache.
ETA calculation without external API costs
For applications where Google Maps API costs matter at scale, Haversine distance with average speed gives a useful approximation:
func haversineKm(lat1, lng1, lat2, lng2 float64) float64 {
const R = 6371.0
dLat := (lat2 - lat1) * math.Pi / 180
dLng := (lng2 - lng1) * math.Pi / 180
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*
math.Sin(dLng/2)*math.Sin(dLng/2)
return R * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
}
func estimateETA(driverLat, driverLng, destLat, destLng, avgSpeedKmh float64) time.Duration {
distKm := haversineKm(driverLat, driverLng, destLat, destLng)
hours := distKm / avgSpeedKmh
return time.Duration(hours * float64(time.Hour))
}
For high-accuracy ETA requirements in city traffic conditions across MENA, HERE Maps and TomTom both offer regional traffic data at pricing suited for deployment-scale volumes.
Automatic monthly partition creation
Monthly partitions need to exist before data arrives. A scheduled job creates the next month's partition at the start of each month:
func (m *PartitionManager) EnsureNextMonthPartition(ctx context.Context) error {
next := time.Now().AddDate(0, 1, 0)
start := time.Date(next.Year(), next.Month(), 1, 0, 0, 0, 0, time.UTC)
end := start.AddDate(0, 1, 0)
tableName := fmt.Sprintf("driver_locations_%d_%02d", start.Year(), start.Month())
_, err := m.db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s
PARTITION OF driver_locations
FOR VALUES FROM ('%s') TO ('%s')
`, tableName, start.Format("2006-01-02"), end.Format("2006-01-02")))
return err
}
Key lessons from production
The non-blocking broadcast pattern is non-negotiable. A single slow subscriber blocking the hub will cause visible lag for every other dashboard user in the same tenant.
Redis for last-known position and PostgreSQL for history is the correct architectural split. Do not try to serve real-time dashboard requests from PostgreSQL directly.
Design buffered batch writes before going to production. Adding them after the first load test under real fleet volume is significantly more disruptive than building them from the start.
Plan partition management from day one. Monthly partitions created automatically by a scheduled job are far more reliable than remembering to create them manually at the start of each month.
Enjoying this article?
Enter your email and get a clean, formatted PDF of this article - free, no spam.
Not sure where to start?
Voxire builds production operational systems for logistics, restaurant, and delivery companies across Lebanon and the MENA region. If you are designing a fleet tracking or real-time delivery operations platform, reach out at https://voxire.com/get-a-quote/


