DK Back to case study

System design: how Signal would actually run

Architecture, data model, and the key flows behind the score, the deep-dive, and the job feed - plus the constraints that shaped them.

Companion to the Signal product case study & prototype

The system in one paragraph

A mobile client sends a resume, and later a single job description, to a backend that keeps a continuously refreshed index of postings pulled from licensed job-board and ATS APIs. Every posting gets a cheap, instant score against the resume. Opening a specific role can trigger one on-demand call to Claude for a deeper written analysis and resume rewrite. Contact details and applicant-comparison stats come from the same licensed portal APIs - never scraped. Nothing is sent to an employer or a recruiter without the person tapping send.

Architecture

Mobile client - Resume · Matches · Contacts
↓
API gateway - auth, rate limiting, routing
↓
Resume Service
Matching Engine
AI Analysis Service (Claude)
Contact & Applicant-Stats Service
↓
Job Ingestion Pipeline - polls, normalizes, dedupes, stamps freshness
↓
Data layer - Postgres · Redis · object storage
External, licensed only: job-board/ATS APIs · LinkedIn Talent Solutions · Anthropic API - no scraping, anywhere.

Components

Resume Service

Parses pasted or uploaded resumes into structured fields (skills, titles, dates). Scoring runs against this structured form, not raw string search.

Matching Engine

Computes the instant, free score shown in the list - skill overlap, seniority, location/work-style fit, freshness. No LLM involved.

AI Analysis Service

On-demand only. Calls Claude with the resume + one full job description, returns a strict-JSON gap analysis and rewrite suggestions. The only component with LLM cost.

Contact & Applicant-Stats Service

Thin adapter over each portal's official API. Returns "unavailable" honestly when a portal doesn't expose contact or applicant data - never backfills with a guess.

Job Ingestion Pipeline

Scheduled polling of job-board/ATS APIs. Normalizes, dedupes cross-posted roles, and stamps first_seen_at once, honestly, at true ingestion time.

Data layer

Postgres for relational data, Redis for score caching and rate limits, object storage for raw resume files.

Core data model

EntityKey fields
Userpreferences: locations[], work_style[]
Resumeraw_text, structured_json, updated_at
JobPostingsource_portal, title, company_id, location, work_style, skills[], first_seen_at, source_url
Companyname, summary, size_band, site_url
Matchscore, matched_skills[], missing_skills[], computed_at
DeepAnalysismatch_percent, call_likelihood, resume_suggestions[], model_version
Contactname, role, source_portal, retrieved_at
ApplicantStatsapplicant_count, avg_experience_years, source_portal

Key flows

Find matches

  1. Client sends resume text + preferences to the Matching Engine.
  2. Engine scores every active posting, applies preference adjustments, returns a ranked list.
  3. Client renders the list - no LLM call has happened yet.

AI deep-dive (per role, on demand)

  1. User taps "Run live AI analysis" on one job.
  2. Client requests a DeepAnalysis for (resume, job).
  3. Service checks cache; on miss, calls Claude, parses the strict-JSON reply, stores and returns it.
  4. Client renders the written analysis and rewrite suggestions.

Job ingestion (continuous, background)

  1. Scheduler polls each connected portal/ATS API on its own cadence.
  2. New or changed postings are normalized and deduplicated against existing listings.
  3. Postings unseen for N polls are marked stale rather than deleted, so existing Match records stay valid.

Non-functional requirements

Cost control

The heuristic score does the bulk of ranking; the LLM is reserved for explicit deep-dives, keeping spend proportional to real engagement.

Latency

Matches render from cached scores in well under a second; a deep-dive is allowed a few seconds with a clear loading state.

Privacy

Resumes are PII. Encrypted at rest/in transit, full deletion supported, never shared beyond the LLM call itself.

ToS compliance

Enforced architecturally - the Contact & Applicant-Stats Service only has adapters for licensed APIs, so no code path can scrape.

Suggested stack

React Native Node / TypeScript API Postgres Redis S3-equivalent object storage Scheduled worker queue Anthropic API (Claude)

What I'd reconsider at real scale

01

Cross-portal dedup (the same role posted on LinkedIn and a company's own site) needs fuzzy matching in practice - easy to either merge distinct roles or miss real duplicates.

02

Applicant-comparison stats are only as good as what each portal exposes; the honest empty state needs to be a well-designed common path, not an edge case.

03

Ingestion polling cadence is a real cost/staleness trade-off per portal, worth tuning against actual rate-limit data rather than an illustrative range.