Skip to content

Instantly share code, notes, and snippets.

@Mk-Etlinger
Last active October 31, 2025 05:00
Show Gist options
  • Select an option

  • Save Mk-Etlinger/9314453a47482bdce5796c1bd8e7dc34 to your computer and use it in GitHub Desktop.

Select an option

Save Mk-Etlinger/9314453a47482bdce5796c1bd8e7dc34 to your computer and use it in GitHub Desktop.
Capital RX Backend Design Problem
// Design a system for interfacing with multiple flight APIs
// Technical Requirements:
//
// 1. The system should be flexible enough to trivially add new flight APIs
// 2. The flight responses should conform to an internal schema/interface
// Questions:
//
// 1. How many different APIs/Data sources do we need to interact with?
// 2. What are some examples of API clients and responses?
// 3. What are the inputs and the outputs of the system?
// 4. What are the main complexities of the system?
// 5. What are the domain boundaries in this system?
// 6. What type of data are we working with? JSON, xml, etc? Filesystem vs APIs
// 7. How many active users?
// 8. What are the data access patterns?
// 9. How do we test this effectively?
// Thoughts:
//
// I immediately think of the adapter pattern. Abstracting away the implementation details
// in favor of a uniform API where each implementation handles a way to get data and transform it.
// Without knowing the answer to all of these questions, I want to start as simply as possible.
// As Gall's law states: "A complex system that works is invariably found to have evolved from a simple system that worked".
// So in this case we start as simple as possible for the immediate needs of our users.
// I tend to take a functional approach unless there are good reasons to use classes.
// A good approach is to identify the distinct behaviors or domains and think about those separately
// Then, after some prototyping, figure out where those behaviors
// Domains/ Concerns
// Data fetching
// 1. API call
// 2. Potentially filesystem or other call
// 3. DB
// Aggregator
// Take all flight responses and aggregate, sort, dedupe, filter them together
// Transformer
// For each data source, transform that data into an InternalFlight shape
// Assumptions
// 1. For the purposes of keeping it simple, I'm going to assume API clients only
interface DataClientConfig {
secret: string;
clientId: string;
}
interface DataClient {
get: (options: { url: string }) => Promise<Response>;
}
interface Adapter {
getFlightsByDate: (date: string) => Promise<InternalFlight>;
}
function createApiClient(config): DataClient {
// Hand waving createHttpClient
const client = createHttpClient(config);
async function get(options): Promise<Response> {
return client.get(options);
}
return {
get,
};
}
function createKayakAdapter(dataClient: DataClient): Adapter {
async function getFlightsByDate(date: string) {
const url = `https://api.kayak.com?date=${date}`;
const response = await dataClient.get({ url });
return transformResponse(response);
}
function transformResponse(
externalResponse: KayakFlightResponse // handwaving this type
): InternalFlight {
return {
date: externalResponse.date,
price: externalResponse.price,
flights: [
{
departureAirport: {
name: externalResponse.depature.airport.name,
id: externalResponse.id,
time: externalResponse.time,
},
arrivalAirport: {
name: externalResponse.arrival.airport.name,
id: externalResponse.id,
time: externalResponse.date,
},
duration: externalResponse.duration,
},
],
};
}
return {
getFlightsByDate,
};
}
interface InternalFlight {
date: string;
price: number;
flights: [
{
departureAirport: {
name: string;
id: string;
time: string;
};
arrivalAirport: {
name: string;
id: string;
time: string;
};
duration: Number;
}
];
}
function aggregator(data: InternalFlight[]) {
// Can do sorting, filtering, deduping etc
// Could break out these items to separate concerns but hand waving for now
}
async function getFlights(date: string) {
const config: DataClientConfig = {
secret: "superSecretSecret",
clientId: "1234",
};
const client = createApiClient(config);
const kayakAdapter = createKayakAdapter(client);
const kayakFlights = await kayakAdapter.getFlightsByDate(date);
// We probably want to fetch concurrently and we can have a registry to iterate through
// and call adapter.getFlightsByDate() and then aggregate all the responses etc.
const flights = aggregator([kayakFlights]);
return flights;
}
// Final Thoughts:
//
// This approach is very functional which has some tradeoffs. But this is relatively simple
// And simple is a good start. I'm sure you have feedback and this is something we can iterate on.
// For example: I assume that we only ever have an API client. You mentioned perhaps we're reading XML
// or getting data from different sources like a filesystem, or s3. Those requirements might inform
// a better abstraction so that the data fetching client is agnostic to its source.
//
// In terms of testing: By using dependency injection, we simplify a lot. You can create a mock client
// and then return the data you expect. Each piece of functionality is separated which is good for testing.
//
// Tradeoffs: This approach definitely has tradeoffs. There's some boilerplate for example.
// You have to instantiate an API client, and then pass that to an adapter. If there's only
// 2 APIs we interface with, this might be overkill. With the requirement that we want to be able
// to extend easily, this seems like a sufficient starting point.
// Thanks for reading!
// - Mike
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment