ESC

Type to search the knowledge base.

Infinite Maps Marker UI

Frontend system design for map UIs — viewport queries, marker clustering, tiles, performance, and selection UX.

advanced4 min read
  • system-design
  • interview
  • architecture
  • maps

Scope the problem

Design a map-heavy UI (store locator, listings on map, delivery tracking): pan/zoom loads relevant markers without melting the browser.

In scope: viewport-driven data fetch, clustering, marker rendering performance, detail selection, mobile gestures.

Out of scope: building a tile server; use Mapbox/Google/MapLibre as the engine.

Requirements

Type Examples
Functional pan/zoom, markers, click → detail, search this area
Non-functional 60fps pan; don’t fetch whole world
Scale 100k catalog → tens of markers on screen

Architecture

MapEngine (MapLibre/GL)
├── Tile layer (raster/vector)
├── Marker layer / GeoJSON source
├── Cluster layer
└── UI overlay (React)
     ├── Search / filters
     ├── Result list (sync with map)
     └── Selected card

Keep map camera state as source of truth for “what data to load”; list and map stay coordinated.

Data loading: viewport queries

type BBox = {
  west: number;
  south: number;
  east: number;
  north: number;
};

// GET /pois?bbox=w,s,e,n&zoom=12&filters...

On moveend (debounced 200–300ms):

  1. Read bounds + zoom
  2. Abort prior request
  3. Fetch POIs for bbox
  4. Diff into map source

At low zoom, don’t return raw 50k points — server returns clusters or aggregated counts.

Tile/vector alternatives

  • Server vector tiles with points already baked — great at scale
  • Client clustering libraries for medium sets

Clustering

zoom < 12 → cluster bubbles with counts
zoom ≥ 14 → individual markers

Click cluster → easeTo zoom expand. Accessibility: list panel still browsable without map precision.

Rendering performance

Technique Why
Map GL layers not DOM markers GPU batching
DOM markers only for selected/hover expensive
Limit labels collision
Simplify geometries tile size
Worker for heavy GeoJSON prep main thread

Avoid React re-rendering thousands of marker components — use map source setData.

State model

type MapUiState = {
  center: [number, number];
  zoom: number;
  bounds: BBox;
  selectedId: string | null;
  filters: Filters;
  items: Poi[]; // last fetch
};

URL sync: ?lat=&lng=&z=&id= for shareable views.

List ↔ map sync

  • Hover list row → highlight marker
  • Click marker → scroll list into view
  • Filters apply to both; empty states clear layers

Mobile UX

  • Full-screen map + bottom sheet results
  • Large hit targets
  • Respect reduced data: lower tile quality
  • Geolocation permission priming

Caching

Layer What
HTTP tile caching by CDN
Memory recent bbox results keyed by quantized bounds
Session last camera

Quantize bbox keys to improve cache hits (toFixed(3)).

Offline

Limited: cache recent tiles + last POIs; show stale banner. Full offline maps are product-specific downloads.

Accessibility

  • Don’t map-only: provide list and search
  • Keyboard: list navigation primary; map is progressive enhancement
  • Announce selected place name

Tradeoffs

  1. DOM markers vs GL layers
  2. Server clusters vs client
  3. Fetch on move vs “Search this area” button (cost control)
  4. Third-party map cost/quotas vs self-host tiles

Interview close

Viewport bbox queries with abort → zoom-dependent clustering → GL layers for volume → React for chrome only → URL camera state → list fallback for a11y. Call out not downloading the world.

Filter + map composition

Filters (open now, category, price) always hit the server with bbox. Client-only filtering of a previous world download does not scale. Debounce filter chips with the same moveend coalescing so you don’t N× network.

Marker states

default · hover · selected · clustered

Only selected uses a heavier DOM card anchored to projection coordinates updated on move.

Testing strategy

  • Unit: bbox serialization, cluster expand zoom math
  • Integration: mock map events → assert fetch called with bounds
  • Visual: reduced-motion; high-contrast pins

Cost control

“Search this area” button instead of fetch-on-every-pan reduces API bills for browsing users. Auto-search is nicer UX — gate it behind zoom ≥ threshold.

Further reading