Jayden Wang

Full-Stack Engineering · Social Impact · 2025

A platform that matches visually impaired runners with volunteer guides — in real time.

The coordination layer that visually impaired runners and volunteer guides needed simply didn't exist — so I built it, backend first, then the app.

  • Java 17
  • Spring Boot
  • MySQL
  • Redis
  • WebSocket
  • Swift
  • SwiftUI
They had the runners and the guides. The coordination layer that connected them simply didn't exist.
role
Backend: sole engineer. iOS: took over the app and led it from there.
my contribution
I built the entire backend alone — matching engine, real-time location, identity verification, concurrency safeguards. A teammate started the iOS app; I took it over in July 2026 and wrote most of what shipped since, including SOS, live companion tracking, voice ordering, and the accessibility work.
status
Pre-launch · iOS app in internal acceptance testing
Matching happens serially — exactly one guide is ever committed per request, on purpose.
Matching happens serially — exactly one guide is ever committed per request, on purpose.

Why this project

I run, and at a road race in Shenzhen I found blind runners doing the same. For many of them a guided run is one of the few standing reasons to leave the house, and the clubs that organize it had runners, had volunteers, and had no way to put the two together: coordination ran on phone calls and group chats, and volunteer certification ran on one organizer traveling city to city. I built the layer that was missing, then guided a run myself and rebuilt the parts that had quietly assumed a sighted user.

In China, where I live, accessible running programs are rare. A blind runner who wants to run today often can’t, not because there’s no willing guide, but because there’s no way to find one in time.

The matching problem

The obvious design — broadcast a request to every nearby guide, first to accept wins — is wrong. It creates races (two guides both commit), pressures guides into snap decisions, and gives the runner no say in who shows up. I went with serial dispatch: score every eligible guide, then offer the run to them one at a time, best match first. The cover diagram shows the shape of it.

Certification isn’t part of the score — an unverified guide is filtered out of the candidate pool entirely before scoring begins. What’s left gets weighed on five dimensions, out of 100:

DimensionWeightWhat it weighs
Distance40how close, right now
Schedule overlap25free for this particular window
Past rating20feedback from previous runners
Pace match10can they actually run this runner’s pace
Acceptance rate5do they usually show up when asked

Pace was the dimension I underestimated. A guide who is close, free, and well-rated is still the wrong guide if they can’t hold the runner’s pace for 10 km.

Three engineering problems that surprised me

These are the bugs that don’t show up until real concurrency hits.

1. The rate limiter that could lock someone out forever

My first rate limiter did increment, then expire as two separate Redis commands. The failure mode isn’t the one you’d guess. If the service died in the gap between the two, the counter survived with no TTL at all — so that key never expired, and that user was rate-limited permanently, with nothing in the logs to explain why. The fix was to collapse both commands into one Lua script, which Redis executes atomically: the counter and its expiry are now set together or not at all.

2. Two guides, one run

Serial dispatch already means only one guide holds an offer at a time, and an accept is checked against who the run was actually offered to. But “already correct by design” is not the same as “safe under concurrency,” so the order row also carries a version column: a write only commits if the version hasn’t changed since it was read. Three layers — serial dispatch, ownership check, and the version guard — and only the last one is there purely for the millisecond-apart case.

3. Emergencies must never wait behind normal traffic

Emergency escalation flow: SOS triggers immediate contact notification, with volunteer confirmation running in parallel
Fig 3 — emergency contacts are notified the moment SOS fires; volunteer confirmation runs beside it, not in front of it.

My first version escalated serially: notify the volunteer, wait 30 seconds for a confirmation, and only then reach the runner’s emergency contacts. I replaced it, because that design has an obvious victim — a runner who has actually fallen, whose family sits through a wasted 30-second round before anyone tells them. Now the emergency contacts are notified the instant SOS fires, and the volunteer notification runs in parallel as a side path. The 30-second timer still exists, but it’s only a volunteer-confirmation timeout now; nothing that matters waits on it.

Underneath, async work runs on a bounded pool with caller-runs backpressure — 10 core threads, 50 max, a 200-deep queue. When the queue fills, the calling thread runs the task itself, which slows the producer down instead of dropping work or growing memory without limit.

Building for real users

Layered backend architecture: mobile clients, API gateway, matching service, Redis, MySQL, bounded thread pool
Fig 2 — system architecture.

Real-time location runs over WebSockets, with a session registry backed by Redis pub/sub so the service can run on more than one instance without a rewrite. Identity verification is its own pipeline, using liveness face-check. Redis carries the locks and rate limits that keep the concurrency honest, and APNs is the fallback when a phone’s socket is gone.

Two features I didn’t anticipate needing, and now consider the most important ones:

  • Voice ordering. Booking a run without touching the screen — speech goes to an LLM that extracts the slots (when, where, how far).
  • A phone call before the run is committed. Runner and guide talk first, over a privacy-masked number, and either can walk away. Matching two strangers who are about to be physically tethered to each other turns out to be a social problem before it’s a scheduling one.

The app

I took over the iOS app in July 2026 and wrote the majority of it from there: 92 Swift files, roughly 39,000 lines of SwiftUI.

Accessibility is the whole point of this app, so it isn’t a pass at the end — 382 accessibility labels, 210 hints, explicit VoiceOver focus management so the reader lands where it should after a state change, and adaptation for both reduced-motion and differentiate-without-color. There’s a dedicated audit test suite for it: twenty audits over missing labels, truncated text, contrast and Dynamic Type, plus hand-written assertions for the two things that audit can’t see — traversal order, and the repeat-current-state button every screen owes a user who looked away.

Status

Not shipped. Internal acceptance testing with community partners is underway, and the remaining blockers are honest ones: push notifications need a paid developer account to get the entitlement, the privacy policy still has no in-app entry point, and a fixed test verification code is still enabled. Those have to be closed before this can go near the App Store.

This is the project I’m proudest of — not because it’s the most technically dense, but because the coordination layer these runners needed genuinely didn’t exist.

results

  • 877 automated tests
  • 104 API endpoints
  • 1,200 concurrent WebSocket connections verified