Event-Driven Architecture for Travel Booking
How events flow through the system when a traveler books a flight, and why decoupling matters — event types, eventual consistency, and monitoring event health.

A traveler taps "Confirm booking." One action. Behind that tap, seven things happen: the booking record is created, the payment is captured, a confirmation email is sent, a push notification fires, a PDF document is generated, the analytics system records the conversion, and the trip record is updated with the new booking. None of these seven things should block the traveler's confirmation screen. The confirmation should appear in under 2 seconds. The email can arrive in 8.
This is the core principle of our event-driven architecture: the synchronous path is minimal (confirm the booking, show the success), and everything else happens asynchronously through events. The traveler doesn't wait for the email to send. The email doesn't wait for the document to generate. Each downstream action is triggered by an event and processes independently.
Event types in travel booking

Our system produces several categories of events.
Booking events. `booking.created` when the booking is confirmed. `booking.updated` when the booking status changes (confirmed by provider, ticketed, cancelled). `booking.payment.captured` when the payment is successfully charged. These are the highest-priority events because they represent financial transactions.
Notification events. `notification.email.send` when an email needs to be sent. `notification.push.send` when a push notification needs to be sent. `notification.inapp.create` when an in-app notification needs to be displayed. These are triggered by booking events and other user actions.
Document events. `document.generate` when a travel document (booking confirmation PDF, itinerary summary) needs to be created. `document.ready` when the generation is complete and the document is available for download.
Analytics events. `analytics.booking.completed` with the booking value, destination, and conversion path. `analytics.search.completed` with the search parameters and result count. These drive the reporting dashboards and business metrics.
External events. Events that originate outside our system. Payment processor webhooks fire when a payment status changes. Travel data provider webhooks fire when a booking is ticketed or when a schedule change affects a booking. These external events enter our system through webhook handlers and are converted into internal events.
Event bus vs. direct service calls
Not every interaction should be event-driven. The question is: when should service A call service B directly, and when should service A emit an event that service B reacts to?
Direct calls are appropriate when the caller needs the result immediately. The booking endpoint needs to know whether the payment succeeded before returning a confirmation. This is a synchronous dependency. The booking endpoint calls the payment service directly and waits for the result.
Events are appropriate when the caller doesn't need the result, or when multiple consumers need to react to the same action. The booking endpoint doesn't need to wait for the email to send. It doesn't need to wait for the document to generate. It doesn't need to wait for the analytics to record. These are all fire-and-forget from the booking endpoint's perspective.
The rule we follow: if the traveler is waiting for the result, it's a direct call. If the traveler has already received their response, it's an event.
Our event bus is implemented through job queues. Seven named queues handle different event categories: booking operations, email notifications, push notifications, document generation, analytics processing, and others. When the booking endpoint confirms a booking, it enqueues events on the relevant queues. Dedicated worker processes consume events from each queue and process them.
The queues provide reliability guarantees that a simple event emitter doesn't. Events are persisted to disk. If a worker crashes during processing, the event is retried. If processing fails repeatedly, the event moves to a dead letter queue for manual inspection. Events are not lost.
Eventual consistency

Event-driven architecture means eventual consistency. The booking is confirmed (synchronously), but the email hasn't sent yet (asynchronously). The trip record hasn't updated yet. The analytics haven't recorded yet. These will happen, but not instantly.
For the traveler, this means the confirmation screen says "Booking confirmed" while the confirmation email is still being composed. The trip details page might not show the new booking for a few seconds while the trip sync event processes. The app experience is designed for this: the confirmation screen itself contains all the booking details the traveler needs. They don't need to check email or navigate to the trip page to know their booking succeeded.
Our eventual consistency timeline for a typical booking: the booking is confirmed at T+0 (synchronous). The confirmation email sends at T+3 seconds. The push notification fires at T+5 seconds. The trip record updates at T+5 seconds. The booking confirmation PDF generates at T+10 seconds. Analytics record at T+8 seconds. All downstream processing completes within 15 seconds of the booking confirmation.
The consistency timeline has SLAs. Emails must send within 30 seconds. Push notifications within 15 seconds. Trip sync within 10 seconds. If these SLAs are breached, monitoring alerts fire and we investigate. The traveler won't notice a 10-second delay in email delivery. They will notice a 5-minute delay.
Event ordering and deduplication
Events can arrive out of order. A `booking.payment.captured` event might be processed before the `booking.created` event if the payment queue has more available workers. The payment capture handler must tolerate this: if the booking record doesn't exist yet, the handler should retry after a short delay, not fail permanently.
We handle ordering by making handlers idempotent and tolerant of missing prerequisites. Each handler checks whether its preconditions are met. If not, it retries with a backoff. If the preconditions are met, it processes and records that it has processed this event (to prevent duplicate processing).
Deduplication is essential because events can be delivered more than once. The queue guarantees at-least-once delivery, not exactly-once. A worker that crashes after processing an event but before acknowledging it will receive the event again when it restarts.
Each event has a unique ID. Each handler records the IDs of events it has processed. Before processing an event, the handler checks whether it has already processed that ID. If yes, it skips processing and acknowledges the event. This is the standard idempotent consumer pattern.
For booking events specifically, the idempotency is critical. Processing a `booking.created` event twice should not create two booking records. Processing a `notification.email.send` event twice should not send two identical emails (though sending a duplicate email is less harmful than creating a duplicate booking).
Monitoring event flow health
An event-driven system fails silently. If the email queue stops processing, no error appears in the API server's logs. The booking endpoint succeeded. The traveler got their confirmation. They just didn't get their email. Nobody notices until the traveler contacts support.
We monitor event flow health through several signals.
Queue depth. The number of unprocessed events in each queue. Normal depth varies by queue (the analytics queue might always have a few events, the booking queue should be near zero). A depth that grows continuously indicates that processing is slower than production. A sudden spike indicates either a burst of events or a worker failure.
Processing latency. The time between event production and event processing. This directly maps to the eventual consistency timeline. If email events are taking 60 seconds to process instead of 3, the traveler is waiting a full minute for their confirmation email.
Error rate. The percentage of events that fail processing. Failed events are retried, so a brief spike in errors might be transient. A sustained error rate indicates a systemic problem: a misconfigured email service, a document generation bug, or a database issue.
Dead letter queue depth. Events that failed all retry attempts. These events represent work that didn't happen. A booking event in the dead letter queue means a traveler didn't get their confirmation email despite multiple attempts. Dead letter events get investigated immediately.
We have dashboards that show the health of each queue in real time. The dashboard shows current depth, processing rate, error rate, and dead letter count. When any metric exceeds its threshold, an alert fires.
Design event-driven architecture for your booking system
If you're building a booking system, here's the approach to event-driven architecture.
Identify the synchronous core. What must the traveler wait for? Booking confirmation and payment capture. Everything else can be async.
Define your event catalog. List every event, its producer, its consumers, and its SLA. The catalog is the contract between producers and consumers. It's a document that the team references when adding new features or debugging issues.
Make every handler idempotent. At-least-once delivery means handlers will occasionally process the same event twice. If your handler isn't idempotent, duplicate processing will cause data inconsistency.
Monitor queue health aggressively. Silent failures are the biggest risk of event-driven architecture. Monitor depth, latency, error rate, and dead letters for every queue.
Set SLAs for eventual consistency. "Eventually" isn't good enough. "Within 30 seconds" is. Measure against your SLAs and alert when they're breached.
Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. Plan your next trip.