We review pull requests, lint our code, and run tests before shipping. Then we send a resume that we've never verified.
That's strange, because a resume has a "consumer" much like software does. Often the first reader isn't a person but an applicant tracking system (ATS) that parses your file into text and fields. If the parse fails, a human may never see your best work.
This post treats your resume like a build artifact: choose a sensible structure, test the output, and measure it against a spec (the job description).
1. Choose the layout like you'd choose an architecture
Templates come in a few broad families, and each has trade-offs:
| Style | Trade-off |
|---|---|
| Classical (single column, standard headings) | Most predictable for parsers and humans; less visual personality |
| Modern (clean hierarchy, better typography) | Slightly more design, usually still simple |
| Europass (standardized, dense one-pager) | Common in Europe; lots of structure |
| Creative (sidebars, color blocks) | Memorable, but riskier for parsing |
For most developer applications I'd default to classical or modern. A gallery such as Noloii's resume templates lets you compare these side by side, including a developer-oriented option called TechPass in the professional section, plus classical and modern layouts. Some templates are free and some are premium, and each is labeled.
Rule of thumb: the more columns, icons, and graphics, the more ways a parser can scramble your content. Simple layout = fewer failure modes.
2. The "plain text" test
Here's the quickest parsing check: extract your resume's text and read it.
# poppler-utils (Linux: apt install poppler-utils; macOS: brew install poppler)
pdftotext -layout resume.pdf - | less
Read the output top to bottom and ask:
- Is the order logical (name, summary, experience, education)?
- Are columns interleaved, with lines from the sidebar mixed into the main text?
- Did any text vanish (often text inside images)?
- Are bullet characters turned into garbage?
If it reads like nonsense here, a parser will likely struggle too. You can also try the copy-paste version: select all in your PDF viewer, paste into a plain text editor, and look.
3. Lint your headings
Parsers and recruiters both look for conventional section names. "Experience" is safe. "My Journey" is not.
Here's a small script that checks whether standard headings survive extraction:
import re
import subprocess
import sys
EXPECTED = {
"experience": r"\b(work\s+)?experience\b|\bemployment\b",
"education": r"\beducation\b",
"skills": r"\bskills\b|\btechnologies\b",
}
def pdf_text(path: str) -> str:
out = subprocess.run(
["pdftotext", "-layout", path, "-"],
capture_output=True, text=True, check=True,
)
return out.stdout.lower()
def check_headings(text: str) -> None:
for name, pattern in EXPECTED.items():
status = "OK " if re.search(pattern, text) else "MISSING"
print(f"[{status}] {name}")
if __name__ == "__main__":
check_headings(pdf_text(sys.argv[1]))
Run it:
python check_headings.py resume.pdf
[OK ] experience
[OK ] education
[MISSING] skills
A MISSING result means either the heading is named unusually or the text didn't extract. Either is worth fixing.
4. Measure keyword coverage against the job description
Treat the job posting as your spec. A rough way to compare is to count which meaningful terms appear in both the posting and your resume.
import re
import sys
from collections import Counter
STOPWORDS = set("""
a an and are as at be by for from has have in is it of on or that the to
with you your we our will this their they them who what when where which
about into more other such than then these those also can may must should
experience work team role years year ability strong
""".split())
def tokens(text: str) -> list[str]:
words = re.findall(r"[a-zA-Z][a-zA-Z+#.\-]{1,}", text.lower())
return [w.strip(".-") for w in words if w not in STOPWORDS and len(w) > 2]
def coverage(resume: str, job: str, top: int = 25) -> None:
resume_set = set(tokens(resume))
job_counts = Counter(tokens(job))
wanted = [w for w, _ in job_counts.most_common(top)]
hits = [w for w in wanted if w in resume_set]
misses = [w for w in wanted if w not in resume_set]
print(f"Coverage: {len(hits)}/{len(wanted)}")
print("In your resume: ", ", ".join(hits))
print("Not in your resume:", ", ".join(misses))
if __name__ == "__main__":
resume_text = open(sys.argv[1], encoding="utf-8").read()
job_text = open(sys.argv[2], encoding="utf-8").read()
coverage(resume_text, job_text)
Save your resume as text (from the extraction step above) and the job description as job.txt:
pdftotext -layout resume.pdf resume.txt
python keyword_coverage.py resume.txt job.txt
Example output:
Coverage: 17/25
In your resume: python, postgresql, docker, kubernetes, api, ...
Not in your resume: terraform, graphql, observability, ...
How to use the result (honestly)
This is a crude heuristic, not how any specific ATS scores you. Real systems vary, and many don't rank by keyword count at all. Use it as a prompt to ask:
- Did I actually use
terraform, but forget to write it down? - Is the job using different words for something I already did?
Only add keywords for skills and experience you genuinely have. Stuffing in terms you can't back up will fail in the interview, and it makes the resume worse for human readers.
5. Write bullets like commit messages
Good commit messages say what changed and why. Good resume bullets say what you did and what happened.
- Responsible for maintaining backend services
+ Cut API p95 latency from 800ms to 250ms by adding query caching
+ and removing two N+1 queries in the orders service
A reliable formula:
Action verb + what you did + measurable result (or scope).
If you can't measure it, describe scale: team size, users, data volume, or request rate. Only use numbers that are true and you can explain.
6. Keep a single source of truth
Developers hate duplication, and resumes are no exception. A few approaches:
- Keep a master document with everything you've done, then pull a one-page tailored version from it for each application.
- Use structured data. The JSON Resume project defines a schema if you want your resume as data that can render into different layouts.
- Version it in Git. You get history, diffs, and a record of what you sent where.
A minimal structure could look like:
{
"basics": { "name": "Your Name", "label": "Backend Engineer" },
"work": [
{
"name": "Company",
"position": "Software Engineer",
"startDate": "2022-03",
"highlights": [
"Cut API p95 latency from 800ms to 250ms via caching"
]
}
],
"skills": [{ "name": "Backend", "keywords": ["Python", "PostgreSQL"] }]
}
Then the template becomes a presentation layer you can swap, which is the same separation of content and view that we use everywhere else.
Pre-submit checklist
- [ ] Single column (or a simple layout) that extracts in logical order
- [ ] Standard headings: Experience, Education, Skills
- [ ] No important text inside images
- [ ] Bullets show results, not just duties
- [ ] Summary and skills tailored to this job
- [ ] Keywords included only where truthful
- [ ] Exported PDF passes the plain-text test
- [ ] File name is sensible (
firstname-lastname-resume.pdf) - [ ] Proofread by a human (ideally someone else)
A note on "ATS-friendly"
No template can guarantee it gets past every ATS, because systems differ and content matters most. A clean layout reduces avoidable parsing problems. It doesn't replace relevant, well-written content.
Where to start
If you'd rather pick a layout than design one, Noloii's free resume templates collect classical, Europass, modern, professional, and creative designs in one gallery, with sample data in the previews. Pick one, run the plain-text test on your exported file, and iterate.
What's your resume workflow: Word, LaTeX, JSON Resume, something else? Share it in the comments.
Top comments (0)