Server-Side Routing: Build an Abstraction Layer to Switch Between Maps Providers
ArchitectureAPIsMapping

Server-Side Routing: Build an Abstraction Layer to Switch Between Maps Providers

uuntied
2026-01-27
9 min read
Advertisement

Design a routing abstraction layer to switch Google/Waze/OSRM without touching business logic. Practical pattern, starter repo layout, and latency strategies.

Stop Tying Business Logic to One Maps Vendor — Build an Abstraction Layer

Hook: If your delivery app, fleet platform, or location-based feature is brittle because routing logic is scattered across services that call a single maps provider, you’re bleeding developer velocity and locking yourself into vendor pricing and latency characteristics. In 2026, teams need an explicit abstraction layer for maps and routing: one that lets you swap providers, add fallbacks, and reason about latency and cost without touching business logic.

Why this matters in 2026

Over 2024–2026 we've seen three clear trends that make provider-agnostic routing indispensable:

  • Edge and multi-cloud deployments shifted traffic patterns: latency varies by region and provider more than it used to.
  • Open-source routing backends (OSRM, Valhalla, and GraphHopper) matured and became cost-effective alternatives for high-volume routing use cases.
  • Regulatory and cost pressures pushed teams away from single-vendor lock-in — teams plan for provider switching as a feature.

That means architects and platform engineers must design a routing abstraction that encapsulates provider differences, exposes a stable interface to business domains, and supports runtime switching and reliable fallbacks.

High-level architecture pattern

Keep the routing concerns inside a single bounded context: a Routing Service (or library, depending on scale). The service implements the Adapter Pattern: it exposes a concise, domain-oriented interface (the interface your application code calls) and maps those calls to provider-specific adapters.

Core principles

  • One public interface for business logic — route calculation, ETA, cost estimation, and snapping/waypoint handling.
  • Adapters implement provider specifics: request shaping, authentication, response normalization.
  • Provider registry & selection — select provider by config, region, or runtime metrics.
  • Fallback & hedging — retry strategies and racing requests to lower tail latency.
  • Observability — measure latency, success rate, cost per call, and error types per provider.

Where to host the abstraction

  • Small teams: ship as a library consumed by services (npm, pip, Maven).
  • Growth teams: deploy as a dedicated Routing microservice behind an internal API gateway.
  • Latency-sensitive fleets: deploy provider-specific adapters closer to the edge or regionally to remove cross-region hops.

Designing the public interface

The public interface should be domain-focused, not provider-focused. Here’s a minimal TypeScript-style contract that we'll use in examples below:

export interface RoutingResult {
  polyline: string; // encoded polyline
  distanceMeters: number;
  durationSeconds: number;
  steps?: Array<{instruction: string; distance: number; duration: number}>;
}

export interface RouterProvider {
  route(origin: {lat:number;lng:number},
        destination: {lat:number;lng:number},
        opts?: {profile?: 'driving'|'walking'|'cycling'|'truck'; alternatives?: boolean})
        : Promise<RoutingResult>;
  reverseGeocode?(lat:number, lng:number): Promise<string> // optional
}

Keep the interface small and focused. Business logic only needs route, ETA, and occasionally reverse geocoding or snap-to-road. Avoid leaking provider-specific options into the contract.

Adapter implementations: example providers

Examples of providers you’ll want to support in 2026:

  • Commercial: Google Maps Directions API (still dominant for consumer mapping), and partner APIs like Waze where available for traffic/live-routing via business partnerships.
  • Open-source / self-hosted: OSRM (super-low latency, C++), Valhalla, and GraphHopper (Java) — all matured by 2025 and widely used in production.

Below are trimmed adapter examples (TypeScript / Node). These focus on normalization — building a RoutingResult regardless of provider shape.

GoogleMapsAdapter (TypeScript)

class GoogleMapsAdapter implements RouterProvider {
  constructor(private apiKey: string) { }

  async route(origin, destination, opts = {}) {
    const url = `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.lat},${origin.lng}` +
      `&destination=${destination.lat},${destination.lng}&key=${this.apiKey}&mode=${opts.profile || 'driving'}`;

    const res = await fetch(url);
    const body = await res.json();

    if (body.status !== 'OK') throw new Error(`Google Directions error: ${body.status}`);

    const leg = body.routes[0].legs[0];
    return {
      polyline: body.routes[0].overview_polyline.points,
      distanceMeters: leg.distance.value,
      durationSeconds: leg.duration.value,
      steps: leg.steps.map(s => ({instruction: s.html_instructions.replace(/<[^>]+>/g, ''), distance: s.distance.value, duration: s.duration.value}))
    };
  }
}

OSRMAdapter (TypeScript)

class OSRMAdapter implements RouterProvider {
  constructor(private baseUrl: string = 'http://osrm:5000') {}

  async route(origin, destination, opts = {}) {
    const profile = (opts.profile === 'truck') ? 'truck' : (opts.profile || 'driving');
    const url = `${this.baseUrl}/route/v1/${profile}/${origin.lng},${origin.lat};${destination.lng},${destination.lat}?overview=full&alternatives=${opts.alternatives?1:0}`;

    const res = await fetch(url);
    const body = await res.json();
    if (body.code !== 'Ok') throw new Error(`OSRM error: ${body.code}`);

    const route = body.routes[0];
    return {
      polyline: route.geometry, // polyline6 or polyline depending on server
      distanceMeters: Math.round(route.distance),
      durationSeconds: Math.round(route.duration),
      steps: [] // step parsing omitted for brevity
    };
  }
}

Provider registration & runtime selection

Register adapters and expose a provider selection policy. You want three selection modes:

  • Static — config selects provider (good for testing / canary).
  • Region-aware — choose provider by geo/region to reduce latency and respect data residency.
  • Metric-driven — choose provider using recent latency/error metrics and cost constraints.

Example registry and selection logic:

class RouterRegistry {
  private providers: Record<string, RouterProvider> = {};
  private metrics: Record<string, {latencyMs:number, errors:number}> = {};

  register(name:string, provider:RouterProvider) { this.providers[name] = provider; }

  async select(region?:string) {
    // simple metric-driven choice: prefer providers with low latency and low error rate
    const entries = Object.entries(this.providers);
    // sort by metrics (older entries get default values)
    entries.sort((a,b) => (this.metrics[a[0]]?.latencyMs||2000) - (this.metrics[b[0]]?.latencyMs||2000));
    return entries[0][1];
  }
}

For production, extend this with rolling-window histograms (Prometheus summaries or HDR histograms), SLO targets, and cost-per-call weighting.

Fallback, hedging, and latency strategies

Latency and tail latencies are the biggest user-facing issues. Consider these patterns:

1) Primary + fallback

Call the primary provider first. If it fails or returns errors, fall back to a secondary provider. Good for cost control but increases error window.

2) Hedged requests (race)

Speculatively send parallel requests to two or more providers and use the first successful response. This reduces p95/p99 latency at a cost of more API calls and potentially higher spend.

3) Early-exit with cached candidate

If a route between two commonly used geohashes exists, return cached polyline immediately while refreshing the cache in background. This gives a fast cached experience and amortizes provider calls.

4) Circuit breaker & backoff

When a provider has elevated errors or timeouts, short-circuit to fallback and auto-heal when metrics recover. Use libraries like opossum (Node) or resilience4j (Java).

Example: hedged race in TypeScript

async function raceProviders(providers: RouterProvider[], origin, destination, opts) {
  // return first successful result within 1s window
  const promises = providers.map(p => p.route(origin, destination, opts).then(r => ({ok:true,r})).catch(e => ({ok:false,e})));
  const results = await Promise.all(promises.map(p => Promise.race([p, new Promise(res => setTimeout(() => res({ok:false,e:new Error('timeout')}), 1000))])));
  const success = results.find(x => x.ok);
  if (success) return success.r;
  throw new Error('All providers failed or timed out');
}

Caching and normalization patterns

Caching decreases cost and lowers latency — but routes are sensitive to traffic. Strategies:

  • Short TTL routing cache (10–60s) for dynamic traffic-aware routes to balance freshness and cost.
  • Long TTL static cache for fixed geometry (e.g., precomputed delivery corridor routes).
  • Keying strategy: use coarse-grained keys like profile:originHash:destHash, where hashes are geohash buckets or S2 cells to avoid exact-match collisions.

Normalize units (meters/seconds) and choose a canonical polyline encoding. Document how rounding and collapsing of step instructions are handled to keep business logic stable across providers.

Observability, SLAs, and cost accounting

Make provider choice data-driven:

  • Collect per-call latency, error type, and cost tags (if provider charges per request).
  • Build dashboards with p50/p95/p99 per-provider latencies and errors.
  • Tag routes by feature (e.g., ETA for dispatch vs. navigation for driver UI) so you can trade off cost and performance.

Automate alerts for provider regressions and run periodic failover drills (simulate provider outage in staging) to ensure the fallback behavior works as intended.

Be careful with provider Terms of Service. Google Maps and Waze have specific display and usage rules; some licenses require showing the provider's map tiles or restrict server-side caching. If you self-host OSRM/Valhalla with OSM data, follow OSM attribution rules and keep up with license changes. See guidance on responsible data and provenance for patterns that help with compliance and auditability.

Starter repo: project layout and usage

Below is a starter repository layout you can copy into your organization. Treat it as a template for a Routing microservice or shared library.

maps-provider-adapter-starter/
├─ src/
│  ├─ adapters/
│  │  ├─ googleMapsAdapter.ts
│  │  ├─ osrmAdapter.ts
│  ├─ index.ts              // public API
│  ├─ registry.ts
│  ├─ selectionPolicy.ts
│  └─ metrics.ts
├─ tests/
├─ docker/
│  └─ docker-compose.osrm.yml
├─ README.md
├─ package.json
└─ infra/
   └─ terraform/ (optional for deploying self-hosted OSRM/Valhalla clusters)

index.ts — public API

export * from './registry';
export * from './adapters/googleMapsAdapter';
export * from './adapters/osrmAdapter';

deployment notes

  • Include example Terraform modules to deploy an OSRM cluster on a regional node pool.
  • Provide Helm charts for the Routing microservice and an optional edge-side adapter as a sidecar for latency-sensitive services.
  • Ship Prometheus metrics (histograms) and Grafana dashboards as part of the repo.

Advanced strategies and future directions (2026+)

As of early 2026, you should consider these advanced techniques:

  • Edge routing adapters: Run lightweight adapters at the edge (Cloud Run, Lambda@Edge, or on-device) to eliminate cross-region hops for latency-critical requests.
  • Model-assisted routing: Use ML models to predict ETA variance and choose providers probabilistically; see this edge-first model case study for patterns that make on-device inference practical.
  • Geo-federated provider pools: Maintain provider pools per region and fail over globally based on regional outages; hybrid orchestration patterns are covered in guides to hybrid edge workflows.
  • Billing-aware throttling: Enforce quotas and degrade features in-app when provider cost thresholds are exceeded — plan your cost tradeoffs like high-throughput platforms do in market-data stacks (see cloud cost & lock-in analysis).

Practical checklist to implement in the next sprint (actionable takeaways)

  1. Define your domain contract (route, ETA, snap) and publish it as the public interface.
  2. Implement at least two adapters (one commercial, one open-source self-hosted) and normalize outputs.
  3. Instrument per-call metrics (latency, success, cost) and create dashboards for provider comparison.
  4. Implement a simple selection policy and a hedged-request experiment to reduce tail latency.
  5. Run a failover drill in staging: disable the primary provider and verify business functionality remains intact.
“Design for change: built-in provider flexibility is not a luxury — it’s a resilience and cost-control strategy.”

Closing: Build the abstraction once, swap providers without chaos

By centralizing routing logic behind a small, well-documented interface and using adapters for each provider, teams can swap Google Maps, Waze (when partner APIs are available), or self-hosted backends like OSRM without changing business code. You gain the ability to optimize for latency, cost, and resilience as declarative platform choices — not emergency rewrite projects.

Starter repo guidance above gives a concrete scaffold you can use today. Adopt the Adapter Pattern, instrument aggressively, and run failover drills. In the current multi-cloud, edge-forward landscape of 2026, that design buys you developer velocity and operational freedom.

Call to action

Ready to decouple your routing? Clone the starter layout into your org, implement two adapters (Google and an open-source backend), and run a hedged-request test in staging this week. If you want a review of your adapter design or help setting up observability, reach out to untied.dev for a design session.

Advertisement

Related Topics

#Architecture#APIs#Mapping
u

untied

Contributor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

Advertisement
2026-02-04T00:27:53.975Z