How to Build a Resilient Travel API Aggregator with Laravel
Building a travel API aggregator requires more than connecting multiple suppliers. Learn how to handle timeouts, retries, circuit breakers, concurrent requests, and graceful degradation with Laravel.
- ota
- flight
How to Build a Resilient Travel API Aggregator with Laravel
Building an Online Travel Agency (OTA) is not simply about connecting a few APIs.
When your platform depends on multiple flight, hotel, bus, train, eSIM, and attraction suppliers, you are also depending on systems that you don't control.
One supplier might respond in 2 seconds.
Another might take 10 seconds.
Another might timeout completely.
The real challenge is:
How do you keep your application fast and reliable when your external dependencies are not?
In this article, we'll look at practical techniques for building a resilient travel API aggregator with Laravel.
The Problem With Multiple Suppliers
Imagine a flight search request going through five suppliers:
┌── Supplier A → 2s ✅
│
User → OTA → Search ├── Supplier B → 5s ✅
│
├── Supplier C → 18s ⚠️
│
├── Supplier D → Timeout ❌
│
└── Supplier E → 3s ✅
If your application waits indefinitely for every supplier, one slow API can make the entire search slow.
This becomes even more important when you're handling thousands of search requests.
The solution is not to make every supplier reliable.
The solution is to make your system resilient when suppliers aren't reliable.
1. Always Set API Timeouts
Never allow an external API request to wait indefinitely.
Laravel's HTTP client makes it easy to define a timeout:
use Illuminate\Support\Facades\Http; $response = Http::timeout(15) ->connectTimeout(3) ->get($supplierUrl);
Here, the connection gets a maximum of 3 seconds to establish, while the complete request has a 15-second timeout.
A timeout protects your application from getting stuck waiting for an unhealthy supplier.
Why timeouts matter
Without a timeout, a slow external dependency can:
- Keep PHP workers busy
- Increase request latency
- Consume server resources
- Increase queue processing time
- Create cascading failures
- Eventually affect healthy suppliers and users
A timeout is one of the simplest resilience mechanisms you can add.
2. Use Retries Carefully
Temporary network failures don't always mean that the supplier is unavailable.
For temporary failures, a retry can help:
$response = Http::retry( 2, [500, 1500] ) ->timeout(15) ->get($supplierUrl);
The application can retry the request after 500ms and then 1500ms.
However, retries should not be blindly applied to every operation.
For example, retrying a flight search may be relatively safe, but retrying a booking or payment request requires much more careful handling.
Why?
Imagine this:
Client → Booking API → Supplier
↓
Booking Created
↓
Network Timeout
↓
Client receives error
The client might think the booking failed, while the supplier actually created it.
Automatically retrying the same booking request could potentially create a duplicate transaction.
For critical operations, use idempotency keys, unique transaction IDs, or supplier-supported booking references where appropriate.
3. Use Circuit Breakers
What happens when a supplier continuously fails?
Continuing to send requests to that supplier wastes resources and can make your system slower.
A circuit breaker can temporarily stop requests to an unhealthy supplier.
The typical states are:
Supplier Healthy
↓
CLOSED
↓
Requests Pass
↓
Repeated Failures
↓
OPEN
↓
Requests Blocked
↓
Wait / Cooldown
↓
HALF-OPEN
↓
Test Supplier Again
↙ ↘
Success Failure
↓ ↓
CLOSED OPEN
CLOSED
The supplier is healthy.
Requests are allowed to pass normally.
OPEN
The supplier has experienced too many failures.
Requests are temporarily blocked so your application doesn't keep hitting the failing service.
HALF-OPEN
After a cooldown period, a small number of requests are allowed to test whether the supplier has recovered.
If the supplier is healthy, the circuit returns to CLOSED.
If it fails again, the circuit returns to OPEN.
This prevents one unhealthy dependency from consuming resources continuously.
4. Use Concurrent Requests
Calling suppliers one after another can increase the total response time.
For example:
Supplier A → 2s Supplier B → 5s Supplier C → 8s Supplier D → 3s Supplier E → 4s Total ≈ 22 seconds
If those calls are independent, processing them sequentially is inefficient.
Instead, aim to process independent supplier requests concurrently:
┌── Supplier A → 2s
│
├── Supplier B → 5s
│
User → OTA ──────┼── Supplier C → 8s
│
├── Supplier D → 3s
│
└── Supplier E → 4s
The overall processing time can be closer to the slowest supplier rather than the sum of every supplier's response time.
Laravel provides HTTP concurrency capabilities that can be used for independent requests.
For example:
use Illuminate\Http\Client\Pool; use Illuminate\Support\Facades\Http; $responses = Http::pool(fn (Pool $pool) => [ $pool->as('supplier_a') ->timeout(15) ->get($supplierAUrl), $pool->as('supplier_b') ->timeout(15) ->get($supplierBUrl), $pool->as('supplier_c') ->timeout(15) ->get($supplierCUrl), ]);
The important point is that concurrency should be used where the operations are genuinely independent.
5. Don't Let One Supplier Fail Everything
Suppose five suppliers are queried and one times out.
That shouldn't necessarily mean the user gets no results.
Instead, your aggregator can return partial results from the healthy suppliers.
For example:
{ "suppliers": { "supplier_a": "success", "supplier_b": "success", "supplier_c": "timeout", "supplier_d": "success", "supplier_e": "success" } }
This is called graceful degradation.
Your system continues providing value even when part of the infrastructure is unavailable.
For a search system, this can be much better than returning a complete failure simply because one supplier is unavailable.
6. Normalize Supplier Responses
Every supplier has its own API format.
One supplier might return:
{ "price": 450, "currency": "USD" }
Another might return:
{ "totalAmount": 450, "currencyCode": "USD" }
Your application shouldn't let supplier-specific response structures spread throughout the entire codebase.
Instead, create a common internal format.
For example:
[
'supplier' => 'supplier_a',
'price' => 450,
'currency' => 'USD',
'segments' => [...],
]
Then your application can work with a consistent structure regardless of where the data came from.
A useful architecture is:
Supplier API
↓
Supplier Client
↓
Response Mapper
↓
Normalized DTO / Model
↓
Business Logic
↓
API Response
This makes adding another supplier much easier.
7. Separate Search From Booking
Search and booking have very different reliability requirements.
Search is usually:
User ↓ Search ↓ Multiple Suppliers ↓ Aggregate Results
Booking is more sensitive:
User ↓ Price Check ↓ Availability ↓ Booking ↓ Payment ↓ Ticket / Confirmation
A failed search request can usually be retried or ignored.
A failed booking request requires much more careful state management.
You should track transaction states explicitly.
For example:
INITIATED ↓ PRICE_CHECKED ↓ BOOKING_PENDING ↓ BOOKED ↓ TICKETED
And failure states:
BOOKING_PENDING
↓
FAILED / UNKNOWN
↓
Reconcile with supplier
This is especially important when the network fails after the supplier has already processed the request.
8. Use Queues for Non-Critical Work
Not everything needs to happen inside the user's HTTP request.
Tasks such as:
- Sending emails
- Generating reports
- Sending notifications
- Synchronizing data
- Updating analytics
- Processing supplier callbacks
can often be moved to queues.
For example:
SendBookingEmail::dispatch($booking);
Then a Redis-backed queue worker can process the task:
php artisan queue:work redis
This keeps the user-facing request focused on the work that actually needs to finish before responding.
9. Monitor Supplier Performance
Once you have multiple suppliers, monitoring becomes extremely important.
Track things such as:
- Average response time
- Timeout rate
- Error rate
- Successful searches
- Failed bookings
- Supplier availability
- Retry count
- Circuit-breaker state
- HTTP status codes
- Request volume
For example:
Supplier A → 98% success → 2.1s average Supplier B → 94% success → 4.8s average Supplier C → 71% success → 12.4s average
This information helps you identify unreliable suppliers and optimize your search strategy.
You can also track supplier performance over time:
Supplier Success Avg Response Timeout ------------------------------------------------- Supplier A 98.2% 2.1s 0.4% Supplier B 94.7% 4.8s 2.1% Supplier C 81.3% 9.6s 8.7%
Observability turns production problems into measurable engineering problems.
10. Think About Failure Budgets
One common mistake is trying to make every supplier call perfect.
In a distributed system, that is unrealistic.
Instead, define acceptable limits.
For example:
Connection timeout: 3 seconds Request timeout: 15 seconds Retries: 2 Circuit threshold: 5 failures Cooldown: 60 seconds
These values are examples, not universal rules.
They should be based on your application's requirements, supplier behavior, traffic volume, and business priorities.
A Practical Architecture
Putting everything together:
┌── Supplier A
│
├── Supplier B
│
User → API → Search ├── Supplier C
│
├── Supplier D
│
└── Supplier E
│
↓
Timeout / Retry
│
↓
Circuit Breaker
│
↓
Response Mapper
│
↓
Result Aggregator
│
↓
Final Results
The goal is to isolate failures rather than allowing them to propagate through the entire application.
Common Mistakes to Avoid
❌ No timeout
Never assume an external API will always respond quickly.
❌ Unlimited retries
Retries can make an already unhealthy service even worse.
❌ Retrying non-idempotent operations
Be extremely careful with booking and payment operations.
❌ Sequential supplier calls
Independent supplier requests can often be processed concurrently.
❌ One supplier failure breaks the entire search
Use graceful degradation where the business logic allows it.
❌ No supplier monitoring
If you don't measure supplier performance, you won't know which dependency is hurting your system.
❌ Mixing supplier-specific logic everywhere
Keep supplier integrations isolated behind dedicated clients and adapters.
Final Thoughts
A travel API aggregator is essentially a distributed system.
Your application sits between users and multiple external systems that have different:
- Response times
- Availability
- Error behaviors
- Rate limits
- Pricing rules
- Booking processes
- Data formats
You cannot control all of these systems.
But you can control how your application reacts to them.
A resilient aggregator should use:
Timeouts → Retries → Circuit Breakers → Concurrent Requests → Response Normalization → Graceful Degradation → Monitoring
The goal isn't simply to make every API call succeed.
The goal is to make sure your platform continues working even when some API calls don't.
In distributed systems, failure is not an exception. It's something you should design for.
Key Takeaways
- Always configure sensible timeouts.
- Retry temporary failures carefully.
- Use circuit breakers for repeatedly failing suppliers.
- Process independent supplier calls concurrently.
- Return partial results when appropriate.
- Normalize supplier responses.
- Treat booking and payment operations differently from search.
- Move non-critical work to queues.
- Monitor supplier performance continuously.
- Design your system expecting external failures.
A reliable travel platform isn't built by assuming every dependency will work.
It's built by designing for the moment when something doesn't.