Back to blog

· 8 min read

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

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.

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

  1. Always configure sensible timeouts.
  2. Retry temporary failures carefully.
  3. Use circuit breakers for repeatedly failing suppliers.
  4. Process independent supplier calls concurrently.
  5. Return partial results when appropriate.
  6. Normalize supplier responses.
  7. Treat booking and payment operations differently from search.
  8. Move non-critical work to queues.
  9. Monitor supplier performance continuously.
  10. 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.