It's been a productive few months. While job searching and sharpening my skills, I built something I now use myself. Today I want to walk through ResumeMatch, a fully serverless AI powered resume analyzer I designed, built, and deployed on AWS from scratch.
TL;DR
- Input: Resume PDF plus job description
- Output: Match score from 0 to 100, missing keywords, experience mismatch warning, rewritten resume
- Stack: S3 to Lambda to Textract to Bedrock Claude Haiku to DynamoDB to API Gateway to CloudFront React SPA
- Hardening: IAM least privilege, API throttling, per user limits, reserved concurrency, structured logs
This project is a direct reflection of the AWS knowledge I built while preparing for and passing the AWS Certified Developer Associate DVA C02 exam. Rather than letting that knowledge sit on paper, I wanted to apply it in a real production system, using the exact patterns the certification emphasizes: event driven Lambda design, IAM least privilege, DynamoDB data modeling, API Gateway throttling, Cognito authentication, and CloudFront SPA delivery.
What It Does
ResumeMatch lets you upload your resume as a PDF, paste in a job description, and receive an AI generated analysis: a match score, missing keywords, experience gap detection, and a rewritten version of your resume with gaps addressed. Everything runs serverlessly on AWS with no backend server to manage.
Example output simplified:
{
"matchScore": 82,
"missingKeywords": ["SASS", "Redux", "Root Cause Analysis"],
"presentKeywords": ["JavaScript", "TypeScript", "Python", "React", "AWS"],
"experienceCheck": {
"requiredYears": "3",
"actualYears": "2.2",
"hasMismatch": true,
"warning": "Resume shows ~2.2 years but JD requires 3+ years."
},
"suggestions": [
{ "keyword": "Redux", "reason": "State management not mentioned", "whereToAdd": "Frontend & UI skills" }
]
}
System Architecture
The core of the system is an event driven pipeline triggered by an S3 upload. When a user uploads a resume PDF, S3 fires an event that invokes a Lambda orchestrator function to run the end to end workflow.
High level flow:
After Textract extracts text from the PDF, the pipeline executes four passes using Amazon Bedrock with Claude Haiku:
- Pass 1 Keyword Extraction: Parse the job description and extract required technical skills, soft skills, tools, experience level, and nice to haves as structured JSON.
- Pass 2 Match Scoring: Compare the resume against extracted keywords using a calibrated rubric across the full 0 to 100 range, returning the score, missing and present keywords, and actionable suggestions.
- Pass 3 Years of Experience Detection: Programmatically calculate years of experience from resume work dates using regex and
dateutil, then use Bedrock only to extract required years from the job description and generate a warning if there is a mismatch. - Pass 4 Resume Rewriting: Rewrite the resume to naturally incorporate missing keywords based on existing experience without fabricating roles, employers, dates, or accomplishments.
All results are persisted in DynamoDB and served back to the frontend through API Gateway.
Frontend and Auth
The frontend is a React and TypeScript single page application deployed on CloudFront with S3 as the origin. Authentication is handled by Amazon Cognito hosted UI with JWT based authorization on API Gateway.
Resume uploads go directly to S3 through pre signed URLs generated by a Lambda function. This keeps the API layer stateless and avoids proxying large files through the backend.
Production Hardening
I treated ResumeMatch like a real production system instead of a demo, focusing on security, cost control, and operational visibility.
- IAM least privilege: Each Lambda has its own execution role scoped only to the services it needs.
- API Gateway throttling: Usage plans and rate limits prevent abuse at the API layer.
- Per user rate limiting: A DynamoDB GSI on
userIdandtimestampenforces a maximum of ten analyses per user per day and is checked before any Bedrock calls. - Reserved Lambda concurrency: Caps simultaneous Bedrock invocations to prevent runaway costs during traffic spikes.
- Structured CloudWatch logging: Errors are logged as JSON with
userIdandanalysisIdfor fast filtering and debugging.
Key Engineering Decisions
A few decisions I made deliberately and would make again.
Programmatic YOE calculation instead of LLM date math.
Early versions asked Bedrock to compute years of experience from resume dates, but the results were unreliable.
I replaced this with a Python date parser that extracts the work experience section and calculates months programmatically using
dateutil.relativedelta. Bedrock now focuses on interpreting the job description and generating a human readable warning.
Calibrated scoring rubric in the prompt.
Without explicit guidance, Claude Haiku tended to anchor scores around the mid range.
Adding a detailed rubric with examples across the full scoring range produced more differentiated and accurate results.
Cross region inference profiles for Bedrock.
Newer Claude models require cross region inference profiles rather than direct model identifiers.
Using the us. prefix routes requests across United States regions automatically, improving availability and reducing throttling errors.
Why not Step Functions.
I considered Step Functions for orchestration but kept the workflow in a single Lambda to reduce complexity and speed up iteration.
If the system grows with more steps, retries, or long running tasks, Step Functions would be a natural upgrade for state visibility and durable retries.
Failure Modes and Fixes
- LLM JSON drift: Enforced strict schemas, validated outputs, and retried with repair prompts when needed.
- DynamoDB type constraints: Normalized numeric and string fields and stored structured outputs consistently for predictable reads.
- Resume format variance: Added guardrails for multi column resumes and common OCR noise patterns after Textract.
- Cost protection: Rate limiting and reserved concurrency ensured Bedrock usage stayed bounded even under repeated calls.
What I Learned
Building ResumeMatch reinforced something I already believed. The gap between a working prototype and a production ready system is where most of the real engineering happens. Getting Bedrock to return consistent JSON, handling DynamoDB constraints, debugging GSI edge cases, and tuning prompts for stable scoring were all problems that only surfaced by shipping and operating a real system.
If you want to explore the live app, visit ResumeMatch and use the demo credentials on the login page to try it.
If you try it, I'd love to hear what you think — feel free to reach out via LinkedIn.
See you in the next post.
Update: Outreach Tracker
Since launching ResumeMatch, I added a new feature that came from a gap I noticed in my own workflow: an Outreach Tracker built into the app as a dedicated nav tab.
While using ResumeMatch to tailor resumes, I was managing contacts and follow ups in a separate spreadsheet. Having that context live outside the tool created friction, so I brought it into the same interface where the resume work was already happening.
What It Does
The Outreach Tracker lets you log and manage professional contacts and follow ups directly inside ResumeMatch. Each entry captures the company, contact name, channel, status, and timestamps, giving you a unified view of your networking activity alongside your resume analyses.
How It's Built
For the initial release, I kept the implementation deliberately simple. Data is persisted in localStorage with full CRUD operations, so everything works immediately with no backend changes. The tracker includes a demo mode that ships with sample data, letting visitors try the feature without signing in.
DynamoDB backed persistence is planned for a later phase, which will sync data per user using the same Cognito auth and API Gateway layer the rest of the app already uses. I intentionally deferred this to ship the feature faster and validate the UX before adding infrastructure.
Building it this way reinforced a principle I keep coming back to: ship the simplest version that solves the real problem, then layer in durability once the interface is proven.
