Job dispatch marketplace0/59 done
Part 3 · System design · Design problems

Job dispatch marketplace

Businesses post jobs with a place and a time window. Nearby qualified workers see them, and one worker gets each job. Design matching and assignment.

Ask first

  • First come first served, or does the business choose from offers?
  • How near is near? Filters for skills and ratings?
  • How many jobs a day, how many workers online?
  • What happens when an assigned worker cancels?
Numbers
jobssay 50k a day, about 1 per second, bursty in business hours
workers online100k, each sending location every 30 s, about 3k writes per second
nearby searchshould answer in about 100 ms
API
POST /jobs { location, skills, window }     { jobId }
GET  /jobs/nearby?lat=..&lng=..&radius=..   [jobs]
POST /jobs/{id}/claim { version }          200 assigned, or 409 taken
Data
jobs(id, status, location, skills, version, assignee_id)  with a geo index
worker locations: Redis GEO, overwritten, never the main database
job_events: one row per state change (audit + outbox)
Deep dive: How do you find nearby fast?

A geo index: PostGIS with a GiST index, Redis GEOSEARCH, or geohash or H3 cells. Look in the job's cell and its neighbours, then filter by exact distance, skills and rating.

Two workers tap Accept at once?

A conditional update: UPDATE ... WHERE id = ? AND status = 'open' AND version = ?. One row updated means you won; zero rows means 409. The row is the lock, no distributed lock needed.

How are workers told?

Push offers to workers in nearby cells, best matches first, in waves: the top 10, then wider after a short wait.

How do you keep states sane?

A state machine: open, assigned, in progress, done, cancelled. Every transition is a conditional update plus an outbox event.

See the core idea run

Core logic (JavaScript, tested)
// Many workers see one open job; exactly one may win. The row is the lock:
// UPDATE jobs SET status = 'assigned', assignee = $1, version = version + 1
//   WHERE id = $2 AND status = 'open' AND version = $3      -- 1 row = you won, 0 rows = 409
function claim(jobs, jobId, workerId, seenVersion) {
  const j = jobs.get(jobId);
  if (!j || j.status !== "open" || j.version !== seenVersion) return false;
  Object.assign(j, { status: "assigned", assignee: workerId, version: j.version + 1 });
  return true;
}
// Nearby: bucket workers into grid cells (stand-in for geohash, H3 or a PostGIS index), scan the 3x3 around the job.
const cellOf = (lat, lng, size = 0.1) => `${Math.floor(lat / size)},${Math.floor(lng / size)}`;
function nearby(index, lat, lng, size = 0.1) {
  const [cy, cx] = cellOf(lat, lng, size).split(",").map(Number), out = [];
  for (let dy = -1; dy <= 1; dy++)
    for (let dx = -1; dx <= 1; dx++) out.push(...(index.get(`${cy + dy},${cx + dx}`) ?? []));
  return out; // then filter by exact distance, skills, rating
}
Failures
Worker accepts, then goes silentA check-in deadline. Missed, the job goes back to open with a new version.
Push notifications are slowWorkers can also pull the list. Push is a hint, not the source of truth.
Location updates spikeLocations are hot, disposable data: Redis only, latest wins, no database writes.
Trade-offs
  • First come first served is simple and fast; offers plus a choice are fairer but slower.
  • Geohash cells shard easily; PostGIS is easier to query.
Say out loud: A geo index finds candidates, offers go out in waves, and claiming is a conditional UPDATE on status and version, so exactly one worker wins without locks. Every state change also writes an outbox event.