androidinterview.com

Android System Design Interview Questions

How do you implement a hotel list and detail screen? Discuss the APIs you would create and how the layout will work.

Tier: CommonDifficulty: HardAsked at: booking-com

Design the screens and APIs for finding a hotel and inspecting its details. A user enters a destination, dates and guest count, browses matching properties and opens one to see photos, amenities and available room offers.

The problem

The list should load quickly, support filters and pagination, and preserve the search context when a detail screen opens. The detail screen needs richer information without forcing every list item to download it all. Prices and availability can change while the user browses.

For example, Ana searches for two guests for three nights, opens a hotel and returns to the list. Her filters and scroll position should remain. A cached photo can still be useful, but yesterday's room price must not be presented as a guaranteed current offer.

Focus on Android screen state, response shapes, caching and loading behaviour. Reserving room inventory and processing payment are separate flows. Distinguish stable property information from offers that depend on dates and current availability.

Starting the design

The trap in this question is designing one API that returns everything and one screen that renders it all at once. Hotel data splits into what's stable, photos, amenities and description, and what's volatile, price and availability. Treating those the same is what makes this kind of screen slow and hard to cache.

What I'd clarify first

  • Does search need to support filters, price range, star rating, amenities, and sorting, or is it just a city and a date range.
  • Should the list support a map view alongside it, since that changes what the search response needs to carry, coordinates for every result, not just the ones currently visible.
  • Does the detail screen need to work offline for a hotel the user already viewed, and if so, is showing a stale price acceptable or does that need to be blocked.
  • Is the displayed price the price the user pays, or does the backend add taxes, fees and a currency conversion later, because that gap between the number on the card and the number at checkout is where the complaints come from.

The APIs

Splitting stable data from volatile data across separate endpoints is the core design decision here.

  • GET /hotels/search, query params for city, dates, guests, filters, and a cursor. Returns a lightweight list, id, name, one thumbnail, star rating, review score, distance, and a starting price. Deliberately thin, this is the payload that has to scroll smoothly. Paging is cursor based rather than by offset, because availability changes while the user scrolls, and an offset into a list that is being rewritten underneath you repeats some hotels and skips others.
  • GET /hotels/{id}, the stable detail, full photo gallery URLs, description, amenities, cancellation policy, exact coordinates. This barely changes hotel to hotel over time, so it's the part worth caching aggressively.
  • GET /hotels/{id}/rooms, separate from the detail call, takes the same dates and guest count as the search, returns room types with live pricing and availability. This is volatile by nature and should never be bundled into the cacheable detail payload, otherwise every price refresh forces a fetch again of photos and descriptions that haven't changed.
  • GET /hotels/{id}/reviews, paginated on its own, since a popular hotel can have thousands of reviews and the detail screen shouldn't have to load them all to render its header.

How the layout works

The list screen is a LazyColumn driven by Paging 3 through collectAsLazyPagingItems, or a RecyclerView with the paging adapter on a View based codebase. Either way it is one PagingSource calling the search endpoint page by page, with a deliberate Paging maxSize if loaded pages must be dropped to bound memory. Each row decodes one downsampled thumbnail, not a full gallery, following the same downsampling and cancellation approach as any image loading pipeline, so a fast scroll through hundreds of hotels doesn't queue hundreds of full resolution decodes.

Tapping a row navigates to the detail screen, ideally with a shared element transition on the thumbnail so the tap feels continuous rather than a hard cut. On arrival, the detail screen fires the /hotels/{id} call and the /hotels/{id}/rooms call in parallel, not sequentially. Each section renders as its own data comes back, photo gallery and description first if they resolve first, room pricing filled in a beat later rather than blocking the whole screen on the slower call. Reviews load lazily further down, either a "view all" link into their own paginated screen or a small embedded page that fetches more only as the user scrolls into it.

The search criteria are state, not arguments. Dates, guests, filters and the selected room live in a SavedStateHandle rather than in the composable or the fragment, so a rotation or a process death while the user was in the camera app brings them back to the same list with the same dates rather than to an empty search form.

The price the user finally acts on comes back as a server issued quote with an expiry rather than as a bare number, and that quote id is what the checkout flow charges against. That is the bridge between this screen and booking, and it is what stops a user paying yesterday's price for a room that repriced while the app sat in the background.

How I'd build this on Android

Hotel browsing use casesThe system boundary Hotel browsing holds the use cases Search and filter, Browse more hotels, View rooms, Request a fresh quote. Traveller takes part in Search and filter, Browse more hotels, View rooms, Request a fresh quote.
Hotel browsing use cases, a use case diagram for Hotel browsing
Hotel search and detail componentsList and detail routes to Hotel screen ViewModels, navigate by ID. Hotel screen ViewModels to HotelRepository, query and observe. HotelRepository to Hotel DAO and Hotel APIs, refresh details and prices.navigate by IDquery and observerefresh details and pricesList and detail routesPaged cards and independent sectionsHotel screen ViewModelsSaved criteria and section stateHotelRepositoryHotel details and current pricesHotel DAO and Hotel APIsSearch cursors, details, rooms
Hotel search and detail components
Each arrow shows a call from one component to another.

I'd give the list and detail screens their own ViewModels and share HotelRepository. A Pager belongs to one search, including city, dates, guests, currency and filters. Room stores basic hotel details by hotel ID, but a room price belongs to the full search criteria and expires quickly. SavedStateHandle keeps IDs and small search inputs, not hotel objects or images.

sealed interface SectionState<out T> {
    data object Loading : SectionState<Nothing>
    data class Ready<T>(val value: T) : SectionState<T>
    data object Failed : SectionState<Nothing>
}

suspend fun <T> sectionCall(load: suspend () -> T): SectionState<T> = try {
    SectionState.Ready(load())
} catch (cancelled: CancellationException) {
    throw cancelled
} catch (failure: IOException) {
    SectionState.Failed
} catch (failure: HttpException) {
    SectionState.Failed
}

// Inside HotelDetailViewModel. Each section has its own StateFlow and retry.
fun loadDetail(hotelId: String, criteria: BookingCriteria) = viewModelScope.launch {
    supervisorScope {
        launch { detail.value = sectionCall { repository.detail(hotelId) } }
        launch { rooms.value = sectionCall { repository.rooms(hotelId, criteria) } }
    }
}

The ViewModel keeps mutable detail and room state private and exposes read only state. BookingCriteria holds dates, guests and currency. The repository reads hotel details from the DAO and refreshes room quotes separately. supervisorScope lets one child fail without cancelling its siblings, but it does not catch the exception. sectionCall still has to turn expected failures into a state the section can display. When criteria change, cancel old work or use flatMapLatest and check which request a response belongs to.

The list uses collectAsLazyPagingItems(), hotel IDs as keys and fixed thumbnail bounds. First load, refresh and next page failures need different displays. Navigation passes the hotel ID and search criteria so the detail screen can restore itself. I'd use one vertical lazy list with a horizontal photo row. Each card should not make its own room API request. The booking flow checks the quote again before payment.

I'd inject the API, DAO and clock into the repository. Hilt or a manual factory creates each screen's ViewModel.

I'd check these cases.

  • Changing filters while the next page loads.
  • A room price failure with hotel details still visible.
  • Changing currency.
  • Rotation.
  • An expired quote.

I'd measure time to first content, downloaded bytes and dropped frames while scrolling. See Paging with network and database.

Tradeoffs I'd call out

  • One heavyweight detail endpoint vs several focused ones. A single endpoint is simpler to call, one request, one response, but it means every price refresh drags the whole payload along with it and makes the stable content impossible to cache on its own. Splitting detail from rooms costs an extra round trip but lets the client cache what rarely changes and always hit the network for what does.
  • Thin list payload vs richer list items. Keeping the list response to one thumbnail and a starting price keeps scrolling fast, but if product wants something like "3 room types left," that has to be a small aggregate the backend computes and includes, not something the client derives by calling /rooms for every visible row, that would turn a scroll into a fan out of network calls.
  • Caching a price the user might act on. Caching the stable detail payload for offline viewing is straightforward. Caching a price is riskier, since a stale number is not just an inconvenience but something a user could try to book against. The honest answer is to show cached content with a visible "may be out of date" indicator and require a fresh network check before allowing checkout to proceed.

What breaks at scale, offline, and on a poor connection

At scale, city level searches can return thousands of results, so pagination has to be server side, and filter changes need to be debounced client side rather than firing a new search on every keystroke of a price slider. On a poor connection, the parallel detail and rooms calls should fail independently, if pricing times out, the screen should still show photos and amenities with a retry affordance just for that section, not throw away everything that did load. Offline, a previously viewed hotel's stable detail can render from a Room backed cache, but booking actions should be disabled until connectivity and a fresh price check succeed, since committing to a number that might already be wrong is a business risk, not just a UX one.

Watch