Last active
October 31, 2025 05:00
-
-
Save Mk-Etlinger/9314453a47482bdce5796c1bd8e7dc34 to your computer and use it in GitHub Desktop.
Capital RX Backend Design Problem
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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