androidinterview.com

Android System Design Interview Questions

Solve design problems based on a location based app.

Tier: Less commonDifficulty: Hard

A location based app uses where someone or something is to offer a feature. The prompt needs a concrete use case before it has one design. Finding nearby shops, following a courier and notifying someone when they enter an area are different tasks.

The problem

Pick the user action and expected result first. Nearby search needs a position and a radius. Live tracking needs a sequence of positions over time. A geofence is a geographic boundary whose entry or exit triggers an event.

For example, Find pharmacies within two kilometres needs one useful position and a search of nearby places. It does not automatically require continuous tracking. Follow my courier instead needs repeated updates, their timestamps and a way to show a stale position. Remind me when I reach home needs a boundary crossing event.

Choose one shape, then agree on accuracy, freshness, foreground or background use, consent and battery budget. The walkthrough compares these shapes so the APIs and architecture follow from the actual feature.

Starting the design

Location questions come as a family, and there are only three shapes in it, nearby search, live tracking, and geofencing. Every one of them lands on the same tensions, accuracy versus battery, update frequency versus data cost, and how much position you're willing to expose to other users or store at all. On Android the permission model is the fourth tension, and it shapes the design more than the other three.

The first thing worth doing out loud is mapping the shape onto the API, because picking wrong here is a rewrite rather than a tweak.

Feature shapeWhat the client usesWhat the backend does
Find things near me, onceA single fix from the fused location providerGeohash or S2 range lookup on a spatial index
Live tracking of a moving thingFused provider updates from a foreground service typed for locationWrites the latest position, pushes it out on a live channel
Notify me on entry or exitThe platform geofencing APIAlmost nothing, it only supplies the regions

What I'd clarify first

  • Which of the three shapes is this, since each one is a different Android API and a different backend query, not just a different radius.
  • Does it have to keep working when the app isn't on screen, because that's the line between one permission and three, and it decides whether a foreground service exists at all.
  • Is approximate location good enough, because a user on Android 12 and up can hand you exactly that whether you asked for it or not.
  • Does a fake position pay for anyone here, delivery proof, attendance, a game with rewards, because that turns into a server side problem.

The permission model

This is where most of the actual design problems live, so I'd cover it before anything else.

  • Two foreground permissions, not one. ACCESS_COARSE_LOCATION gives approximate location whose accuracy varies and can be much broader than a city block. ACCESS_FINE_LOCATION gives a precise one. Declare coarse always and fine only if the feature truly needs it, because asking for precision you never use is what gets an app questioned in review.
  • The user can downgrade you. From Android 12 the runtime dialog offers approximate even when you asked for precise, so the feature needs a coarse only mode that still works, a nearby search with a wider radius rather than a broken screen. You can prompt once to upgrade to precise, and you have to take no for an answer.
  • Background is a separate trip. ACCESS_BACKGROUND_LOCATION can't be bundled into the same request. You ask for foreground first, and the background ask sends the user out to Settings to choose allow all the time. Most features don't need it, and Play policy treats it as something you have to justify.
  • Only this time expires. The one time grant has a limited lifetime that depends on the app and service state, so any code that assumes yesterday's permission is still held is a bug. Check on every entry into the feature, don't cache the answer.
  • Foreground service, typed. Continuous user visible trip tracking can use a foreground service declared android:foregroundServiceType="location", with the matching FOREGROUND_SERVICE_LOCATION permission and a visible ongoing notification. Background location has separate limits, and service start and while in use permission rules still apply. Geofencing and occasional background work have different execution models.

The recurring building blocks

  • Fused location provider, not raw GPS, requesting an accuracy and power priority that matches the feature, balanced power for most nearby search work, high accuracy only for something like turn by turn navigation where it genuinely pays.
  • A spatial index on the backend, for anything answering what's near this point. Storing raw latitude and longitude and scanning every row for distance doesn't scale. Encoding position into a geohash or an S2 cell turns a proximity search into an indexed range lookup.
  • Geofencing through the platform API, rather than polling location and computing distances yourself. The OS does the work and your process doesn't have to be running. The limits are the design problems, though, so say them. Geofences need background location to register at all from Android 10. You get 100 per app, so a large set means registering only the ones near the user and registering again as they move. They don't survive a reboot, so you listen for boot completed and put them back. And delivery is throttled, a couple of minutes normally and up to six on a device that's been sitting still in Doze, so nothing time critical should hang off one.
  • Batched, adaptive updates, tied to detected movement, frequent while actually moving and sparse or stopped while stationary, for any feature that isn't a one time lookup.
  • A plausibility check where location has value. Setting a mock provider takes a developer setting and a free app. Location.isMock is the cheap client side check, but the real defence is server side, comparing a claimed position against the last one and rejecting a jump no vehicle could have made.

How I'd reason through a specific variant

For a one time nearby search, coffee shops near me, it's a single fix at balanced power priority, since a slightly stale position doesn't change the results, sent once to a backend that runs the spatial query.

For continuous tracking, a delivery or a ride, it's the adaptive update loop above running inside a typed foreground service, plus a live channel out, WebSocket or SSE, to deliver position changes to whoever's watching.

For geofencing, alert me near this stop, the platform API does the heavy lifting. The app registers a region and a callback and gets woken by the OS, with no polling at all, as long as it accepts that the wake up can be minutes late.

Two of those three are worked through in full elsewhere on this site, and they're worth reading as the long versions of this page. Continuous tracking with a server authoritative trip and a driver publishing from a foreground service is the Uber design. Nearby search with a consent and privacy model on top of the geohash is the near by friends design.

How I'd build this on Android

Location feature Android boundaryLocation route to Feature ViewModel and use case, user action. Feature ViewModel and use case to Repository and LocationSource, request location or places. Repository and LocationSource to Provider, Room and backend, capture and query.user actionrequest location or placescapture and queryLocation routePermission launcher and map or listFeature ViewModel and use caseLoading, permission and fix statesRepository and LocationSourcePlaces and Android location accessProvider, Room and backendLocation age and nearby search
Location feature Android boundary
Each arrow shows a call from one component to another.

I'd put Android location calls behind LocationSource, place data behind a repository and permission requests in the UI. The feature decides how long tracking lives. Nearby search can stop with its screen. An active trip needs a service or another supported platform owner. A geofence receiver handles its short callback and schedules longer work separately.

data class Position(val latitude: Double, val longitude: Double, val accuracyMeters: Float)
interface LocationSource {
    suspend fun current(): Position?
    fun updates(): Flow<Position>
}
interface PlacesRepository {
    suspend fun nearby(position: Position): List<Place>
}
data class Place(val id: String, val name: String)

// A one-shot use case, called from a ViewModel action after permission is granted.
class FindNearbyPlaces(
    private val location: LocationSource,
    private val places: PlacesRepository,
) {
    suspend operator fun invoke(): List<Place>? {
        val position = location.current() ?: return null
        return places.nearby(position)
    }
}

The repository calls the places API and can cache results in Room by region and query. No location fix, permission denied and network failure are different states. A cached position includes its age and accuracy. If it is too old for this feature, ask for a fresh fix. Nearby search can also let the user type a location.

The Compose route owns the Activity Result permission launcher and collects ViewModel state with lifecycle awareness. Camera position and map animations stay in UI state. Place rows use stable IDs. A callback adapter uses callbackFlow and unregisters in awaitClose, as shown in delivery tracking.

I'd inject the source, API, DAO and clock.

I'd check these cases.

  • Approximate permission.
  • Permission revoked during tracking.
  • No fix indoors.
  • Location without internet.
  • Restoring registered regions after reboot.

Approximate permission does not promise one fixed accuracy. A one time grant also does not necessarily expire the instant the screen closes. I'd check current permission when needed instead of relying on a saved result from yesterday. See requesting location access.

Tradeoffs I'd call out

  • Update frequency vs battery. This is the tradeoff every variant comes back to. More frequent, higher accuracy updates make a feature feel live, but continuous high accuracy GPS is one of the fastest ways to flatten a phone, and users notice and uninstall. Tying frequency to detected movement rather than a fixed timer is almost always the right default.
  • Client computed vs server computed geospatial logic. A nearby search run client side against a cached dataset is fast and free, but only works for small, mostly static data. Anything at real scale, or anything that changes often, needs the index server side, with the client just sending a position and receiving filtered results.
  • Precise vs coarse location. Exact coordinates give the best accuracy, but sharing an exact position with other users, or storing it long term, is a real privacy exposure. Snapping to a coarser grid, a geohash cell rather than a raw coordinate, is usually accurate enough for the feature at a much smaller footprint. It also means a leak of your database leaks less.

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

At scale an unindexed proximity query is what falls over first, so this has to be geospatially indexed on the backend whichever variant is being asked about. Without internet, local location sensors may still provide a fresh position. Remote places or another person's position may be stale, so distinguish sensor availability from server connectivity and label cached remote data with its age. On a poor connection, batching those fixes instead of firing a request per fix is what keeps live tracking from either flooding a weak link with tiny frequent requests or falling permanently behind. A queue that grows and flushes in batches degrades gracefully where one request per fix just starts failing.

Read more Request location permissions (opens in a new tab)Create and monitor geofences (opens in a new tab)

Watch