Architecture

Architecting a Real-Time Ride-Hailing Platform with .NET, SignalR, Firebase and Flutter

How we designed the dispatch, live tracking and payment flows of a ride-hailing platform for Finland — and the trade-offs between SignalR and Firebase for real-time data.

A ride-hailing product looks simple from the passenger's seat: tap, wait, ride, pay. Behind that simplicity sits one of the most demanding real-time systems a small team can build. In this article we walk through the architecture we designed for a ride-hailing platform serving the Finnish market — four applications, one backend, and a lot of decisions about what has to be instant and what merely has to be reliable.

The four applications

  • Rider app (Flutter) — booking, live tracking, payments, ratings.
  • Driver app (Flutter) — availability, trip offers, navigation, earnings.
  • Web admin (Angular) — live fleet map, fares, promotions, disputes, reports.
  • Backend API (ASP.NET Core) — the source of truth for trips, users, pricing and payments.

Choosing Flutter for both mobile apps let one team share design tokens, networking and analytics code between rider and driver, while still shipping native-feeling apps on iOS and Android. Angular's strong typing and opinionated structure made it a natural fit for a data-heavy admin console maintained by .NET developers.

Real-time: SignalR or Firebase?

The two hot paths are driver location updates (thousands of small writes per minute) and trip status events (few, but critical). We use both technologies, deliberately:

ConcernSignalR (ASP.NET Core)Firebase Realtime Database
LatencyVery low, persistent WebSocketLow, managed globally
Offline / reconnectManual handlingBuilt-in local cache and sync
Fan-out to many ridersGroups, needs Redis backplane at scaleNative, scales automatically
Business rulesRuns inside your domain codeSecurity rules only
Cost modelCompute you already runPer connection / bandwidth

Our rule: anything that changes money or trip state goes through the API over SignalR; the high-frequency, low-value location stream is mirrored into Firebase so the rider app can subscribe cheaply and keep working through tunnels and dead zones.

C# · ASP.NET Core
// ASP.NET Core — driver location hub
public class TrackingHub : Hub
{
    private readonly ITripService _trips;
    public TrackingHub(ITripService trips) => _trips = trips;

    public async Task UpdateLocation(Guid tripId, double lat, double lng, double heading)
    {
        await _trips.RecordLocationAsync(tripId, lat, lng, heading);
        await Clients.Group($"trip:{tripId}")
                     .SendAsync("driverMoved", new { lat, lng, heading, at = DateTime.UtcNow });
    }

    public Task WatchTrip(Guid tripId) =>
        Groups.AddToGroupAsync(Context.ConnectionId, $"trip:{tripId}");
}

Matching drivers to riders

Dispatch is a geospatial query followed by an offer workflow. We store the last known driver location as a spatial column and query the nearest available drivers within a radius, then rank by ETA from the Google Maps Distance Matrix API rather than straight-line distance — a lake or a railway line makes a big difference in Helsinki.

SQL
-- Nearest available drivers (PostGIS / SQL Server spatial)
SELECT TOP (10) d.Id, d.Location.STDistance(@pickup) AS Metres
FROM   Drivers d
WHERE  d.Status = 'Available'
  AND  d.Location.STDistance(@pickup) < 4000
ORDER  BY Metres;

The offer itself is a small state machine: Offered → Accepted | Declined | TimedOut. A Hangfire delayed job enforces the timeout, so a driver who loses connectivity never blocks the queue.

The rider's live map

In Flutter, the rider screen simply subscribes to a stream of positions and re-renders the marker. Because the stream comes from Firebase, it survives app backgrounding and flaky connections with no extra code.

Dart · Flutter
// Flutter rider app — live driver marker
StreamBuilder<DriverPosition>(
  stream: trackingService.positions(tripId),
  builder: (context, snap) {
    if (!snap.hasData) return const Center(child: CircularProgressIndicator());
    final p = snap.data!;
    return GoogleMap(
      initialCameraPosition: CameraPosition(target: LatLng(p.lat, p.lng), zoom: 15),
      markers: { Marker(markerId: const MarkerId('driver'), position: LatLng(p.lat, p.lng), rotation: p.heading) },
    );
  },
);

Payments, SMS and background work

  • Stripe handles cards, Apple Pay and Google Pay. We authorise an estimate at booking and capture the final fare at drop-off.
  • Twilio sends OTP codes for phone verification and trip notifications where push isn't delivered.
  • Hangfire runs the timers, weekly driver payouts, receipt emails and nightly reports — all visible in its dashboard.
Lesson learned: put idempotency keys on every payment and every status transition from day one. Mobile networks retry; your money flow must not.

Built for Europe

Serving Finland meant euro pricing with VAT breakdowns on receipts, Finnish and English localisation, and strict GDPR handling: location history is pseudonymised after 30 days, and every personal-data export or deletion is a single API call. Hosting in an EU region on Azure keeps data residency simple.

Takeaways

  1. Separate state (API + SignalR) from telemetry (Firebase).
  2. Rank drivers by real ETA, not distance.
  3. Make every transition idempotent and timed.
  4. Share code between rider and driver apps with Flutter packages, not copy-paste.

Planning a ride-hailing, taxi or fleet product? Talk to our team — we've already solved most of the hard parts.

IG
Written by the Ishtar Gate engineering team

Our engineers write about what they build every day — real-time systems, mobile apps, cloud platforms and the trade-offs behind them. Have a question about your own project? We'd love to talk.

All articles

Have an idea? Let's build it together.

Tell us about your product, your timeline and your goals. Within two working days we reply with a clear proposal, an architecture sketch and honest advice.

Emailinfo@ishtar-gate.com Manchester, United Kingdom+44 7503 321169 Baghdad, Iraq+964 770 677 1307