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
| jobs | say 50k a day, about 1 per second, bursty in business hours |
| workers online | 100k, each sending location every 30 s, about 3k writes per second |
| nearby search | should 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 takenData
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 silent | A check-in deadline. Missed, the job goes back to open with a new version. |
| Push notifications are slow | Workers can also pull the list. Push is a hint, not the source of truth. |
| Location updates spike | Locations 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.