DEV Community

Aftab Bashir
Aftab Bashir

Posted on

I built a Travel Data Platform API in .NET - search, analytics, and recommendations with Redis caching

Most travel APIs I have seen treat search, analytics, and recommendations
as completely separate systems. Different teams, different databases,
different codebases. This project explores what it looks like to build
all three on a single clean data model, with caching built in from the
start rather than bolted on later.

What it does

The platform has four areas.

Ingestion accepts bulk flight and user data via POST endpoints.
In a real system this would connect to partner feeds or scrapers.
Here it seeds 240 flights across 12 routes with realistic pricing.

Search lets you filter flights by origin, destination, airline,
price, and departure date. All results are paginated. Every unique
combination of filters gets its own Redis cache key, so a repeated
search returns in under 100ms instead of hitting the database.

Analytics exposes three aggregated views - popular routes ranked
by volume, monthly price trends for any route, and peak travel
periods by month. These are the endpoints a commercial or product
team would actually use to make decisions.

Recommendations takes a user ID and returns personalised flights
based on their preferred origin, airline, and maximum budget. It
falls back gracefully when preferred airline results are sparse,
topping up with other options so the user always gets a full list.

The caching strategy

Every read endpoint uses Redis with a cache-aside pattern. The key
is built from every filter parameter so different queries never
collide with each other.

var cacheKey = $"flights:{origin}:{destination}:{airline}:{maxPrice}:{page}:{pageSize}";

var cached = await cache.GetStringAsync(cacheKey);
if (cached is not null)
    return Ok(JsonSerializer.Deserialize<PagedResult<Flight>>(cached));

// ... query the database ...

await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result),
    new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
    });
Enter fullscreen mode Exit fullscreen mode

TTLs are tuned per endpoint. Search results expire in 5 minutes
because flight availability changes. Analytics results last 15
minutes because aggregates change slowly. Recommendations sit at
10 minutes - long enough to feel fast, short enough to stay
relevant.

A cold search on LHR to DXB takes around 500ms. The same search
cached returns in under 100ms. That gap matters at scale.

Pagination

Every list endpoint returns a typed wrapper instead of a raw array.

public class PagedResult<T>
{
    public IEnumerable<T> Items      { get; set; } = [];
    public int            TotalCount { get; set; }
    public int            Page       { get; set; }
    public int            PageSize   { get; set; }
    public int            TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
    public bool           HasNext    => Page < TotalPages;
    public bool           HasPrevious => Page > 1;
}
Enter fullscreen mode Exit fullscreen mode

The client gets everything it needs to build pagination controls
without making a second request for the total count.

What I learned

Cache serialization needs a concrete type. Early on I cached
anonymous types and deserialized to object. The properties came
back as JsonElement instead of named fields, so the JavaScript
dashboard got undefined everywhere. Switching to named classes
fixed it cleanly.

Redis and PostgreSQL complement each other well. PostgreSQL
handles the complex GROUP BY queries that power analytics.
Redis handles the repeated reads that would hammer the database
under load. Neither tries to do the other's job.

Seed data matters for demos. Random data with a fixed seed
produces realistic-looking results every time. Using new Random(42)
means the demo looks the same whether you run it locally or show
it to someone in an interview.

The stack

Layer Technology
API ASP.NET Core 9
Database PostgreSQL + Entity Framework Core 9
Caching Redis (StackExchange.Redis)
Logging Serilog
API docs Scalar

Try it yourself

Source code is on GitHub:
https://github.com/aftabkh4n/data-platform

Clone it, spin up the Docker containers, and run the API.
The database seeds automatically on first run.


If you have questions or feedback, drop a comment below.

Top comments (0)