Ride Event Logs
Every support ticket was an archaeology problem. A domain event stream that answers who did what, from where, and when — for rides nobody can reproduce.
- Period
- 2026
- Role
- Backend Engineer — designed the event model and instrumented the ride lifecycle
- Node.js
- Express.js
- MySQL
- Redis
- Bull
Production context
- Coverage
- Full ride lifecycle
- Actors
- Passenger · Driver · Admin · System
- Execution
- HTTP · cron · queue workers
- Write path
- Async / non-blocking
- Invariant
- Observability must never break a ride
Overview
An append-only event stream covering the full ride lifecycle on a multi-tenant mobility platform. Every meaningful transition — requested, offered, accepted, rejected, arrived, started, ended, cancelled, retried, abandoned — is recorded with the actor who caused it, the application they acted from, and any event-specific detail. Reconstructing what happened to a ride becomes one query instead of an investigation.
The Problem
With many white-labeled clients live, most support requests arrive in the same shape: a specific ride behaved unexpectedly, and nobody can reproduce it. The ride is over, the state machine has moved on, and the current row in the database records where the ride ended up — not how it got there.
Answering those questions meant correlating application logs across services by timestamp, guessing at which code path had run, and frequently concluding that the outcome was consistent with several different sequences of events. The information needed was cheap to record and impossible to recover afterwards.
Requirements
- Reconstruct a ride's full history from its identifier alone
- Distinguish who acted — customer, driver, admin, or the system itself
- Distinguish where they acted from, since the same action arrives through several applications
- Cover automated actors: cron jobs, retry workers, and timeouts are the hardest outcomes to explain
- Per-client opt-in, because write volume is real and not every tenant needs it
- Never able to break a ride — an observability failure must not become a booking failure
Architecture
Events are domain facts, not HTTP records. Each entry carries a stable event code naming what happened in the platform's own vocabulary, plus the ride or request it belongs to, the tenant and city, and whether the ride was on-demand or scheduled. Event-specific detail lives in a JSON column rather than in columns that would be null for most event types.
The dimension that does the most work is the actor pair: who caused this, and from which application. A cancellation is a single outcome with several very different explanations — the passenger cancelled, the driver cancelled, an operator cancelled from the admin panel, or the system cancelled after dispatch was exhausted. Recording the outcome without the actor answers the shallow question and leaves the real one open.
The system is a first-class actor for exactly that reason. Cron-driven transitions, retry workers, and dispatch timeouts produce the outcomes clients most often escalate, precisely because no human was involved and nobody can describe what they did.
Trade-offs
- Event-specific detail as JSON keeps the schema narrow and gives up the query guarantees a typed column would provide — accepted, because the alternative is a wide sparse table that grows with every new event type
- Instrumentation is spread across every lifecycle handler rather than centralised, which is more places to forget and the only way to capture intent; a central interceptor sees requests, not meaning
- Per-client opt-in avoids charging write volume to tenants who won't read it, and means the data isn't there when a client turns it on after the incident they wanted it for
- Stable string event codes are readable in a support context and unenforced at the database level, so consistency depends on discipline rather than the schema
Challenges
Attribution is harder than it looks. Several endpoints serve both an end-user application and the admin panel acting on that user's behalf, so the acting identity has to be resolved from the authenticated context rather than accepted from the caller — otherwise the audit trail records whoever the request claims to be, which is precisely the property an audit trail cannot afford to lack.
Instrumenting the automated paths meant carrying tenant and city context into places that had never needed it — cron loops and queue workers operate on rows, not sessions, and reconstructing the context an event needs is more work there than in a request handler.
Granularity was a judgment call throughout. Too few events and the timeline has gaps exactly where the interesting thing happened; too many and the noise reproduces the problem the system was built to solve. The rule that held: record decisions and transitions, not steps.
Lessons Learned
Observability that answers business questions is a different system from observability that answers technical ones. Request logs tell you what the server did; event logs tell you what happened to the ride, and support tickets are always about the second.
The actor is the field that matters. Outcomes are usually already visible in the data — what is missing, and unrecoverable later, is who caused them and from where.
Anything that observes a system must be incapable of breaking it. A logging write that can throw into a ride transition has converted an observability feature into a new failure mode, which is a strictly worse position than having no logging at all.